From 7fb84a23ec1ab008b2e21f02b99efb6640ee7e5d Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Wed, 17 Jan 2024 16:37:46 -0500 Subject: [PATCH 01/34] Add Clone Linode power-off notice --- .../LinodesCreate/SelectLinodePanel.tsx | 12 ++++++----- .../TabbedContent/FromBackupsContent.tsx | 10 ++++++---- .../TabbedContent/FromLinodeContent.tsx | 20 +++++++++++++------ 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx index 1e17f0b2fb3..d1802d0560e 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx @@ -28,7 +28,7 @@ interface Props { handleSelection: (id: number, type: null | string, diskSize?: number) => void; header?: string; linodes: ExtendedLinode[]; - notice?: Notice; + notices?: Notice[]; selectedLinodeID?: number; } @@ -39,7 +39,7 @@ const SelectLinodePanel = (props: Props) => { handleSelection, header, linodes, - notice, + notices, selectedLinodeID, } = props; @@ -72,9 +72,11 @@ const SelectLinodePanel = (props: Props) => { <> {error && } - {notice && !disabled && ( - - )} + {notices && + !disabled && + notices.map((notice, i) => ( + + ))} {!!header ? header : 'Select Linode'} diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx index a8b5eef31ef..9e921edf8d4 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx @@ -120,13 +120,15 @@ export class FromBackupsContent extends React.Component { ), filterLinodesWithBackups )(linodesData)} - notice={{ - level: 'warning', - text: `This newly created Linode will be created with + notices={[ + { + level: 'warning', + text: `This newly created Linode will be created with the same password and SSH Keys (if any) as the original Linode. Also note that this Linode will need to be manually booted after it finishes provisioning.`, - }} + }, + ]} disabled={disabled} error={hasErrorFor('linode_id')} handleSelection={this.handleLinodeSelect} diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx index 5c1d6f5c9b3..1561b92f297 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx @@ -2,12 +2,11 @@ import * as React from 'react'; import { useHistory } from 'react-router-dom'; import VolumeIcon from 'src/assets/icons/entityIcons/volume.svg'; -import { Placeholder } from 'src/components/Placeholder/Placeholder'; import { Paper } from 'src/components/Paper'; +import { Placeholder } from 'src/components/Placeholder/Placeholder'; import { buildQueryStringForLinodeClone } from 'src/features/Linodes/LinodesLanding/LinodeActionMenu'; import { extendType } from 'src/utilities/extendType'; import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; -import { StyledGrid } from './CommonTabbedContent.styles'; import SelectLinodePanel from '../SelectLinodePanel'; import { @@ -16,6 +15,7 @@ import { WithLinodesTypesRegionsAndImages, } from '../types'; import { extendLinodes } from '../utilities'; +import { StyledGrid } from './CommonTabbedContent.styles'; const errorResources = { label: 'A label', @@ -99,10 +99,18 @@ export const FromLinodeContent = (props: CombinedProps) => { extendedTypes, regionsData )} - notice={{ - level: 'warning', - text: `This newly created Linode will be created with the same password and SSH Keys (if any) as the original Linode.`, - }} + notices={[ + { + level: 'warning', + text: + 'This newly created Linode will be created with the same password and SSH Keys (if any) as the original Linode.', + }, + { + level: 'warning', + text: + 'To help avoid data corruption during the cloning process, we recommend powering off your Compute Instance prior to cloning.', + }, + ]} data-qa-linode-panel disabled={userCannotCreateLinode} error={hasErrorFor('linode_id')} From 193f98604bdf971c0b7f8c4c9d8731e8b42d6b5e Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Wed, 17 Jan 2024 16:54:49 -0500 Subject: [PATCH 02/34] Add new feature flag for Linode Clone UI updates --- .../manager/src/dev-tools/FeatureFlagTool.tsx | 1 + packages/manager/src/featureFlags.ts | 1 + .../TabbedContent/FromLinodeContent.tsx | 17 ++++++++++++----- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/dev-tools/FeatureFlagTool.tsx b/packages/manager/src/dev-tools/FeatureFlagTool.tsx index 63db7f3b901..e20d55397fa 100644 --- a/packages/manager/src/dev-tools/FeatureFlagTool.tsx +++ b/packages/manager/src/dev-tools/FeatureFlagTool.tsx @@ -15,6 +15,7 @@ const options: { flag: keyof Flags; label: string }[] = [ { flag: 'aglb', label: 'AGLB' }, { flag: 'aglbFullCreateFlow', label: 'AGLB Full Create Flow' }, { flag: 'dcGetWell', label: 'DC Get Well' }, + { flag: 'linodeCloneUIChanges', label: 'Linode Clone UI Changes' }, { flag: 'metadata', label: 'Metadata' }, { flag: 'parentChildAccountAccess', label: 'Parent/Child Account' }, { flag: 'selfServeBetas', label: 'Self Serve Betas' }, diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index 348503f50da..f47b9daef8b 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -50,6 +50,7 @@ export interface Flags { firewallNodebalancer: boolean; ipv6Sharing: boolean; kubernetesDashboardAvailability: boolean; + linodeCloneUIChanges: boolean; linodeCreateWithFirewall: boolean; mainContentBanner: MainContentBanner; metadata: boolean; diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx index 1561b92f297..7a027f86f48 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx @@ -5,6 +5,7 @@ import VolumeIcon from 'src/assets/icons/entityIcons/volume.svg'; import { Paper } from 'src/components/Paper'; import { Placeholder } from 'src/components/Placeholder/Placeholder'; import { buildQueryStringForLinodeClone } from 'src/features/Linodes/LinodesLanding/LinodeActionMenu'; +import { useFlags } from 'src/hooks/useFlags'; import { extendType } from 'src/utilities/extendType'; import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; @@ -47,6 +48,8 @@ export const FromLinodeContent = (props: CombinedProps) => { const history = useHistory(); + const flags = useFlags(); + const updateSearchParams = (search: string) => { history.replace({ search }); }; @@ -105,11 +108,15 @@ export const FromLinodeContent = (props: CombinedProps) => { text: 'This newly created Linode will be created with the same password and SSH Keys (if any) as the original Linode.', }, - { - level: 'warning', - text: - 'To help avoid data corruption during the cloning process, we recommend powering off your Compute Instance prior to cloning.', - }, + ...(flags.linodeCloneUIChanges + ? [ + { + level: 'warning' as const, + text: + 'To help avoid data corruption during the cloning process, we recommend powering off your Compute Instance prior to cloning.', + }, + ] + : []), ]} data-qa-linode-panel disabled={userCannotCreateLinode} From 649ae7126848dd2c8918308ead097a3e26ad0209 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Wed, 17 Jan 2024 17:08:03 -0500 Subject: [PATCH 03/34] Added changeset: Clone Linode power-off notice --- .../manager/.changeset/pr-10072-changed-1705529283172.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10072-changed-1705529283172.md diff --git a/packages/manager/.changeset/pr-10072-changed-1705529283172.md b/packages/manager/.changeset/pr-10072-changed-1705529283172.md new file mode 100644 index 00000000000..975d2b9edfd --- /dev/null +++ b/packages/manager/.changeset/pr-10072-changed-1705529283172.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Clone Linode power-off notice ([#10072](https://github.com/linode/manager/pull/10072)) From bc2d2977b80c47bc5794138f2f188e53f9c8d241 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Thu, 18 Jan 2024 13:11:24 -0500 Subject: [PATCH 04/34] Reduce spacing between notices --- .../LinodesCreate/SelectLinodePanel.tsx | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx index d1802d0560e..278874650d4 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx @@ -10,6 +10,7 @@ import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFoot import { Paper } from 'src/components/Paper'; import { RenderGuard } from 'src/components/RenderGuard'; import { SelectionCard } from 'src/components/SelectionCard/SelectionCard'; +import { Stack } from 'src/components/Stack'; import { Typography } from 'src/components/Typography'; export interface ExtendedLinode extends Linode { @@ -71,12 +72,27 @@ const SelectLinodePanel = (props: Props) => { return ( <> - {error && } - {notices && - !disabled && - notices.map((notice, i) => ( - - ))} + + {error && ( + + )} + {notices && + !disabled && + notices.map((notice, i) => ( + + ))} + {!!header ? header : 'Select Linode'} From 49f5c1102caa01696ea05e89d4c20ab8deedc862 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Fri, 19 Jan 2024 16:54:51 -0500 Subject: [PATCH 05/34] Refactored DebouncedSearchTextField to enable external state management --- .../DebouncedSearchTextField.tsx | 72 +++++++++++-------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx b/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx index fa8759bf210..1d87b9d1b26 100644 --- a/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx +++ b/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx @@ -1,3 +1,4 @@ +import Clear from '@mui/icons-material/Clear'; import Search from '@mui/icons-material/Search'; import { styled } from '@mui/material/styles'; import * as React from 'react'; @@ -5,10 +6,22 @@ import * as React from 'react'; import { CircleProgress } from 'src/components/CircleProgress'; import { InputAdornment } from 'src/components/InputAdornment'; import { TextField, TextFieldProps } from 'src/components/TextField'; -import { usePrevious } from 'src/hooks/usePrevious'; export interface DebouncedSearchProps extends TextFieldProps { className?: string; + /** + * Whether to show a clear button at the end of the input. + */ + clearable?: boolean; + /** + * Including this prop will disable this field from being self-managed. + * The user must then manage the state of the text field and provide a + * value and change handler. + */ + customValue?: { + onChange: (newValue: string | undefined) => void; + value: string | undefined; + }; /** * Interval in milliseconds of time that passes before search queries are accepted. * @default 400 @@ -16,6 +29,7 @@ export interface DebouncedSearchProps extends TextFieldProps { debounceTime?: number; defaultValue?: string; hideLabel?: boolean; + /** * Determines if the textbox is currently searching for inputted query */ @@ -31,6 +45,8 @@ const DebouncedSearch = (props: DebouncedSearchProps) => { const { InputProps, className, + clearable, + customValue, debounceTime, defaultValue, hideLabel, @@ -40,34 +56,23 @@ const DebouncedSearch = (props: DebouncedSearchProps) => { placeholder, ...restOfTextFieldProps } = props; - const [query, setQuery] = React.useState(''); - const prevQuery = usePrevious(query); + + // Manage the textfield state if customValue is not provided + const managedValue = React.useState(); + const [textFieldValue, setTextFieldValue] = customValue + ? [customValue.value, customValue.onChange] + : managedValue; React.useEffect(() => { - /* - This `didCancel` business is to prevent a warning from React. - See: https://github.com/facebook/react/issues/14369#issuecomment-468267798 - */ - let didCancel = false; - /* - don't run the search if the query hasn't changed. - This is mostly to prevent this effect from running on first mount - */ - if ((prevQuery || '') !== query) { - setTimeout(() => { - if (!didCancel) { - onSearch(query); - } - }, debounceTime || 400); + if (textFieldValue != undefined) { + const timeout = setTimeout( + () => onSearch(textFieldValue), + debounceTime !== undefined ? debounceTime : 400 + ); + return () => clearTimeout(timeout); } - return () => { - didCancel = true; - }; - }, [query]); - - const _setQuery = (e: React.ChangeEvent) => { - setQuery(e.target.value); - }; + return undefined; + }, [textFieldValue]); return ( { ) : ( - + clearable && ( + + setTextFieldValue('')} /> + + ) ), startAdornment: ( @@ -91,7 +100,7 @@ const DebouncedSearch = (props: DebouncedSearchProps) => { defaultValue={defaultValue} hideLabel={hideLabel} label={label} - onChange={_setQuery} + onChange={(e) => setTextFieldValue(e.target.value)} placeholder={placeholder || 'Filter by query'} {...restOfTextFieldProps} /> @@ -105,3 +114,10 @@ const StyledSearchIcon = styled(Search)(({ theme }) => ({ color: theme.color.grey1, }, })); + +const StyledClearIcon = styled(Clear)(({ theme }) => ({ + '&&, &&:hover': { + color: theme.color.grey1, + }, + cursor: 'pointer', +})); From aed317c2ed8699cfdedd97d2d8212f4885e55745 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Fri, 19 Jan 2024 17:17:36 -0500 Subject: [PATCH 06/34] Add search to SelectLinodePanel and autopopulate when cloning --- .../LinodesCreate/SelectLinodePanel.tsx | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx index 278874650d4..cd0790201c4 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx @@ -1,9 +1,10 @@ import { Linode } from '@linode/api-v4/lib/linodes'; import Grid from '@mui/material/Unstable_Grid2'; -import { styled } from '@mui/material/styles'; +import { styled, useTheme } from '@mui/material/styles'; import * as React from 'react'; import { Box } from 'src/components/Box'; +import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; import { Notice } from 'src/components/Notice/Notice'; import Paginate from 'src/components/Paginate'; import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; @@ -44,6 +45,27 @@ const SelectLinodePanel = (props: Props) => { selectedLinodeID, } = props; + const theme = useTheme(); + const [userSearchText, setUserSearchText] = React.useState< + string | undefined + >(undefined); + + const searchText = React.useMemo( + () => + userSearchText !== undefined + ? userSearchText + : linodes.find((linode) => linode.id === selectedLinodeID)?.label || '', + [linodes, selectedLinodeID, userSearchText] + ); + + const filteredLinodes = React.useMemo( + () => + linodes.filter((linode) => + linode.label.toLowerCase().includes(searchText.toLowerCase()) + ), + [linodes, searchText] + ); + const renderCard = (linode: ExtendedLinode) => { return ( { }; return ( - + {({ count, data: linodesData, @@ -93,9 +115,27 @@ const SelectLinodePanel = (props: Props) => { /> ))} - + {!!header ? header : 'Select Linode'} + {linodesData.map((linode) => { From 4ed06f96f8cdd37629b6b8330b8dcbaa6f25a960 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Fri, 19 Jan 2024 18:21:45 -0500 Subject: [PATCH 07/34] Fix bug causing filter value to update every time a Linode is selected --- .../features/Linodes/LinodesCreate/SelectLinodePanel.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx index cd0790201c4..27f0a449f66 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx @@ -50,12 +50,15 @@ const SelectLinodePanel = (props: Props) => { string | undefined >(undefined); + const [preselectedLinodeID] = React.useState(selectedLinodeID); + const searchText = React.useMemo( () => userSearchText !== undefined ? userSearchText - : linodes.find((linode) => linode.id === selectedLinodeID)?.label || '', - [linodes, selectedLinodeID, userSearchText] + : linodes.find((linode) => linode.id === preselectedLinodeID)?.label || + '', + [linodes, preselectedLinodeID, userSearchText] ); const filteredLinodes = React.useMemo( From c2bfb1fa3c8459ade047c6a2dffab696322d3a6f Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Fri, 19 Jan 2024 18:28:03 -0500 Subject: [PATCH 08/34] Make clear icon blue on hover --- .../DebouncedSearchTextField/DebouncedSearchTextField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx b/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx index 1d87b9d1b26..88d5352d239 100644 --- a/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx +++ b/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx @@ -116,7 +116,7 @@ const StyledSearchIcon = styled(Search)(({ theme }) => ({ })); const StyledClearIcon = styled(Clear)(({ theme }) => ({ - '&&, &&:hover': { + '&&': { color: theme.color.grey1, }, cursor: 'pointer', From e3fe3efa131d339894d6a5f081e7a0dbeff78045 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Fri, 19 Jan 2024 18:36:14 -0500 Subject: [PATCH 09/34] Gate changes behind feature flag --- .../LinodesCreate/SelectLinodePanel.tsx | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx index 27f0a449f66..13d10509c51 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx @@ -13,6 +13,7 @@ import { RenderGuard } from 'src/components/RenderGuard'; import { SelectionCard } from 'src/components/SelectionCard/SelectionCard'; import { Stack } from 'src/components/Stack'; import { Typography } from 'src/components/Typography'; +import { useFlags } from 'src/hooks/useFlags'; export interface ExtendedLinode extends Linode { heading: string; @@ -45,12 +46,16 @@ const SelectLinodePanel = (props: Props) => { selectedLinodeID, } = props; + const flags = useFlags(); + const theme = useTheme(); const [userSearchText, setUserSearchText] = React.useState< string | undefined >(undefined); - const [preselectedLinodeID] = React.useState(selectedLinodeID); + const [preselectedLinodeID] = React.useState( + flags.linodeCloneUIChanges && selectedLinodeID + ); const searchText = React.useMemo( () => @@ -119,26 +124,30 @@ const SelectLinodePanel = (props: Props) => { ))} {!!header ? header : 'Select Linode'} - + {flags.linodeCloneUIChanges && ( + + )} {linodesData.map((linode) => { From fbd5106ab125b79f888be77c93def9704da61bcc Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Fri, 19 Jan 2024 18:41:37 -0500 Subject: [PATCH 10/34] Added changeset: Search filter in Clone Linode and Create Linode from Backup flows --- packages/manager/.changeset/pr-10088-added-1705707697666.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10088-added-1705707697666.md diff --git a/packages/manager/.changeset/pr-10088-added-1705707697666.md b/packages/manager/.changeset/pr-10088-added-1705707697666.md new file mode 100644 index 00000000000..959497f7bb2 --- /dev/null +++ b/packages/manager/.changeset/pr-10088-added-1705707697666.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +Search filter in Clone Linode and Create Linode from Backup flows ([#10088](https://github.com/linode/manager/pull/10088)) From e63b7de848c9547ce60ff6712ca9af575e4a337e Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 23 Jan 2024 13:30:47 -0500 Subject: [PATCH 11/34] Added -> upcoming --- ...707697666.md => pr-10088-upcoming-features-1705707697666.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename packages/manager/.changeset/{pr-10088-added-1705707697666.md => pr-10088-upcoming-features-1705707697666.md} (78%) diff --git a/packages/manager/.changeset/pr-10088-added-1705707697666.md b/packages/manager/.changeset/pr-10088-upcoming-features-1705707697666.md similarity index 78% rename from packages/manager/.changeset/pr-10088-added-1705707697666.md rename to packages/manager/.changeset/pr-10088-upcoming-features-1705707697666.md index 959497f7bb2..87ee6dddc78 100644 --- a/packages/manager/.changeset/pr-10088-added-1705707697666.md +++ b/packages/manager/.changeset/pr-10088-upcoming-features-1705707697666.md @@ -1,5 +1,5 @@ --- -"@linode/manager": Added +"@linode/manager": Upcoming Features --- Search filter in Clone Linode and Create Linode from Backup flows ([#10088](https://github.com/linode/manager/pull/10088)) From 5acd91a6ae5c0a9935334426bc8f3ec35b6d5a90 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 23 Jan 2024 16:07:48 -0500 Subject: [PATCH 12/34] Add explanatory comment --- .../src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx index 13d10509c51..af8b2b6276f 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx @@ -53,6 +53,8 @@ const SelectLinodePanel = (props: Props) => { string | undefined >(undefined); + // Capture the selected linode when this component mounts, + // so it doesn't change when the user selects a different one. const [preselectedLinodeID] = React.useState( flags.linodeCloneUIChanges && selectedLinodeID ); From 58b2cd3dfb5097e195caf03eb28d586099e4805c Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 23 Jan 2024 17:24:15 -0500 Subject: [PATCH 13/34] Change InputAdornment to IconButton --- .../DebouncedSearchTextField.tsx | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx b/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx index 88d5352d239..9b96a493f93 100644 --- a/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx +++ b/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx @@ -7,6 +7,8 @@ import { CircleProgress } from 'src/components/CircleProgress'; import { InputAdornment } from 'src/components/InputAdornment'; import { TextField, TextFieldProps } from 'src/components/TextField'; +import { IconButton } from '../IconButton'; + export interface DebouncedSearchProps extends TextFieldProps { className?: string; /** @@ -83,9 +85,19 @@ const DebouncedSearch = (props: DebouncedSearchProps) => { ) : ( clearable && ( - - setTextFieldValue('')} /> - + setTextFieldValue('')} + size="small" + > + ({ + '&&': { + color: theme.color.grey1, + }, + })} + /> + ) ), startAdornment: ( @@ -114,10 +126,3 @@ const StyledSearchIcon = styled(Search)(({ theme }) => ({ color: theme.color.grey1, }, })); - -const StyledClearIcon = styled(Clear)(({ theme }) => ({ - '&&': { - color: theme.color.grey1, - }, - cursor: 'pointer', -})); From 80beada824a8238cb87b7789614f39352555f946 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 23 Jan 2024 17:25:11 -0500 Subject: [PATCH 14/34] Add missing useEffect dependencies --- .../DebouncedSearchTextField/DebouncedSearchTextField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx b/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx index 9b96a493f93..3a4d5403989 100644 --- a/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx +++ b/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx @@ -74,7 +74,7 @@ const DebouncedSearch = (props: DebouncedSearchProps) => { return () => clearTimeout(timeout); } return undefined; - }, [textFieldValue]); + }, [debounceTime, onSearch, textFieldValue]); return ( Date: Tue, 23 Jan 2024 17:25:55 -0500 Subject: [PATCH 15/34] Fix unused customValue prop --- .../DebouncedSearchTextField/DebouncedSearchTextField.tsx | 5 +++-- .../features/Linodes/LinodesCreate/SelectLinodePanel.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx b/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx index 3a4d5403989..5cfea826dc9 100644 --- a/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx +++ b/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx @@ -39,7 +39,7 @@ export interface DebouncedSearchProps extends TextFieldProps { /** * Function to perform when searching for query */ - onSearch: (query: string) => void; + onSearch?: (query: string) => void; placeholder?: string; } @@ -68,7 +68,7 @@ const DebouncedSearch = (props: DebouncedSearchProps) => { React.useEffect(() => { if (textFieldValue != undefined) { const timeout = setTimeout( - () => onSearch(textFieldValue), + () => onSearch && onSearch(textFieldValue), debounceTime !== undefined ? debounceTime : 400 ); return () => clearTimeout(timeout); @@ -114,6 +114,7 @@ const DebouncedSearch = (props: DebouncedSearchProps) => { label={label} onChange={(e) => setTextFieldValue(e.target.value)} placeholder={placeholder || 'Filter by query'} + value={textFieldValue} {...restOfTextFieldProps} /> ); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx index af8b2b6276f..f9cfbafd690 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx @@ -136,6 +136,10 @@ const SelectLinodePanel = (props: Props) => { {flags.linodeCloneUIChanges && ( { expand={true} hideLabel label="" - onSearch={setUserSearchText} placeholder="Search" - value={searchText} /> )} From c0afbebe5fffc6ed3fc14389d04bd987248b15c4 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Wed, 24 Jan 2024 15:57:39 -0500 Subject: [PATCH 16/34] Simplify export --- .../DebouncedSearchTextField.tsx | 150 +++++++++--------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx b/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx index 5cfea826dc9..9e4b7c320fb 100644 --- a/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx +++ b/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx @@ -43,84 +43,84 @@ export interface DebouncedSearchProps extends TextFieldProps { placeholder?: string; } -const DebouncedSearch = (props: DebouncedSearchProps) => { - const { - InputProps, - className, - clearable, - customValue, - debounceTime, - defaultValue, - hideLabel, - isSearching, - label, - onSearch, - placeholder, - ...restOfTextFieldProps - } = props; +export const DebouncedSearchTextField = React.memo( + (props: DebouncedSearchProps) => { + const { + InputProps, + className, + clearable, + customValue, + debounceTime, + defaultValue, + hideLabel, + isSearching, + label, + onSearch, + placeholder, + ...restOfTextFieldProps + } = props; - // Manage the textfield state if customValue is not provided - const managedValue = React.useState(); - const [textFieldValue, setTextFieldValue] = customValue - ? [customValue.value, customValue.onChange] - : managedValue; + // Manage the textfield state if customValue is not provided + const managedValue = React.useState(); + const [textFieldValue, setTextFieldValue] = customValue + ? [customValue.value, customValue.onChange] + : managedValue; - React.useEffect(() => { - if (textFieldValue != undefined) { - const timeout = setTimeout( - () => onSearch && onSearch(textFieldValue), - debounceTime !== undefined ? debounceTime : 400 - ); - return () => clearTimeout(timeout); - } - return undefined; - }, [debounceTime, onSearch, textFieldValue]); + React.useEffect(() => { + if (textFieldValue != undefined) { + const timeout = setTimeout( + () => onSearch && onSearch(textFieldValue), + debounceTime !== undefined ? debounceTime : 400 + ); + return () => clearTimeout(timeout); + } + return undefined; + }, [debounceTime, onSearch, textFieldValue]); - return ( - - - - ) : ( - clearable && ( - setTextFieldValue('')} - size="small" - > - ({ - '&&': { - color: theme.color.grey1, - }, - })} - /> - - ) - ), - startAdornment: ( - - - - ), - ...InputProps, - }} - className={className} - data-qa-debounced-search - defaultValue={defaultValue} - hideLabel={hideLabel} - label={label} - onChange={(e) => setTextFieldValue(e.target.value)} - placeholder={placeholder || 'Filter by query'} - value={textFieldValue} - {...restOfTextFieldProps} - /> - ); -}; - -export const DebouncedSearchTextField = React.memo(DebouncedSearch); + return ( + + + + ) : ( + clearable && ( + setTextFieldValue('')} + size="small" + > + ({ + '&&': { + color: theme.color.grey1, + }, + })} + /> + + ) + ), + startAdornment: ( + + + + ), + ...InputProps, + }} + className={className} + data-qa-debounced-search + defaultValue={defaultValue} + hideLabel={hideLabel} + label={label} + onChange={(e) => setTextFieldValue(e.target.value)} + placeholder={placeholder || 'Filter by query'} + value={textFieldValue} + {...restOfTextFieldProps} + /> + ); + } +); const StyledSearchIcon = styled(Search)(({ theme }) => ({ '&&, &&:hover': { From 2284d0009493ab2f34bafb43246e5710ab6b79f7 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Mon, 12 Feb 2024 16:37:45 -0500 Subject: [PATCH 17/34] Add table view to Clone and Backups flow --- .../InlineMenuAction/InlineMenuAction.tsx | 4 + .../LinodesCreate/SelectLinodePanel.tsx | 262 +++++++++++------- .../Linodes/LinodesCreate/SelectLinodeRow.tsx | 204 ++++++++++++++ .../TabbedContent/FromBackupsContent.tsx | 39 ++- .../TabbedContent/FromLinodeContent.tsx | 63 +++-- packages/manager/src/queries/types.ts | 11 +- 6 files changed, 440 insertions(+), 143 deletions(-) create mode 100644 packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.tsx diff --git a/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx b/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx index 9df73864fdd..95cf9040a6c 100644 --- a/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx +++ b/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx @@ -8,6 +8,8 @@ import { StyledActionButton } from 'src/components/Button/StyledActionButton'; interface InlineMenuActionProps { /** Required action text */ actionText: string; + /** Optional height when displayed as a button */ + buttonHeight?: number; /** Optional class names */ className?: string; /** Optional disabled */ @@ -27,6 +29,7 @@ interface InlineMenuActionProps { export const InlineMenuAction = (props: InlineMenuActionProps) => { const { actionText, + buttonHeight, className, disabled, href, @@ -53,6 +56,7 @@ export const InlineMenuAction = (props: InlineMenuActionProps) => { disabled={disabled} loading={loading} onClick={onClick} + sx={buttonHeight != undefined ? { height: `${buttonHeight}px` } : {}} tooltipAnalyticsEvent={tooltipAnalyticsEvent} tooltipText={tooltip} {...rest} diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx index f9cfbafd690..2423be52632 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx @@ -1,4 +1,5 @@ import { Linode } from '@linode/api-v4/lib/linodes'; +import { useMediaQuery } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; import { styled, useTheme } from '@mui/material/styles'; import * as React from 'react'; @@ -6,15 +7,22 @@ import * as React from 'react'; import { Box } from 'src/components/Box'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; import { Notice } from 'src/components/Notice/Notice'; +import { OrderByProps } from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import { Paper } from 'src/components/Paper'; -import { RenderGuard } from 'src/components/RenderGuard'; import { SelectionCard } from 'src/components/SelectionCard/SelectionCard'; import { Stack } from 'src/components/Stack'; +import { Table } from 'src/components/Table'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; +import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { Typography } from 'src/components/Typography'; import { useFlags } from 'src/hooks/useFlags'; +import { PowerActionsDialog } from '../PowerActionsDialogOrDrawer'; +import { SelectLinodeRow, SelectLinodeTableRowHead } from './SelectLinodeRow'; + export interface ExtendedLinode extends Linode { heading: string; subHeadings: string[]; @@ -30,29 +38,36 @@ interface Props { error?: string; handleSelection: (id: number, type: null | string, diskSize?: number) => void; header?: string; - linodes: ExtendedLinode[]; notices?: Notice[]; + orderBy: OrderByProps; selectedLinodeID?: number; } -const SelectLinodePanel = (props: Props) => { +export const SelectLinodePanel = (props: Props) => { const { disabled, error, handleSelection, header, - linodes, notices, + orderBy, selectedLinodeID, } = props; - const flags = useFlags(); + const linodes = orderBy.data; + const flags = useFlags(); const theme = useTheme(); + const matchesMdUp = useMediaQuery(theme.breakpoints.up('md')); + const [userSearchText, setUserSearchText] = React.useState< string | undefined >(undefined); + const [powerOffLinode, setPowerOffLinode] = React.useState< + { linodeID: number } | false + >(false); + // Capture the selected linode when this component mounts, // so it doesn't change when the user selects a different one. const [preselectedLinodeID] = React.useState( @@ -76,8 +91,120 @@ const SelectLinodePanel = (props: Props) => { [linodes, searchText] ); - const renderCard = (linode: ExtendedLinode) => { - return ( + return ( + <> + + {({ + count, + data: linodesData, + handlePageChange, + handlePageSizeChange, + page, + pageSize, + }) => { + return ( + <> + + + {error && ( + + )} + {notices && + !disabled && + notices.map((notice, i) => ( + + ))} + + + {!!header ? header : 'Select Linode'} + + {flags.linodeCloneUIChanges && ( + + )} + + {(matchesMdUp ? renderTable : renderCards)({ + disabled: disabled ?? false, + handlePowerOff: (linodeID) => + setPowerOffLinode({ linodeID }), + handleSelection, + orderBy: { ...orderBy, data: linodesData }, + selectedLinodeID, + })} + + + + + ); + }} + + {powerOffLinode && ( + setPowerOffLinode(false)} + /> + )} + + ); +}; + +interface RenderLinodeProps { + disabled: boolean; + handlePowerOff: (linodeID: number) => void; + handleSelection: Props['handleSelection']; + orderBy: OrderByProps; + selectedLinodeID: number | undefined; +} + +const renderCards = ({ + disabled, + handleSelection, + orderBy: { data: linodes }, + selectedLinodeID, +}: RenderLinodeProps) => ( + + {linodes.map((linode) => ( { handleSelection(linode.id, linode.type, linode.specs.disk); @@ -88,92 +215,41 @@ const SelectLinodePanel = (props: Props) => { key={`selection-card-${linode.id}`} subheadings={linode.subHeadings} /> - ); - }; + ))} + +); - return ( - - {({ - count, - data: linodesData, - handlePageChange, - handlePageSizeChange, - page, - pageSize, - }) => { - return ( - <> - - - {error && ( - - )} - {notices && - !disabled && - notices.map((notice, i) => ( - - ))} - - - {!!header ? header : 'Select Linode'} - - {flags.linodeCloneUIChanges && ( - - )} - - - {linodesData.map((linode) => { - return renderCard(linode); - })} - - - - - - ); - }} - - ); -}; +const renderTable = ({ + disabled, + handlePowerOff, + handleSelection, + orderBy, + selectedLinodeID, +}: RenderLinodeProps) => ( + + + + + + {orderBy.data.length > 0 ? ( + orderBy.data.map((linode) => ( + + handleSelection(linode.id, linode.type, linode.specs.disk) + } + disabled={disabled} + handlePowerOff={() => handlePowerOff(linode.id)} + key={linode.id} + linodeId={linode.id} + selected={Number(selectedLinodeID) === linode.id} + /> + )) + ) : ( + + )} + +
+); const StyledBox = styled(Box, { label: 'StyledBox', @@ -186,5 +262,3 @@ const StyledPaper = styled(Paper, { label: 'StyledPaper' })(({ theme }) => ({ flexGrow: 1, width: '100%', })); - -export default RenderGuard(SelectLinodePanel); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.tsx new file mode 100644 index 00000000000..6222166d867 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.tsx @@ -0,0 +1,204 @@ +import ErrorOutline from '@mui/icons-material/ErrorOutline'; +import { useTheme } from '@mui/material'; +import * as React from 'react'; +import { useQueryClient } from 'react-query'; + +import { Box } from 'src/components/Box'; +import { CircleProgress } from 'src/components/CircleProgress'; +import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; +import { Link } from 'src/components/Link'; +import { OrderByProps } from 'src/components/OrderBy'; +import { Radio } from 'src/components/Radio/Radio'; +import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; +import { TableCell, TableCellProps } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import { TableSortCell } from 'src/components/TableSortCell'; +import { Typography } from 'src/components/Typography'; +import { getLinodeIconStatus } from 'src/features/Linodes/LinodesLanding/utils'; +import { useImageQuery } from 'src/queries/images'; +import { + queryKey as linodesQueryKey, + useLinodeQuery, +} from 'src/queries/linodes/linodes'; +import { useTypeQuery } from 'src/queries/types'; +import { capitalizeAllWords } from 'src/utilities/capitalize'; +import { formatStorageUnits } from 'src/utilities/formatStorageUnits'; + +import { RegionIndicator } from '../LinodesLanding/RegionIndicator'; + +// import { +// NETWORK_INTERFACES_GUIDE_URL, +// VPC_REBOOT_MESSAGE, +// WARNING_ICON_UNRECOMMENDED_CONFIG, +// } from '../constants'; +// import { +// hasUnrecommendedConfiguration as _hasUnrecommendedConfiguration, +// getSubnetInterfaceFromConfigs, +// } from '../utils'; +// import { +// StyledActionTableCell, +// StyledTableCell, +// StyledTableHeadCell, +// StyledTableRow, +// StyledWarningIcon, +// } from './SubnetLinodeRow.styles'; + +interface Props { + disabled: boolean; + handlePowerOff: () => void; + handleSelection: () => void; + linodeId: number; + selected: boolean; +} + +export const SelectLinodeRow = (props: Props) => { + const queryClient = useQueryClient(); + const { + disabled, + handlePowerOff, + handleSelection, + linodeId, + selected, + } = props; + + const theme = useTheme(); + + const { + data: linode, + error: linodeError, + isLoading: linodeLoading, + } = useLinodeQuery(linodeId); + + const { data: linodeType } = useTypeQuery( + linode?.type ?? '', + Boolean(linode?.type) + ); + + const { data: linodeImage } = useImageQuery( + linode?.image ?? '', + Boolean(linode?.image) + ); + + // If the Linode's status is running, we want to check if its interfaces associated with this subnet have become active so + // that we can determine if it needs a reboot or not. So, we need to invalidate the linode configs query to get the most up to date information. + React.useEffect(() => { + if (linode && linode.status === 'running') { + queryClient.invalidateQueries([ + linodesQueryKey, + 'linode', + linodeId, + 'configs', + ]); + } + }, [linode, linodeId, queryClient]); + + if (linodeLoading || !linode) { + return ( + + + + + + ); + } + + if (linodeError) { + return ( + + + + ({ color: theme.color.red, marginRight: 1 })} + /> + + There was an error loading{' '} + Linode {linodeId} + + + + + ); + } + + const iconStatus = getLinodeIconStatus(linode.status); + const isRunning = linode.status == 'running'; + + return ( + + + + + {linode.label} + + + {capitalizeAllWords(linode.status.replace('_', ' '))} + + {linodeImage?.label ?? linode.image ?? 'Unknown'} + + {linodeType ? formatStorageUnits(linodeType?.label) : linode.type} + + + + + + {isRunning && ( + + )} + + + ); +}; + +export const SelectLinodeTableRowHead = (props: { + orderBy: Omit, 'data'>; +}) => { + const { orderBy } = props; + const CustomSortCell = ( + props: TableCellProps & { + label: string; + } + ) => ( + + ); + return ( + + + + Linode + + + Status + + + Image + + + Plan + + + Region + + + + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx index 9e921edf8d4..9d4b61a0298 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx @@ -7,6 +7,7 @@ import { compose as ramdaCompose } from 'ramda'; import * as React from 'react'; import VolumeIcon from 'src/assets/icons/entityIcons/volume.svg'; +import OrderBy from 'src/components/OrderBy'; import { Paper } from 'src/components/Paper'; import { Placeholder } from 'src/components/Placeholder/Placeholder'; import { reportException } from 'src/exceptionReporting'; @@ -14,7 +15,7 @@ import { extendType } from 'src/utilities/extendType'; import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import { SelectBackupPanel } from '../SelectBackupPanel'; -import SelectLinodePanel from '../SelectLinodePanel'; +import { SelectLinodePanel } from '../SelectLinodePanel'; import { BackupFormStateHandlers, Info, @@ -109,8 +110,8 @@ export class FromBackupsContent extends React.Component { ) : ( - extendLinodes( linodes, @@ -120,21 +121,29 @@ export class FromBackupsContent extends React.Component { ), filterLinodesWithBackups )(linodesData)} - notices={[ - { - level: 'warning', - text: `This newly created Linode will be created with + order="asc" + orderBy="label" + preferenceKey={'linode-create-backups'} + > + {(orderBy) => ( + + }, + ]} + disabled={disabled} + error={hasErrorFor('linode_id')} + handleSelection={this.handleLinodeSelect} + orderBy={orderBy} + selectedLinodeID={selectedLinodeID} + /> + )} + { ) : ( - + order="asc" + orderBy="label" + preferenceKey={'linode-create-clone'} + > + {(orderBy) => ( + + )} + )} diff --git a/packages/manager/src/queries/types.ts b/packages/manager/src/queries/types.ts index 9cbc2efa816..d659396747e 100644 --- a/packages/manager/src/queries/types.ts +++ b/packages/manager/src/queries/types.ts @@ -38,12 +38,14 @@ const specificTypesQueryKey = (type: string) => [queryKey, 'detail', type]; /** * Some Linodes may have types that aren't returned by the /types and /types-legacy endpoints. This * hook may be useful in fetching these "shadow plans". + * + * Always returns an array of the same length of the `types` argument. */ export const useSpecificTypes = (types: string[], enabled = true) => { const queryClient = useQueryClient(); return useQueries( types.map>((type) => ({ - enabled, + enabled: Boolean(type) && enabled, queryFn: () => getSingleType(type, queryClient), queryKey: specificTypesQueryKey(type), ...queryPresets.oneTimeFetch, @@ -52,10 +54,5 @@ export const useSpecificTypes = (types: string[], enabled = true) => { }; export const useTypeQuery = (type: string, enabled = true) => { - return useQuery({ - queryFn: () => getType(type), - queryKey: specificTypesQueryKey(type), - ...queryPresets.oneTimeFetch, - enabled: enabled && Boolean(type), - }); + return useSpecificTypes([type], enabled)[0]; }; From c0396b46b01fd2b8e6f5ce47047610512d419ad7 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 13 Feb 2024 15:37:53 -0500 Subject: [PATCH 18/34] Add unit test for SelectLinodeRow --- .../LinodesCreate/SelectLinodeRow.test.tsx | 121 ++++++++++++++++++ .../Linodes/LinodesCreate/SelectLinodeRow.tsx | 19 +-- 2 files changed, 122 insertions(+), 18 deletions(-) create mode 100644 packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.test.tsx diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.test.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.test.tsx new file mode 100644 index 00000000000..7c3333e139e --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.test.tsx @@ -0,0 +1,121 @@ +import { waitForElementToBeRemoved } from '@testing-library/react'; +import { fireEvent } from '@testing-library/react'; +import * as React from 'react'; +import { QueryClient } from 'react-query'; + +import { imageFactory } from 'src/factories'; +import { linodeFactory } from 'src/factories/linodes'; +import { rest, server } from 'src/mocks/testServer'; +import { renderWithTheme, wrapWithTableBody } from 'src/utilities/testHelpers'; + +import { SelectLinodeRow } from './SelectLinodeRow'; + +const queryClient = new QueryClient(); + +afterEach(() => { + queryClient.clear(); +}); + +const loadingTestId = 'circle-progress'; + +describe('SubnetLinodeRow', () => { + const handlePowerOff = vi.fn(); + const handleSelection = vi.fn(); + + it('should display linode label, status, image, plan and region', async () => { + const linode1 = linodeFactory.build({ id: 1, label: 'linode-1' }); + const image1 = imageFactory.build({ + id: 'linode/debian10', + label: 'Debian 10', + }); + + server.use( + rest.get('*/linode/instances/:linodeId', (req, res, ctx) => { + return res(ctx.json(linode1)); + }), + rest.get('*/images/:imageId', (req, res, ctx) => { + return res(ctx.json(image1)); + }) + ); + + const { getAllByRole, getByTestId, getByText } = renderWithTheme( + wrapWithTableBody( + + ), + { + queryClient, + } + ); + + // Loading state should render + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + getByText(linode1.label); + getByText('Running'); + getByText('Debian 10'); + getByText('Linode 1 GB'); + getByText('Newark, NJ'); + + const selectButton = getAllByRole('button')[0]; + fireEvent.click(selectButton); + expect(handleSelection).toHaveBeenCalled(); + + const powerOffButton = getByText('Power Off'); + fireEvent.click(powerOffButton); + expect(handlePowerOff).toHaveBeenCalled(); + }); + + it('should not display power off linode button if linode is not running', async () => { + const linode1 = linodeFactory.build({ + id: 1, + label: 'linode-1', + status: 'offline', + }); + const image1 = imageFactory.build({ + id: 'linode/debian10', + label: 'Debian 10', + }); + server.use( + rest.get('*/linode/instances/:linodeId', (req, res, ctx) => { + return res(ctx.json(linode1)); + }), + rest.get('*/images/:imageId', (req, res, ctx) => { + return res(ctx.json(image1)); + }) + ); + + const { getByTestId, getByText, queryByText } = renderWithTheme( + wrapWithTableBody( + + ), + { + queryClient, + } + ); + + // Loading state should render + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + getByText(linode1.label); + getByText('Offline'); + getByText('Debian 10'); + getByText('Linode 1 GB'); + getByText('Newark, NJ'); + + expect(queryByText('Power Off')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.tsx index 6222166d867..0e5cd1c4d22 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.tsx @@ -26,25 +26,8 @@ import { formatStorageUnits } from 'src/utilities/formatStorageUnits'; import { RegionIndicator } from '../LinodesLanding/RegionIndicator'; -// import { -// NETWORK_INTERFACES_GUIDE_URL, -// VPC_REBOOT_MESSAGE, -// WARNING_ICON_UNRECOMMENDED_CONFIG, -// } from '../constants'; -// import { -// hasUnrecommendedConfiguration as _hasUnrecommendedConfiguration, -// getSubnetInterfaceFromConfigs, -// } from '../utils'; -// import { -// StyledActionTableCell, -// StyledTableCell, -// StyledTableHeadCell, -// StyledTableRow, -// StyledWarningIcon, -// } from './SubnetLinodeRow.styles'; - interface Props { - disabled: boolean; + disabled?: boolean; handlePowerOff: () => void; handleSelection: () => void; linodeId: number; From 5563ee87efb7093fa0804e52d6ca884fb8ae3c53 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 13 Feb 2024 16:16:52 -0500 Subject: [PATCH 19/34] Switch to useOrder hook --- .../LinodesCreate/SelectLinodePanel.tsx | 25 +++++--- .../TabbedContent/FromBackupsContent.tsx | 36 ++++------- .../TabbedContent/FromLinodeContent.tsx | 62 ++++++++----------- 3 files changed, 57 insertions(+), 66 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx index 2423be52632..0628624065e 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import { Box } from 'src/components/Box'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; import { Notice } from 'src/components/Notice/Notice'; -import { OrderByProps } from 'src/components/OrderBy'; +import { OrderByProps, sortData } from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import { Paper } from 'src/components/Paper'; @@ -19,6 +19,7 @@ import { TableHead } from 'src/components/TableHead'; import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { Typography } from 'src/components/Typography'; import { useFlags } from 'src/hooks/useFlags'; +import { useOrder } from 'src/hooks/useOrder'; import { PowerActionsDialog } from '../PowerActionsDialogOrDrawer'; import { SelectLinodeRow, SelectLinodeTableRowHead } from './SelectLinodeRow'; @@ -38,8 +39,8 @@ interface Props { error?: string; handleSelection: (id: number, type: null | string, diskSize?: number) => void; header?: string; + linodes: ExtendedLinode[]; notices?: Notice[]; - orderBy: OrderByProps; selectedLinodeID?: number; } @@ -49,12 +50,17 @@ export const SelectLinodePanel = (props: Props) => { error, handleSelection, header, + linodes, notices, - orderBy, selectedLinodeID, } = props; - const linodes = orderBy.data; + const { handleOrderChange, order, orderBy } = useOrder( + { order: 'asc', orderBy: 'label' }, + 'create-select-linode' + ); + + const orderedLinodes = sortData(orderBy, order)(linodes); const flags = useFlags(); const theme = useTheme(); @@ -85,10 +91,10 @@ export const SelectLinodePanel = (props: Props) => { const filteredLinodes = React.useMemo( () => - linodes.filter((linode) => + orderedLinodes.filter((linode) => linode.label.toLowerCase().includes(searchText.toLowerCase()) ), - [linodes, searchText] + [orderedLinodes, searchText] ); return ( @@ -159,7 +165,12 @@ export const SelectLinodePanel = (props: Props) => { handlePowerOff: (linodeID) => setPowerOffLinode({ linodeID }), handleSelection, - orderBy: { ...orderBy, data: linodesData }, + orderBy: { + data: linodesData, + handleOrderChange, + order, + orderBy, + }, selectedLinodeID, })}
diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx index 9d4b61a0298..3102292a0fc 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx @@ -7,7 +7,6 @@ import { compose as ramdaCompose } from 'ramda'; import * as React from 'react'; import VolumeIcon from 'src/assets/icons/entityIcons/volume.svg'; -import OrderBy from 'src/components/OrderBy'; import { Paper } from 'src/components/Paper'; import { Placeholder } from 'src/components/Placeholder/Placeholder'; import { reportException } from 'src/exceptionReporting'; @@ -110,8 +109,8 @@ export class FromBackupsContent extends React.Component { ) : ( - extendLinodes( linodes, @@ -121,29 +120,20 @@ export class FromBackupsContent extends React.Component { ), filterLinodesWithBackups )(linodesData)} - order="asc" - orderBy="label" - preferenceKey={'linode-create-backups'} - > - {(orderBy) => ( - - )} - + }, + ]} + disabled={disabled} + error={hasErrorFor('linode_id')} + handleSelection={this.handleLinodeSelect} + selectedLinodeID={selectedLinodeID} + /> { }; /** Set the Linode ID and the disk size and reset the plan selection */ - const handleSelectLinode = (linodeID: number, type: null | string) => { + const handleSelectLinode = (linodeID: number) => { const linode = props.linodesData.find( (eachLinode) => eachLinode.id === linodeID ); @@ -96,45 +95,36 @@ export const FromLinodeContent = (props: CombinedProps) => { ) : ( - - {(orderBy) => ( - - )} - + notices={[ + { + level: 'warning', + text: + 'This newly created Linode will be created with the same password and SSH Keys (if any) as the original Linode.', + }, + ...(flags.linodeCloneUIChanges + ? [ + { + level: 'warning' as const, + text: + 'To help avoid data corruption during the cloning process, we recommend powering off your Compute Instance prior to cloning.', + }, + ] + : []), + ]} + data-qa-linode-panel + disabled={userCannotCreateLinode} + error={hasErrorFor('linode_id')} + handleSelection={handleSelectLinode} + header={'Select Linode to Clone From'} + selectedLinodeID={selectedLinodeID} + /> )} From 624b7740a31f8882203806f5a48157126f0e756b Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 13 Feb 2024 16:19:21 -0500 Subject: [PATCH 20/34] Gate table view behind feature flag --- .../src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx index 0628624065e..2077370aa0b 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx @@ -160,7 +160,9 @@ export const SelectLinodePanel = (props: Props) => { /> )} - {(matchesMdUp ? renderTable : renderCards)({ + {(matchesMdUp && flags.linodeCloneUIChanges + ? renderTable + : renderCards)({ disabled: disabled ?? false, handlePowerOff: (linodeID) => setPowerOffLinode({ linodeID }), From 1108f510ff1a26bd91fad0001c63d7769a090e6a Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 13 Feb 2024 16:22:04 -0500 Subject: [PATCH 21/34] Added changeset: List view for Linode Clone and Create from Backup --- .../.changeset/pr-10182-upcoming-features-1707859324148.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10182-upcoming-features-1707859324148.md diff --git a/packages/manager/.changeset/pr-10182-upcoming-features-1707859324148.md b/packages/manager/.changeset/pr-10182-upcoming-features-1707859324148.md new file mode 100644 index 00000000000..5754ebaf602 --- /dev/null +++ b/packages/manager/.changeset/pr-10182-upcoming-features-1707859324148.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +List view for Linode Clone and Create from Backup ([#10182](https://github.com/linode/manager/pull/10182)) From 61c8af001245cda3e7744a4b21c46730439bf242 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Wed, 14 Feb 2024 14:13:09 -0500 Subject: [PATCH 22/34] No clear icon when search field empty --- .../DebouncedSearchTextField/DebouncedSearchTextField.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx b/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx index 9e4b7c320fb..0c215e5cf97 100644 --- a/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx +++ b/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx @@ -85,7 +85,8 @@ export const DebouncedSearchTextField = React.memo( ) : ( - clearable && ( + clearable && + textFieldValue && ( setTextFieldValue('')} From 06acfc3f7337bf40c70fc2ae0171ba3fc4b6ec64 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Wed, 14 Feb 2024 14:24:59 -0500 Subject: [PATCH 23/34] Extract render functions to components --- .../SelectLinodePanel/SelectCards.tsx | 28 ++++++ .../SelectLinodePanel.tsx | 91 ++++--------------- .../SelectLinodeRow.test.tsx | 0 .../SelectLinodeRow.tsx | 2 +- .../SelectLinodePanel/SelectTable.tsx | 41 +++++++++ .../TabbedContent/FromBackupsContent.tsx | 2 +- .../TabbedContent/FromLinodeContent.tsx | 2 +- 7 files changed, 89 insertions(+), 77 deletions(-) create mode 100644 packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectCards.tsx rename packages/manager/src/features/Linodes/LinodesCreate/{ => SelectLinodePanel}/SelectLinodePanel.tsx (71%) rename packages/manager/src/features/Linodes/LinodesCreate/{ => SelectLinodePanel}/SelectLinodeRow.test.tsx (100%) rename packages/manager/src/features/Linodes/LinodesCreate/{ => SelectLinodePanel}/SelectLinodeRow.tsx (98%) create mode 100644 packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectTable.tsx diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectCards.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectCards.tsx new file mode 100644 index 00000000000..d28bb7e256c --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectCards.tsx @@ -0,0 +1,28 @@ +import Grid from '@mui/material/Unstable_Grid2'; +import React from 'react'; + +import { SelectionCard } from 'src/components/SelectionCard/SelectionCard'; + +import { RenderLinodeProps } from './SelectLinodePanel'; + +export const SelectCards = ({ + disabled, + handleSelection, + orderBy: { data: linodes }, + selectedLinodeID, +}: RenderLinodeProps) => ( + + {linodes.map((linode) => ( + { + handleSelection(linode.id, linode.type, linode.specs.disk); + }} + checked={linode.id === Number(selectedLinodeID)} + disabled={disabled} + heading={linode.heading} + key={`selection-card-${linode.id}`} + subheadings={linode.subHeadings} + /> + ))} + +); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx similarity index 71% rename from packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx rename to packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx index 2077370aa0b..0fc9d2d51c9 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx @@ -1,6 +1,5 @@ import { Linode } from '@linode/api-v4/lib/linodes'; import { useMediaQuery } from '@mui/material'; -import Grid from '@mui/material/Unstable_Grid2'; import { styled, useTheme } from '@mui/material/styles'; import * as React from 'react'; @@ -11,18 +10,14 @@ import { OrderByProps, sortData } from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import { Paper } from 'src/components/Paper'; -import { SelectionCard } from 'src/components/SelectionCard/SelectionCard'; import { Stack } from 'src/components/Stack'; -import { Table } from 'src/components/Table'; -import { TableBody } from 'src/components/TableBody'; -import { TableHead } from 'src/components/TableHead'; -import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { Typography } from 'src/components/Typography'; import { useFlags } from 'src/hooks/useFlags'; import { useOrder } from 'src/hooks/useOrder'; -import { PowerActionsDialog } from '../PowerActionsDialogOrDrawer'; -import { SelectLinodeRow, SelectLinodeTableRowHead } from './SelectLinodeRow'; +import { PowerActionsDialog } from '../../PowerActionsDialogOrDrawer'; +import { SelectCards } from './SelectCards'; +import { SelectTable } from './SelectTable'; export interface ExtendedLinode extends Linode { heading: string; @@ -97,6 +92,9 @@ export const SelectLinodePanel = (props: Props) => { [orderedLinodes, searchText] ); + const SelectComponent = + matchesMdUp && flags.linodeCloneUIChanges ? SelectTable : SelectCards; + return ( <> @@ -160,21 +158,20 @@ export const SelectLinodePanel = (props: Props) => { /> )} - {(matchesMdUp && flags.linodeCloneUIChanges - ? renderTable - : renderCards)({ - disabled: disabled ?? false, - handlePowerOff: (linodeID) => - setPowerOffLinode({ linodeID }), - handleSelection, - orderBy: { + + setPowerOffLinode({ linodeID }) + } + orderBy={{ data: linodesData, handleOrderChange, order, orderBy, - }, - selectedLinodeID, - })} + }} + disabled={disabled ?? false} + handleSelection={handleSelection} + selectedLinodeID={selectedLinodeID} + />
{ ); }; -interface RenderLinodeProps { +export interface RenderLinodeProps { disabled: boolean; handlePowerOff: (linodeID: number) => void; handleSelection: Props['handleSelection']; @@ -210,60 +207,6 @@ interface RenderLinodeProps { selectedLinodeID: number | undefined; } -const renderCards = ({ - disabled, - handleSelection, - orderBy: { data: linodes }, - selectedLinodeID, -}: RenderLinodeProps) => ( - - {linodes.map((linode) => ( - { - handleSelection(linode.id, linode.type, linode.specs.disk); - }} - checked={linode.id === Number(selectedLinodeID)} - disabled={disabled} - heading={linode.heading} - key={`selection-card-${linode.id}`} - subheadings={linode.subHeadings} - /> - ))} - -); - -const renderTable = ({ - disabled, - handlePowerOff, - handleSelection, - orderBy, - selectedLinodeID, -}: RenderLinodeProps) => ( - - - - - - {orderBy.data.length > 0 ? ( - orderBy.data.map((linode) => ( - - handleSelection(linode.id, linode.type, linode.specs.disk) - } - disabled={disabled} - handlePowerOff={() => handlePowerOff(linode.id)} - key={linode.id} - linodeId={linode.id} - selected={Number(selectedLinodeID) === linode.id} - /> - )) - ) : ( - - )} - -
-); - const StyledBox = styled(Box, { label: 'StyledBox', })(({ theme }) => ({ diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.test.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx similarity index 100% rename from packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.test.tsx rename to packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx similarity index 98% rename from packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.tsx rename to packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx index 0e5cd1c4d22..8a68126846c 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodeRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx @@ -24,7 +24,7 @@ import { useTypeQuery } from 'src/queries/types'; import { capitalizeAllWords } from 'src/utilities/capitalize'; import { formatStorageUnits } from 'src/utilities/formatStorageUnits'; -import { RegionIndicator } from '../LinodesLanding/RegionIndicator'; +import { RegionIndicator } from '../../LinodesLanding/RegionIndicator'; interface Props { disabled?: boolean; diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectTable.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectTable.tsx new file mode 100644 index 00000000000..488d6c91b13 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectTable.tsx @@ -0,0 +1,41 @@ +import React from 'react'; + +import { Table } from 'src/components/Table'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; +import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; + +import { RenderLinodeProps } from './SelectLinodePanel'; +import { SelectLinodeRow, SelectLinodeTableRowHead } from './SelectLinodeRow'; + +export const SelectTable = ({ + disabled, + handlePowerOff, + handleSelection, + orderBy, + selectedLinodeID, +}: RenderLinodeProps) => ( + + + + + + {orderBy.data.length > 0 ? ( + orderBy.data.map((linode) => ( + + handleSelection(linode.id, linode.type, linode.specs.disk) + } + disabled={disabled} + handlePowerOff={() => handlePowerOff(linode.id)} + key={linode.id} + linodeId={linode.id} + selected={Number(selectedLinodeID) === linode.id} + /> + )) + ) : ( + + )} + +
+); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx index 3102292a0fc..9c87bde284f 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx @@ -14,7 +14,7 @@ import { extendType } from 'src/utilities/extendType'; import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import { SelectBackupPanel } from '../SelectBackupPanel'; -import { SelectLinodePanel } from '../SelectLinodePanel'; +import { SelectLinodePanel } from '../SelectLinodePanel/SelectLinodePanel'; import { BackupFormStateHandlers, Info, diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx index 9bd1a9f9cac..dd4850e54e3 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx @@ -9,7 +9,7 @@ import { useFlags } from 'src/hooks/useFlags'; import { extendType } from 'src/utilities/extendType'; import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; -import { SelectLinodePanel } from '../SelectLinodePanel'; +import { SelectLinodePanel } from '../SelectLinodePanel/SelectLinodePanel'; import { CloneFormStateHandlers, ReduxStateProps, From 91e5c0301ae72386560085b5abb17ba447261014 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Wed, 14 Feb 2024 14:29:18 -0500 Subject: [PATCH 24/34] linodeID -> linodeId --- .../SelectLinodePanel/SelectCards.tsx | 4 ++-- .../SelectLinodePanel/SelectLinodePanel.tsx | 14 +++++++------- .../SelectLinodePanel/SelectTable.tsx | 4 ++-- .../TabbedContent/FromLinodeContent.tsx | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectCards.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectCards.tsx index d28bb7e256c..eed7d9a9219 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectCards.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectCards.tsx @@ -9,7 +9,7 @@ export const SelectCards = ({ disabled, handleSelection, orderBy: { data: linodes }, - selectedLinodeID, + selectedLinodeId, }: RenderLinodeProps) => ( {linodes.map((linode) => ( @@ -17,7 +17,7 @@ export const SelectCards = ({ onClick={() => { handleSelection(linode.id, linode.type, linode.specs.disk); }} - checked={linode.id === Number(selectedLinodeID)} + checked={linode.id === Number(selectedLinodeId)} disabled={disabled} heading={linode.heading} key={`selection-card-${linode.id}`} diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx index 0fc9d2d51c9..803a5b9f9ea 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx @@ -66,7 +66,7 @@ export const SelectLinodePanel = (props: Props) => { >(undefined); const [powerOffLinode, setPowerOffLinode] = React.useState< - { linodeID: number } | false + { linodeId: number } | false >(false); // Capture the selected linode when this component mounts, @@ -159,8 +159,8 @@ export const SelectLinodePanel = (props: Props) => { )} - setPowerOffLinode({ linodeID }) + handlePowerOff={(linodeId) => + setPowerOffLinode({ linodeId }) } orderBy={{ data: linodesData, @@ -170,7 +170,7 @@ export const SelectLinodePanel = (props: Props) => { }} disabled={disabled ?? false} handleSelection={handleSelection} - selectedLinodeID={selectedLinodeID} + selectedLinodeId={selectedLinodeID} />
@@ -190,7 +190,7 @@ export const SelectLinodePanel = (props: Props) => { setPowerOffLinode(false)} /> @@ -201,10 +201,10 @@ export const SelectLinodePanel = (props: Props) => { export interface RenderLinodeProps { disabled: boolean; - handlePowerOff: (linodeID: number) => void; + handlePowerOff: (linodeId: number) => void; handleSelection: Props['handleSelection']; orderBy: OrderByProps; - selectedLinodeID: number | undefined; + selectedLinodeId: number | undefined; } const StyledBox = styled(Box, { diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectTable.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectTable.tsx index 488d6c91b13..db628544ab2 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectTable.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectTable.tsx @@ -13,7 +13,7 @@ export const SelectTable = ({ handlePowerOff, handleSelection, orderBy, - selectedLinodeID, + selectedLinodeId, }: RenderLinodeProps) => ( @@ -30,7 +30,7 @@ export const SelectTable = ({ handlePowerOff={() => handlePowerOff(linode.id)} key={linode.id} linodeId={linode.id} - selected={Number(selectedLinodeID) === linode.id} + selected={Number(selectedLinodeId) === linode.id} /> )) ) : ( diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx index dd4850e54e3..7c7de769a58 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx @@ -55,9 +55,9 @@ export const FromLinodeContent = (props: CombinedProps) => { }; /** Set the Linode ID and the disk size and reset the plan selection */ - const handleSelectLinode = (linodeID: number) => { + const handleSelectLinode = (linodeId: number) => { const linode = props.linodesData.find( - (eachLinode) => eachLinode.id === linodeID + (eachLinode) => eachLinode.id === linodeId ); if (linode) { From 112218c4c4eeb49bf5449c80eccfd3001d0c06dd Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Wed, 14 Feb 2024 14:48:14 -0500 Subject: [PATCH 25/34] Feedback --- .../src/components/InlineMenuAction/InlineMenuAction.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx b/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx index 95cf9040a6c..e1ae86412d4 100644 --- a/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx +++ b/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx @@ -56,7 +56,7 @@ export const InlineMenuAction = (props: InlineMenuActionProps) => { disabled={disabled} loading={loading} onClick={onClick} - sx={buttonHeight != undefined ? { height: `${buttonHeight}px` } : {}} + sx={buttonHeight != undefined ? { height: buttonHeight } : {}} tooltipAnalyticsEvent={tooltipAnalyticsEvent} tooltipText={tooltip} {...rest} From 581c37f59c9f1d7bcee652e05a22e5c9d41472a9 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Thu, 15 Feb 2024 17:54:32 -0500 Subject: [PATCH 26/34] Remove unnecessary ExtendedLinodes interface and utility --- .../LinodesCreate/LinodeCreateContainer.tsx | 13 +-- .../SelectLinodePanel/SelectLinodeCard.tsx | 54 ++++++++++++ ...{SelectCards.tsx => SelectLinodeCards.tsx} | 20 ++--- .../SelectLinodePanel/SelectLinodePanel.tsx | 19 ++-- .../SelectLinodeRow.test.tsx | 2 +- .../SelectLinodePanel/SelectLinodeRow.tsx | 2 +- ...{SelectTable.tsx => SelectLinodeTable.tsx} | 2 +- .../TabbedContent/FromBackupsContent.tsx | 29 ++----- .../TabbedContent/FromLinodeContent.tsx | 9 +- .../features/Linodes/LinodesCreate/types.ts | 5 -- .../Linodes/LinodesCreate/utilities.test.ts | 44 ---------- .../Linodes/LinodesCreate/utilities.tsx | 87 +------------------ 12 files changed, 84 insertions(+), 202 deletions(-) create mode 100644 packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeCard.tsx rename packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/{SelectCards.tsx => SelectLinodeCards.tsx} (55%) rename packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/{SelectTable.tsx => SelectLinodeTable.tsx} (97%) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx index fa39eab131d..a3b1ca05a68 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx @@ -83,7 +83,6 @@ import { validatePassword } from 'src/utilities/validatePassword'; import LinodeCreate from './LinodeCreate'; import { deriveDefaultLabel } from './deriveDefaultLabel'; import { HandleSubmit, Info, LinodeCreateValidation, TypeInfo } from './types'; -import { getRegionIDFromLinodeID } from './utilities'; import type { CreateLinodeRequest, @@ -589,19 +588,13 @@ class LinodeCreateContainer extends React.PureComponent { * since the API does not infer this automatically. */ - /** - * safe to ignore possibility of "undefined" - * null checking happens in CALinodeCreate - */ - const selectedRegionID = getRegionIDFromLinodeID( - this.props.linodesData!, - id - ); this.setState({ selectedBackupID: undefined, selectedDiskSize: diskSize, selectedLinodeID: id, - selectedRegionID, + selectedRegionID: this.props.linodesData?.find( + (linode) => linode.id == id + )?.region, selectedTypeID: undefined, }); } diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeCard.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeCard.tsx new file mode 100644 index 00000000000..568554fb3cd --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeCard.tsx @@ -0,0 +1,54 @@ +import { Linode } from '@linode/api-v4'; +import React from 'react'; + +import { SelectionCard } from 'src/components/SelectionCard/SelectionCard'; +import { useImageQuery } from 'src/queries/images'; +import { useRegionsQuery } from 'src/queries/regions'; +import { useTypeQuery } from 'src/queries/types'; +import { formatStorageUnits } from 'src/utilities/formatStorageUnits'; +import { isNotNullOrUndefined } from 'src/utilities/nullOrUndefined'; + +interface Props { + disabled?: boolean; + handleSelection: () => void; + linode: Linode; + selected?: boolean; +} + +export const SelectLinodeCard = ({ + disabled, + handleSelection, + linode, + selected, +}: Props) => { + const { data: regions } = useRegionsQuery(); + + const { data: linodeType } = useTypeQuery( + linode?.type ?? '', + Boolean(linode?.type) + ); + + const { data: linodeImage } = useImageQuery( + linode?.image ?? '', + Boolean(linode?.image) + ); + + const type = linodeType ? formatStorageUnits(linodeType?.label) : linode.type; + const image = linodeImage?.label ?? linode.image; + const region = + regions?.find((region) => region.id == linode.region)?.label ?? + linode.region; + + return ( + + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectCards.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeCards.tsx similarity index 55% rename from packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectCards.tsx rename to packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeCards.tsx index eed7d9a9219..eb372dc8e55 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectCards.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeCards.tsx @@ -1,11 +1,10 @@ import Grid from '@mui/material/Unstable_Grid2'; import React from 'react'; -import { SelectionCard } from 'src/components/SelectionCard/SelectionCard'; - +import { SelectLinodeCard } from './SelectLinodeCard'; import { RenderLinodeProps } from './SelectLinodePanel'; -export const SelectCards = ({ +export const SelectLinodeCards = ({ disabled, handleSelection, orderBy: { data: linodes }, @@ -13,15 +12,14 @@ export const SelectCards = ({ }: RenderLinodeProps) => ( {linodes.map((linode) => ( - { - handleSelection(linode.id, linode.type, linode.specs.disk); - }} - checked={linode.id === Number(selectedLinodeId)} + + handleSelection(linode.id, linode.type, linode.specs.disk) + } disabled={disabled} - heading={linode.heading} - key={`selection-card-${linode.id}`} - subheadings={linode.subHeadings} + key={linode.id} + linode={linode} + selected={linode.id == selectedLinodeId} /> ))} diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx index 803a5b9f9ea..1958752ba0c 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx @@ -16,13 +16,8 @@ import { useFlags } from 'src/hooks/useFlags'; import { useOrder } from 'src/hooks/useOrder'; import { PowerActionsDialog } from '../../PowerActionsDialogOrDrawer'; -import { SelectCards } from './SelectCards'; -import { SelectTable } from './SelectTable'; - -export interface ExtendedLinode extends Linode { - heading: string; - subHeadings: string[]; -} +import { SelectLinodeCards } from './SelectLinodeCards'; +import { SelectLinodeTable } from './SelectLinodeTable'; interface Notice { level: 'error' | 'warning'; // most likely only going to need these two 'warning'; 'warning'; @@ -34,7 +29,7 @@ interface Props { error?: string; handleSelection: (id: number, type: null | string, diskSize?: number) => void; header?: string; - linodes: ExtendedLinode[]; + linodes: Linode[]; notices?: Notice[]; selectedLinodeID?: number; } @@ -55,7 +50,7 @@ export const SelectLinodePanel = (props: Props) => { 'create-select-linode' ); - const orderedLinodes = sortData(orderBy, order)(linodes); + const orderedLinodes = sortData(orderBy, order)(linodes); const flags = useFlags(); const theme = useTheme(); @@ -93,7 +88,9 @@ export const SelectLinodePanel = (props: Props) => { ); const SelectComponent = - matchesMdUp && flags.linodeCloneUIChanges ? SelectTable : SelectCards; + matchesMdUp && flags.linodeCloneUIChanges + ? SelectLinodeTable + : SelectLinodeCards; return ( <> @@ -203,7 +200,7 @@ export interface RenderLinodeProps { disabled: boolean; handlePowerOff: (linodeId: number) => void; handleSelection: Props['handleSelection']; - orderBy: OrderByProps; + orderBy: OrderByProps; selectedLinodeId: number | undefined; } diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx index 7c3333e139e..8d217ecf89f 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx @@ -18,7 +18,7 @@ afterEach(() => { const loadingTestId = 'circle-progress'; -describe('SubnetLinodeRow', () => { +describe('SelectLinodeRow', () => { const handlePowerOff = vi.fn(); const handleSelection = vi.fn(); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx index 8a68126846c..20240a49bbb 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx @@ -162,7 +162,7 @@ export const SelectLinodeTableRowHead = (props: { ); return ( - + { const { disabled, errors, - imagesData, linodesData, - regionsData, selectedBackupID, selectedLinodeID, setBackupID, - typesData, } = this.props; - const extendedTypes = typesData?.map(extendType); - const hasErrorFor = getAPIErrorFor(errorResources, errors); const userHasBackups = linodesData.some( @@ -110,16 +102,6 @@ export class FromBackupsContent extends React.Component { ) : ( - extendLinodes( - linodes, - imagesData, - extendedTypes, - regionsData - ), - filterLinodesWithBackups - )(linodesData)} notices={[ { level: 'warning', @@ -132,6 +114,7 @@ export class FromBackupsContent extends React.Component { disabled={disabled} error={hasErrorFor('linode_id')} handleSelection={this.handleLinodeSelect} + linodes={filterLinodesWithBackups(linodesData)} selectedLinodeID={selectedLinodeID} /> { }); throw new Error('selectedLinodeID is not a number'); } - const regionID = getRegionIDFromLinodeID( - this.props.linodesData, - selectedLinodeID - ); + const regionID = this.props.linodesData.find( + (linode) => linode.id == selectedLinodeID + )?.region; this.props.updateRegionID(regionID || ''); } @@ -197,8 +179,7 @@ export class FromBackupsContent extends React.Component { this.setState({ isGettingBackups: false, selectedLinodeWithBackups }); }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .catch((err) => { + .catch(() => { this.setState({ backupsError: 'Error retrieving backups for this Linode.', isGettingBackups: false, diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx index 7c7de769a58..4c21fe2ee87 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx @@ -15,7 +15,6 @@ import { ReduxStateProps, WithLinodesTypesRegionsAndImages, } from '../types'; -import { extendLinodes } from '../utilities'; import { StyledGrid } from './CommonTabbedContent.styles'; const errorResources = { @@ -32,7 +31,6 @@ export type CombinedProps = CloneFormStateHandlers & export const FromLinodeContent = (props: CombinedProps) => { const { errors, - imagesData, linodesData, regionsData, selectedLinodeID, @@ -96,12 +94,6 @@ export const FromLinodeContent = (props: CombinedProps) => { ) : ( { error={hasErrorFor('linode_id')} handleSelection={handleSelectLinode} header={'Select Linode to Clone From'} + linodes={linodesData} selectedLinodeID={selectedLinodeID} /> diff --git a/packages/manager/src/features/Linodes/LinodesCreate/types.ts b/packages/manager/src/features/Linodes/LinodesCreate/types.ts index c7efd6d39bb..b2cbbc64987 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/types.ts +++ b/packages/manager/src/features/Linodes/LinodesCreate/types.ts @@ -11,11 +11,6 @@ import { APIError } from '@linode/api-v4/lib/types'; import { Tag } from 'src/components/TagsInput/TagsInput'; import { ExtendedType } from 'src/utilities/extendType'; -export interface ExtendedLinode extends Linode { - heading: string; - subHeadings: string[]; -} - export type TypeInfo = | { details: string; diff --git a/packages/manager/src/features/Linodes/LinodesCreate/utilities.test.ts b/packages/manager/src/features/Linodes/LinodesCreate/utilities.test.ts index 9cb78582600..98d96209048 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/utilities.test.ts +++ b/packages/manager/src/features/Linodes/LinodesCreate/utilities.test.ts @@ -1,14 +1,9 @@ -import { extendedTypes } from 'src/__data__/ExtendedType'; -import { linode1, linode2 } from 'src/__data__/linodes'; import { imageFactory, normalizeEntities } from 'src/factories'; import { stackScriptFactory } from 'src/factories/stackscripts'; import { - extendLinodes, filterOneClickApps, - formatLinodeSubheading, getMonthlyAndHourlyNodePricing, - getRegionIDFromLinodeID, handleAppLabel, trimOneClickFromLabel, } from './utilities'; @@ -24,45 +19,6 @@ const linodeImage = imageFactory.build({ const images = normalizeEntities(imageFactory.buildList(10)); images['linode/debian10'] = linodeImage; -describe('Extend Linode', () => { - it('should create an array of Extended Linodes from an array of Linodes', () => { - const extendedLinodes = extendLinodes( - [ - { - ...linode1, - image: 'linode/debian10', - }, - ], - images, - extendedTypes - ); - expect(extendedLinodes[0].heading).toBe('test'); - expect(extendedLinodes[0].subHeadings).toEqual(['Nanode 1 GB, Debian 10']); - }); - - it('should concat image, type data, and region data separated by a comma', () => { - const withImage = formatLinodeSubheading('linode', 'image'); - const withoutImage = formatLinodeSubheading('linode'); - const withImageAndRegion = formatLinodeSubheading( - 'linode', - 'image', - 'region' - ); - - expect(withImage).toEqual(['linode, image']); - expect(withoutImage).toEqual(['linode']); - expect(withImageAndRegion).toEqual(['linode, image, region']); - }); -}); - -describe('getRegionIDFromLinodeID', () => { - it('returns the regionID from the given Linodes and Linode ID', () => { - expect(getRegionIDFromLinodeID([linode1, linode2], 2020425)).toBe( - 'us-east' - ); - }); -}); - describe('Marketplace cluster pricing', () => { it('should return the monthly and hourly price multipled by the number of nodes', () => { expect(getMonthlyAndHourlyNodePricing(30, 0.045, 3)).toEqual({ diff --git a/packages/manager/src/features/Linodes/LinodesCreate/utilities.tsx b/packages/manager/src/features/Linodes/LinodesCreate/utilities.tsx index 0a1f0645903..73b5d99dba4 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/utilities.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/utilities.tsx @@ -1,91 +1,6 @@ import { decode } from 'he'; -import * as React from 'react'; -import { Link } from 'src/components/Link'; -import { Typography } from 'src/components/Typography'; -import { displayType } from 'src/features/Linodes/presentation'; -import { ExtendedType } from 'src/utilities/extendType'; - -import { ExtendedLinode } from './types'; - -import type { Image, Linode, Region, StackScript } from '@linode/api-v4/lib'; - -/** - * adds a heading and subheading key to the Linode - */ -export const extendLinodes = ( - linodes: Linode[], - imagesData: Record = {}, - typesData: ExtendedType[] = [], - regionsData: Region[] = [] -): ExtendedLinode[] => { - return linodes.map((linode) => { - /** get image data based on the Linode's image key */ - const linodeImageMetaData = imagesData[linode.image || '']; - - return { - ...linode, - heading: linode.label, - subHeadings: formatLinodeSubheading( - displayType(linode.type, typesData), - linodeImageMetaData ? linodeImageMetaData.label : '', - regionsData.find((region) => region.id === linode.region)?.label ?? - undefined - ), - }; - }); -}; - -export const formatLinodeSubheading = ( - typeLabel: string, - imageLabel?: string, - regionLabel?: string -) => { - if (imageLabel && regionLabel) { - return [`${typeLabel}, ${imageLabel}, ${regionLabel}`]; - } - if (imageLabel) { - return [`${typeLabel}, ${imageLabel}`]; - } - return [typeLabel]; -}; - -export const getRegionIDFromLinodeID = ( - linodes: Linode[], - id: number -): string | undefined => { - const thisLinode = linodes.find((linode) => linode.id === id); - return thisLinode ? thisLinode.region : undefined; -}; - -export const gpuPlanText = (useTypography?: boolean): JSX.Element => { - const gpuPlanTextSegments = [ - 'Linode GPU plans have limited availability and may not be available at the time of your request. Some additional verification may be required to access these services. ', - 'with information on getting started.', - ]; - - if (useTypography) { - return ( - - {gpuPlanTextSegments[0]} - - Here is a guide - {' '} - {gpuPlanTextSegments[1]} - - ); - } - - return ( - <> - {gpuPlanTextSegments[0]} - - {` `}Here is a guide - {' '} - {gpuPlanTextSegments[1]} - - ); -}; +import type { Region, StackScript } from '@linode/api-v4/lib'; export const getMonthlyAndHourlyNodePricing = ( monthlyPrice: null | number | undefined, From 6bb920280771729140ab44e706794fb26eb502b0 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Wed, 21 Feb 2024 17:33:14 -0500 Subject: [PATCH 27/34] Rename feature flag to camelcase --- packages/manager/src/dev-tools/FeatureFlagTool.tsx | 2 +- packages/manager/src/featureFlags.ts | 2 +- .../LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx | 8 ++++---- .../LinodesCreate/TabbedContent/FromLinodeContent.tsx | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/manager/src/dev-tools/FeatureFlagTool.tsx b/packages/manager/src/dev-tools/FeatureFlagTool.tsx index 7a5b7586832..61604cc67a8 100644 --- a/packages/manager/src/dev-tools/FeatureFlagTool.tsx +++ b/packages/manager/src/dev-tools/FeatureFlagTool.tsx @@ -14,7 +14,7 @@ const MOCK_FEATURE_FLAGS_STORAGE_KEY = 'devTools/mock-feature-flags'; const options: { flag: keyof Flags; label: string }[] = [ { flag: 'aclb', label: 'ACLB' }, { flag: 'aclbFullCreateFlow', label: 'ACLB Full Create Flow' }, - { flag: 'linodeCloneUIChanges', label: 'Linode Clone UI Changes' }, + { flag: 'linodeCloneUiChanges', label: 'Linode Clone UI Changes' }, { flag: 'metadata', label: 'Metadata' }, { flag: 'parentChildAccountAccess', label: 'Parent/Child Account' }, { flag: 'selfServeBetas', label: 'Self Serve Betas' }, diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index c972cf17c3f..1f21a7d9d59 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -48,7 +48,7 @@ export interface Flags { databases: boolean; firewallNodebalancer: boolean; ipv6Sharing: boolean; - linodeCloneUIChanges: boolean; + linodeCloneUiChanges: boolean; linodeCreateWithFirewall: boolean; mainContentBanner: MainContentBanner; metadata: boolean; diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx index 1958752ba0c..29bb7740f57 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx @@ -67,7 +67,7 @@ export const SelectLinodePanel = (props: Props) => { // Capture the selected linode when this component mounts, // so it doesn't change when the user selects a different one. const [preselectedLinodeID] = React.useState( - flags.linodeCloneUIChanges && selectedLinodeID + flags.linodeCloneUiChanges && selectedLinodeID ); const searchText = React.useMemo( @@ -88,7 +88,7 @@ export const SelectLinodePanel = (props: Props) => { ); const SelectComponent = - matchesMdUp && flags.linodeCloneUIChanges + matchesMdUp && flags.linodeCloneUiChanges ? SelectLinodeTable : SelectLinodeCards; @@ -129,14 +129,14 @@ export const SelectLinodePanel = (props: Props) => { {!!header ? header : 'Select Linode'} - {flags.linodeCloneUIChanges && ( + {flags.linodeCloneUiChanges && ( { text: 'This newly created Linode will be created with the same password and SSH Keys (if any) as the original Linode.', }, - ...(flags.linodeCloneUIChanges + ...(flags.linodeCloneUiChanges ? [ { level: 'warning' as const, From 1b305d458f6978bd37b3ddc11f7d1a49c3479588 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Wed, 21 Feb 2024 17:34:10 -0500 Subject: [PATCH 28/34] Add unit tests for SelectLinodePanel --- .../SelectLinodePanel.test.tsx | 270 ++++++++++++++++++ .../SelectLinodePanel/SelectLinodePanel.tsx | 1 + .../SelectLinodeRow.test.tsx | 21 +- 3 files changed, 284 insertions(+), 8 deletions(-) create mode 100644 packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.test.tsx diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.test.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.test.tsx new file mode 100644 index 00000000000..08980847e58 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.test.tsx @@ -0,0 +1,270 @@ +import { fireEvent } from '@testing-library/react'; +import { rest } from 'msw'; +import React from 'react'; + +import { imageFactory, linodeFactory } from 'src/factories'; +import { breakpoints } from 'src/foundations/breakpoints'; +import { server } from 'src/mocks/testServer'; +import { renderWithTheme } from 'src/utilities/testHelpers'; +import { resizeScreenSize } from 'src/utilities/testHelpers'; + +import { SelectLinodePanel } from './SelectLinodePanel'; + +const defaultProps = { + handleSelection: vi.fn(), + linodes: linodeFactory.buildList(3), +}; + +const setupMocks = () => { + const image1 = imageFactory.build({ + id: 'linode/debian10', + label: 'Debian 10', + }); + + server.use( + rest.get('*/linode/instances/:linodeId', (req, res, ctx) => { + return res(ctx.json(defaultProps.linodes[0])); + }), + rest.get('*/images/:imageId', (req, res, ctx) => { + return res(ctx.json(image1)); + }) + ); +}; + +describe('SelectLinodePanel (table, desktop)', () => { + beforeAll(() => { + resizeScreenSize(breakpoints.values.lg); + setupMocks(); + }); + + it('renders as a table', async () => { + const { container, findAllByRole, findByRole } = renderWithTheme( + , + { + flags: { linodeCloneUiChanges: true }, + } + ); + + expect(await findByRole('table')).toBeInTheDocument(); + + expect( + container.querySelector('[data-qa-linode-cards]') + ).not.toBeInTheDocument(); + + expect((await findAllByRole('row')).length).toBe(4); + }); + + it('can be disabled', async () => { + const { findAllByRole } = renderWithTheme( + , + { + flags: { linodeCloneUiChanges: true }, + } + ); + + for (const radio of await findAllByRole('radio')) { + expect(radio).toBeDisabled(); + } + }); + + it('selects the plan when clicked', async () => { + const mockOnSelect = vi.fn(); + + const { findAllByRole } = renderWithTheme( + , + { + flags: { linodeCloneUiChanges: true }, + } + ); + + const radioInput = (await findAllByRole('radio'))[0]; + fireEvent.click(radioInput); + + expect(mockOnSelect).toHaveBeenCalledWith( + 0, + defaultProps.linodes[0].type, + defaultProps.linodes[0].specs.disk + ); + }); + + it('allows searching', async () => { + setupMocks(); + + const { findAllByRole, findByRole } = renderWithTheme( + , + { + flags: { linodeCloneUiChanges: true }, + } + ); + + fireEvent.change(await findByRole('textbox'), { + target: { value: defaultProps.linodes[0].label }, + }); + + await expect((await findAllByRole('row')).length).toBe(2); + + expect((await findAllByRole('row'))[1]).toHaveTextContent( + defaultProps.linodes[0].label + ); + }); + + it('displays the heading, notices and error', () => { + const { getByText } = renderWithTheme( + , + { + flags: { linodeCloneUiChanges: true }, + } + ); + + expect(getByText('Example error')).toBeInTheDocument(); + expect(getByText('Example header')).toBeInTheDocument(); + expect(getByText('Example notice')).toBeInTheDocument(); + }); + + it('prefills the search box when mounted with a selected linode', async () => { + setupMocks(); + + const { container, findAllByRole } = renderWithTheme( + , + { + flags: { linodeCloneUiChanges: true }, + } + ); + + expect( + container.querySelector('[data-qa-linode-search] input') + ).toHaveValue(defaultProps.linodes[0].label); + + expect((await findAllByRole('row')).length).toBe(2); + + expect((await findAllByRole('row'))[1]).toHaveTextContent( + defaultProps.linodes[0].label + ); + }); +}); + +describe('SelectLinodePanel (cards, mobile)', () => { + beforeAll(() => { + resizeScreenSize(breakpoints.values.sm); + setupMocks(); + }); + + it('renders as cards', async () => { + const { container, queryByRole } = renderWithTheme( + , + { + flags: { linodeCloneUiChanges: true }, + } + ); + + expect(queryByRole('table')).not.toBeInTheDocument(); + + expect(container.querySelectorAll('[data-qa-selection-card]').length).toBe( + 3 + ); + }); + + it('can be disabled', async () => { + const { container } = renderWithTheme( + , + { + flags: { linodeCloneUiChanges: true }, + } + ); + + container + .querySelectorAll('[data-qa-selection-card]') + .forEach((card) => expect(card).toBeDisabled()); + }); + + it('selects the plan when clicked', async () => { + const mockOnSelect = vi.fn(); + + const { container } = renderWithTheme( + , + { + flags: { linodeCloneUiChanges: true }, + } + ); + + const selectionCard = container.querySelectorAll( + '[data-qa-selection-card]' + )[0]; + fireEvent.click(selectionCard); + + expect(mockOnSelect).toHaveBeenCalledWith( + 0, + defaultProps.linodes[0].type, + defaultProps.linodes[0].specs.disk + ); + }); + + it('allows searching', async () => { + setupMocks(); + + const { container, findByRole } = renderWithTheme( + , + { + flags: { linodeCloneUiChanges: true }, + } + ); + + fireEvent.change(await findByRole('textbox'), { + target: { value: defaultProps.linodes[0].label }, + }); + + await expect( + container.querySelectorAll('[data-qa-selection-card]').length + ).toBe(1); + + expect( + container.querySelectorAll('[data-qa-selection-card]')[0] + ).toHaveTextContent(defaultProps.linodes[0].label); + }); + + it('displays the heading, notices and error', () => { + const { getByText } = renderWithTheme( + , + { + flags: { linodeCloneUiChanges: true }, + } + ); + + expect(getByText('Example error')).toBeInTheDocument(); + expect(getByText('Example header')).toBeInTheDocument(); + expect(getByText('Example notice')).toBeInTheDocument(); + }); + + it('prefills the search box when mounted with a selected linode', async () => { + setupMocks(); + + const { container } = renderWithTheme( + , + { + flags: { linodeCloneUiChanges: true }, + } + ); + + expect( + container.querySelector('[data-qa-linode-search] input') + ).toHaveValue(defaultProps.linodes[0].label); + + expect( + container.querySelector('[data-qa-selection-card]') + ).toBeInTheDocument(); + + expect( + container.querySelector('[data-qa-selection-card]') + ).toHaveTextContent(defaultProps.linodes[0].label); + }); +}); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx index 29bb7740f57..d12f543bf05 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx @@ -147,6 +147,7 @@ export const SelectLinodePanel = (props: Props) => { width: '330px', }} clearable + data-qa-linode-search debounceTime={0} expand={true} hideLabel diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx index 8d217ecf89f..d2e597a5fbd 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx @@ -38,7 +38,12 @@ describe('SelectLinodeRow', () => { }) ); - const { getAllByRole, getByTestId, getByText } = renderWithTheme( + const { + findByText, + getAllByRole, + getByTestId, + getByText, + } = renderWithTheme( wrapWithTableBody( { getByText(linode1.label); getByText('Running'); - getByText('Debian 10'); - getByText('Linode 1 GB'); - getByText('Newark, NJ'); + await findByText('Debian 10'); + await findByText('Linode 1 GB'); + await findByText('Newark, NJ'); const selectButton = getAllByRole('button')[0]; fireEvent.click(selectButton); @@ -91,7 +96,7 @@ describe('SelectLinodeRow', () => { }) ); - const { getByTestId, getByText, queryByText } = renderWithTheme( + const { findByText, getByTestId, getByText, queryByText } = renderWithTheme( wrapWithTableBody( { getByText(linode1.label); getByText('Offline'); - getByText('Debian 10'); - getByText('Linode 1 GB'); - getByText('Newark, NJ'); + await findByText('Debian 10'); + await findByText('Linode 1 GB'); + await findByText('Newark, NJ'); expect(queryByText('Power Off')).not.toBeInTheDocument(); }); From e01bc6b48b467076efbddfe776b72271a0f40abb Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Wed, 21 Feb 2024 17:36:01 -0500 Subject: [PATCH 29/34] Only show power off action for selected linodes --- .../Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx index 20240a49bbb..3624deacdcf 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx @@ -132,7 +132,7 @@ export const SelectLinodeRow = (props: Props) => { - {isRunning && ( + {isRunning && selected && ( Date: Wed, 21 Feb 2024 17:51:10 -0500 Subject: [PATCH 30/34] Feedback @bnussman-akamai --- .../InlineMenuAction/InlineMenuAction.tsx | 2 +- .../SelectLinodePanel/SelectLinodeRow.test.tsx | 17 ++--------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx b/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx index e1ae86412d4..94e0f7d417f 100644 --- a/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx +++ b/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx @@ -56,7 +56,7 @@ export const InlineMenuAction = (props: InlineMenuActionProps) => { disabled={disabled} loading={loading} onClick={onClick} - sx={buttonHeight != undefined ? { height: buttonHeight } : {}} + sx={buttonHeight !== undefined ? { height: buttonHeight } : {}} tooltipAnalyticsEvent={tooltipAnalyticsEvent} tooltipText={tooltip} {...rest} diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx index d2e597a5fbd..4a19ad70c83 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx @@ -1,7 +1,6 @@ import { waitForElementToBeRemoved } from '@testing-library/react'; import { fireEvent } from '@testing-library/react'; import * as React from 'react'; -import { QueryClient } from 'react-query'; import { imageFactory } from 'src/factories'; import { linodeFactory } from 'src/factories/linodes'; @@ -10,12 +9,6 @@ import { renderWithTheme, wrapWithTableBody } from 'src/utilities/testHelpers'; import { SelectLinodeRow } from './SelectLinodeRow'; -const queryClient = new QueryClient(); - -afterEach(() => { - queryClient.clear(); -}); - const loadingTestId = 'circle-progress'; describe('SelectLinodeRow', () => { @@ -51,10 +44,7 @@ describe('SelectLinodeRow', () => { linodeId={linode1.id} selected /> - ), - { - queryClient, - } + ) ); // Loading state should render @@ -104,10 +94,7 @@ describe('SelectLinodeRow', () => { linodeId={linode1.id} selected /> - ), - { - queryClient, - } + ) ); // Loading state should render From ea6d1ad71b8a6823b87c33dcea529ad9a1b846ee Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Wed, 21 Feb 2024 18:16:47 -0500 Subject: [PATCH 31/34] Bulleted notices --- .../SelectLinodePanel.test.tsx | 4 +- .../SelectLinodePanel/SelectLinodePanel.tsx | 44 ++++++++++++------- .../TabbedContent/FromBackupsContent.tsx | 10 ++--- .../TabbedContent/FromLinodeContent.tsx | 12 +---- 4 files changed, 34 insertions(+), 36 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.test.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.test.tsx index 08980847e58..a423e52f689 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.test.tsx @@ -114,7 +114,7 @@ describe('SelectLinodePanel (table, desktop)', () => { {...defaultProps} error={'Example error'} header={'Example header'} - notices={[{ level: 'warning', text: 'Example notice' }]} + notices={['Example notice']} />, { flags: { linodeCloneUiChanges: true }, @@ -233,7 +233,7 @@ describe('SelectLinodePanel (cards, mobile)', () => { {...defaultProps} error={'Example error'} header={'Example header'} - notices={[{ level: 'warning', text: 'Example notice' }]} + notices={['Example notice']} />, { flags: { linodeCloneUiChanges: true }, diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx index d12f543bf05..c66e2d9166a 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx @@ -5,6 +5,8 @@ import * as React from 'react'; import { Box } from 'src/components/Box'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; +import { List } from 'src/components/List'; +import { ListItem } from 'src/components/ListItem'; import { Notice } from 'src/components/Notice/Notice'; import { OrderByProps, sortData } from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; @@ -19,18 +21,13 @@ import { PowerActionsDialog } from '../../PowerActionsDialogOrDrawer'; import { SelectLinodeCards } from './SelectLinodeCards'; import { SelectLinodeTable } from './SelectLinodeTable'; -interface Notice { - level: 'error' | 'warning'; // most likely only going to need these two 'warning'; 'warning'; - text: string; -} - interface Props { disabled?: boolean; error?: string; handleSelection: (id: number, type: null | string, diskSize?: number) => void; header?: string; linodes: Linode[]; - notices?: Notice[]; + notices?: string[]; selectedLinodeID?: number; } @@ -115,17 +112,30 @@ export const SelectLinodePanel = (props: Props) => { variant="error" /> )} - {notices && - !disabled && - notices.map((notice, i) => ( - - ))} + {notices && !disabled && notices.length > 0 && ( + + + Notice: + + ({ + '& > li': { + display: 'list-item', + fontSize: '0.875rem', + pb: 0, + pl: 0, + }, + listStyle: 'disc', + ml: theme.spacing(2), + mt: theme.spacing(), + })} + > + {notices.map((notice, i) => ( + {notice} + ))} + + + )} { { Date: Wed, 21 Feb 2024 18:26:59 -0500 Subject: [PATCH 32/34] Don't show power actions for from backup --- .../SelectLinodePanel/SelectLinodePanel.tsx | 4 ++ .../SelectLinodeRow.test.tsx | 46 +++++++++++++++++++ .../SelectLinodePanel/SelectLinodeRow.tsx | 27 ++++++----- .../SelectLinodePanel/SelectLinodeTable.tsx | 7 ++- .../TabbedContent/FromLinodeContent.tsx | 1 + 5 files changed, 73 insertions(+), 12 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx index c66e2d9166a..78b96ecabb2 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx @@ -29,6 +29,7 @@ interface Props { linodes: Linode[]; notices?: string[]; selectedLinodeID?: number; + showPowerActions?: boolean; } export const SelectLinodePanel = (props: Props) => { @@ -40,6 +41,7 @@ export const SelectLinodePanel = (props: Props) => { linodes, notices, selectedLinodeID, + showPowerActions, } = props; const { handleOrderChange, order, orderBy } = useOrder( @@ -179,6 +181,7 @@ export const SelectLinodePanel = (props: Props) => { disabled={disabled ?? false} handleSelection={handleSelection} selectedLinodeId={selectedLinodeID} + showPowerActions={showPowerActions ?? false} /> @@ -213,6 +216,7 @@ export interface RenderLinodeProps { handleSelection: Props['handleSelection']; orderBy: OrderByProps; selectedLinodeId: number | undefined; + showPowerActions: boolean; } const StyledBox = styled(Box, { diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx index 4a19ad70c83..a651175ff47 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx @@ -43,6 +43,7 @@ describe('SelectLinodeRow', () => { handleSelection={handleSelection} linodeId={linode1.id} selected + showPowerActions /> ) ); @@ -93,6 +94,7 @@ describe('SelectLinodeRow', () => { handleSelection={handleSelection} linodeId={linode1.id} selected + showPowerActions /> ) ); @@ -110,4 +112,48 @@ describe('SelectLinodeRow', () => { expect(queryByText('Power Off')).not.toBeInTheDocument(); }); + + it('should not display power off linode button if not enabled', async () => { + const linode1 = linodeFactory.build({ + id: 1, + label: 'linode-1', + }); + const image1 = imageFactory.build({ + id: 'linode/debian10', + label: 'Debian 10', + }); + server.use( + rest.get('*/linode/instances/:linodeId', (req, res, ctx) => { + return res(ctx.json(linode1)); + }), + rest.get('*/images/:imageId', (req, res, ctx) => { + return res(ctx.json(image1)); + }) + ); + + const { findByText, getByTestId, getByText, queryByText } = renderWithTheme( + wrapWithTableBody( + + ) + ); + + // Loading state should render + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + getByText(linode1.label); + getByText('Running'); + await findByText('Debian 10'); + await findByText('Linode 1 GB'); + await findByText('Newark, NJ'); + + expect(queryByText('Power Off')).not.toBeInTheDocument(); + }); }); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx index 3624deacdcf..04dd67fc21c 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx @@ -32,6 +32,7 @@ interface Props { handleSelection: () => void; linodeId: number; selected: boolean; + showPowerActions: boolean; } export const SelectLinodeRow = (props: Props) => { @@ -42,6 +43,7 @@ export const SelectLinodeRow = (props: Props) => { handleSelection, linodeId, selected, + showPowerActions, } = props; const theme = useTheme(); @@ -131,23 +133,26 @@ export const SelectLinodeRow = (props: Props) => { - - {isRunning && selected && ( - - )} - + {showPowerActions && ( + + {isRunning && selected && ( + + )} + + )} ); }; export const SelectLinodeTableRowHead = (props: { orderBy: Omit, 'data'>; + showPowerActions: boolean; }) => { - const { orderBy } = props; + const { orderBy, showPowerActions } = props; const CustomSortCell = ( props: TableCellProps & { label: string; @@ -181,7 +186,7 @@ export const SelectLinodeTableRowHead = (props: { Region - + {showPowerActions && } ); }; diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeTable.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeTable.tsx index dbb2935fbea..686f0bb646e 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeTable.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeTable.tsx @@ -14,10 +14,14 @@ export const SelectLinodeTable = ({ handleSelection, orderBy, selectedLinodeId, + showPowerActions, }: RenderLinodeProps) => (
- + {orderBy.data.length > 0 ? ( @@ -31,6 +35,7 @@ export const SelectLinodeTable = ({ key={linode.id} linodeId={linode.id} selected={Number(selectedLinodeId) === linode.id} + showPowerActions={showPowerActions} /> )) ) : ( diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx index 6cbe079857f..91fd233eea3 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx @@ -109,6 +109,7 @@ export const FromLinodeContent = (props: CombinedProps) => { header={'Select Linode to Clone From'} linodes={linodesData} selectedLinodeID={selectedLinodeID} + showPowerActions /> )} From d3787b249d52add0ba62250dbe2fc5edacc2c8fd Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Mon, 26 Feb 2024 15:31:13 -0500 Subject: [PATCH 33/34] UX feedback: remove "Notice:" heading --- .../LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx index 78b96ecabb2..9d4b440720f 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx @@ -116,20 +116,16 @@ export const SelectLinodePanel = (props: Props) => { )} {notices && !disabled && notices.length > 0 && ( - - Notice: - ({ '& > li': { display: 'list-item', - fontSize: '0.875rem', - pb: 0, + lineHeight: theme.spacing(3), + padding: 0, pl: 0, }, listStyle: 'disc', ml: theme.spacing(2), - mt: theme.spacing(), })} > {notices.map((notice, i) => ( From 663824afd1622c5af759ecaef28d16341c822c22 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Mon, 26 Feb 2024 15:31:45 -0500 Subject: [PATCH 34/34] Fix table column rendering inconsistencies --- .../LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx | 8 ++++++-- .../LinodesCreate/SelectLinodePanel/SelectLinodeTable.tsx | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx index 04dd67fc21c..5e4bf419a29 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx @@ -80,7 +80,7 @@ export const SelectLinodeRow = (props: Props) => { if (linodeLoading || !linode) { return ( - + @@ -90,7 +90,7 @@ export const SelectLinodeRow = (props: Props) => { if (linodeError) { return ( - + { ); }; +// Keep up to date with number of columns +export const numCols = 7; + export const SelectLinodeTableRowHead = (props: { orderBy: Omit, 'data'>; showPowerActions: boolean; @@ -165,6 +168,7 @@ export const SelectLinodeTableRowHead = (props: { {...props} /> ); + return ( diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeTable.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeTable.tsx index 686f0bb646e..953ca9b1740 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeTable.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeTable.tsx @@ -6,7 +6,11 @@ import { TableHead } from 'src/components/TableHead'; import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { RenderLinodeProps } from './SelectLinodePanel'; -import { SelectLinodeRow, SelectLinodeTableRowHead } from './SelectLinodeRow'; +import { + SelectLinodeRow, + SelectLinodeTableRowHead, + numCols, +} from './SelectLinodeRow'; export const SelectLinodeTable = ({ disabled, @@ -39,7 +43,7 @@ export const SelectLinodeTable = ({ /> )) ) : ( - + )}