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))
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('')}
diff --git a/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx b/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx
index 9df73864fdd..94e0f7d417f 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 } : {}}
tooltipAnalyticsEvent={tooltipAnalyticsEvent}
tooltipText={tooltip}
{...rest}
diff --git a/packages/manager/src/dev-tools/FeatureFlagTool.tsx b/packages/manager/src/dev-tools/FeatureFlagTool.tsx
index a4ea2f4b3f2..a59d1d61d10 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: 'gecko', label: 'Gecko' },
{ 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 0a6b6bee49f..f1c127b6430 100644
--- a/packages/manager/src/featureFlags.ts
+++ b/packages/manager/src/featureFlags.ts
@@ -49,7 +49,7 @@ export interface Flags {
firewallNodebalancer: boolean;
gecko: boolean;
ipv6Sharing: boolean;
- linodeCloneUIChanges: boolean;
+ linodeCloneUiChanges: boolean;
linodeCreateWithFirewall: boolean;
mainContentBanner: MainContentBanner;
metadata: boolean;
diff --git a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx
index 8310c7c9d07..10bcd964208 100644
--- a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx
+++ b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx
@@ -82,7 +82,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,
@@ -588,19 +587,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.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx
deleted file mode 100644
index f9cfbafd690..00000000000
--- a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-import { Linode } from '@linode/api-v4/lib/linodes';
-import Grid from '@mui/material/Unstable_Grid2';
-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';
-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';
-import { useFlags } from 'src/hooks/useFlags';
-
-export interface ExtendedLinode extends Linode {
- heading: string;
- subHeadings: string[];
-}
-
-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: ExtendedLinode[];
- notices?: Notice[];
- selectedLinodeID?: number;
-}
-
-const SelectLinodePanel = (props: Props) => {
- const {
- disabled,
- error,
- handleSelection,
- header,
- linodes,
- notices,
- selectedLinodeID,
- } = props;
-
- const flags = useFlags();
-
- const theme = useTheme();
- const [userSearchText, setUserSearchText] = React.useState<
- 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
- );
-
- const searchText = React.useMemo(
- () =>
- userSearchText !== undefined
- ? userSearchText
- : linodes.find((linode) => linode.id === preselectedLinodeID)?.label ||
- '',
- [linodes, preselectedLinodeID, userSearchText]
- );
-
- const filteredLinodes = React.useMemo(
- () =>
- linodes.filter((linode) =>
- linode.label.toLowerCase().includes(searchText.toLowerCase())
- ),
- [linodes, searchText]
- );
-
- const renderCard = (linode: ExtendedLinode) => {
- return (
- {
- 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}
- />
- );
- };
-
- 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 StyledBox = styled(Box, {
- label: 'StyledBox',
-})(({ theme }) => ({
- padding: `${theme.spacing(2)} 0 0`,
-}));
-
-const StyledPaper = styled(Paper, { label: 'StyledPaper' })(({ theme }) => ({
- backgroundColor: theme.color.white,
- flexGrow: 1,
- width: '100%',
-}));
-
-export default RenderGuard(SelectLinodePanel);
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/SelectLinodeCards.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeCards.tsx
new file mode 100644
index 00000000000..eb372dc8e55
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeCards.tsx
@@ -0,0 +1,26 @@
+import Grid from '@mui/material/Unstable_Grid2';
+import React from 'react';
+
+import { SelectLinodeCard } from './SelectLinodeCard';
+import { RenderLinodeProps } from './SelectLinodePanel';
+
+export const SelectLinodeCards = ({
+ disabled,
+ handleSelection,
+ orderBy: { data: linodes },
+ selectedLinodeId,
+}: RenderLinodeProps) => (
+
+ {linodes.map((linode) => (
+
+ handleSelection(linode.id, linode.type, linode.specs.disk)
+ }
+ disabled={disabled}
+ key={linode.id}
+ linode={linode}
+ selected={linode.id == selectedLinodeId}
+ />
+ ))}
+
+);
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..a423e52f689
--- /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
new file mode 100644
index 00000000000..9d4b440720f
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodePanel.tsx
@@ -0,0 +1,228 @@
+import { Linode } from '@linode/api-v4/lib/linodes';
+import { useMediaQuery } from '@mui/material';
+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 { 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';
+import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter';
+import { Paper } from 'src/components/Paper';
+import { Stack } from 'src/components/Stack';
+import { Typography } from 'src/components/Typography';
+import { useFlags } from 'src/hooks/useFlags';
+import { useOrder } from 'src/hooks/useOrder';
+
+import { PowerActionsDialog } from '../../PowerActionsDialogOrDrawer';
+import { SelectLinodeCards } from './SelectLinodeCards';
+import { SelectLinodeTable } from './SelectLinodeTable';
+
+interface Props {
+ disabled?: boolean;
+ error?: string;
+ handleSelection: (id: number, type: null | string, diskSize?: number) => void;
+ header?: string;
+ linodes: Linode[];
+ notices?: string[];
+ selectedLinodeID?: number;
+ showPowerActions?: boolean;
+}
+
+export const SelectLinodePanel = (props: Props) => {
+ const {
+ disabled,
+ error,
+ handleSelection,
+ header,
+ linodes,
+ notices,
+ selectedLinodeID,
+ showPowerActions,
+ } = props;
+
+ const { handleOrderChange, order, orderBy } = useOrder(
+ { order: 'asc', orderBy: 'label' },
+ 'create-select-linode'
+ );
+
+ const orderedLinodes = sortData(orderBy, order)(linodes);
+
+ 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(
+ flags.linodeCloneUiChanges && selectedLinodeID
+ );
+
+ const searchText = React.useMemo(
+ () =>
+ userSearchText !== undefined
+ ? userSearchText
+ : linodes.find((linode) => linode.id === preselectedLinodeID)?.label ||
+ '',
+ [linodes, preselectedLinodeID, userSearchText]
+ );
+
+ const filteredLinodes = React.useMemo(
+ () =>
+ orderedLinodes.filter((linode) =>
+ linode.label.toLowerCase().includes(searchText.toLowerCase())
+ ),
+ [orderedLinodes, searchText]
+ );
+
+ const SelectComponent =
+ matchesMdUp && flags.linodeCloneUiChanges
+ ? SelectLinodeTable
+ : SelectLinodeCards;
+
+ return (
+ <>
+
+ {({
+ count,
+ data: linodesData,
+ handlePageChange,
+ handlePageSizeChange,
+ page,
+ pageSize,
+ }) => {
+ return (
+ <>
+
+
+ {error && (
+
+ )}
+ {notices && !disabled && notices.length > 0 && (
+
+ ({
+ '& > li': {
+ display: 'list-item',
+ lineHeight: theme.spacing(3),
+ padding: 0,
+ pl: 0,
+ },
+ listStyle: 'disc',
+ ml: theme.spacing(2),
+ })}
+ >
+ {notices.map((notice, i) => (
+ {notice}
+ ))}
+
+
+ )}
+
+
+ {!!header ? header : 'Select Linode'}
+
+ {flags.linodeCloneUiChanges && (
+
+ )}
+
+
+ setPowerOffLinode({ linodeId })
+ }
+ orderBy={{
+ data: linodesData,
+ handleOrderChange,
+ order,
+ orderBy,
+ }}
+ disabled={disabled ?? false}
+ handleSelection={handleSelection}
+ selectedLinodeId={selectedLinodeID}
+ showPowerActions={showPowerActions ?? false}
+ />
+
+
+
+ >
+ );
+ }}
+
+ {powerOffLinode && (
+ setPowerOffLinode(false)}
+ />
+ )}
+ >
+ );
+};
+
+export interface RenderLinodeProps {
+ disabled: boolean;
+ handlePowerOff: (linodeId: number) => void;
+ handleSelection: Props['handleSelection'];
+ orderBy: OrderByProps;
+ selectedLinodeId: number | undefined;
+ showPowerActions: boolean;
+}
+
+const StyledBox = styled(Box, {
+ label: 'StyledBox',
+})(({ theme }) => ({
+ padding: `${theme.spacing(2)} 0 0`,
+}));
+
+const StyledPaper = styled(Paper, { label: 'StyledPaper' })(({ theme }) => ({
+ backgroundColor: theme.color.white,
+ flexGrow: 1,
+ width: '100%',
+}));
diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx
new file mode 100644
index 00000000000..a651175ff47
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.test.tsx
@@ -0,0 +1,159 @@
+import { waitForElementToBeRemoved } from '@testing-library/react';
+import { fireEvent } from '@testing-library/react';
+import * as React from 'react';
+
+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 loadingTestId = 'circle-progress';
+
+describe('SelectLinodeRow', () => {
+ 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 {
+ findByText,
+ getAllByRole,
+ getByTestId,
+ getByText,
+ } = 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');
+
+ 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 { findByText, getByTestId, getByText, queryByText } = renderWithTheme(
+ wrapWithTableBody(
+
+ )
+ );
+
+ // Loading state should render
+ expect(getByTestId(loadingTestId)).toBeInTheDocument();
+
+ await waitForElementToBeRemoved(getByTestId(loadingTestId));
+
+ getByText(linode1.label);
+ getByText('Offline');
+ await findByText('Debian 10');
+ await findByText('Linode 1 GB');
+ await findByText('Newark, NJ');
+
+ 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
new file mode 100644
index 00000000000..5e4bf419a29
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeRow.tsx
@@ -0,0 +1,196 @@
+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';
+
+interface Props {
+ disabled?: boolean;
+ handlePowerOff: () => void;
+ handleSelection: () => void;
+ linodeId: number;
+ selected: boolean;
+ showPowerActions: boolean;
+}
+
+export const SelectLinodeRow = (props: Props) => {
+ const queryClient = useQueryClient();
+ const {
+ disabled,
+ handlePowerOff,
+ handleSelection,
+ linodeId,
+ selected,
+ showPowerActions,
+ } = 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}
+
+
+
+
+ {showPowerActions && (
+
+ {isRunning && selected && (
+
+ )}
+
+ )}
+
+ );
+};
+
+// Keep up to date with number of columns
+export const numCols = 7;
+
+export const SelectLinodeTableRowHead = (props: {
+ orderBy: Omit, 'data'>;
+ showPowerActions: boolean;
+}) => {
+ const { orderBy, showPowerActions } = props;
+ const CustomSortCell = (
+ props: TableCellProps & {
+ label: string;
+ }
+ ) => (
+
+ );
+
+ return (
+
+
+
+ Linode
+
+
+ Status
+
+
+ Image
+
+
+ Plan
+
+
+ Region
+
+ {showPowerActions && }
+
+ );
+};
diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeTable.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeTable.tsx
new file mode 100644
index 00000000000..953ca9b1740
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeTable.tsx
@@ -0,0 +1,50 @@
+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,
+ numCols,
+} from './SelectLinodeRow';
+
+export const SelectLinodeTable = ({
+ disabled,
+ handlePowerOff,
+ handleSelection,
+ orderBy,
+ selectedLinodeId,
+ showPowerActions,
+}: 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}
+ showPowerActions={showPowerActions}
+ />
+ ))
+ ) : (
+
+ )}
+
+
+);
diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx
index 9e921edf8d4..144ec864983 100644
--- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx
+++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx
@@ -3,25 +3,22 @@ import {
LinodeBackupsResponse,
getLinodeBackups,
} from '@linode/api-v4/lib/linodes';
-import { compose as ramdaCompose } from 'ramda';
import * as React from 'react';
import VolumeIcon from 'src/assets/icons/entityIcons/volume.svg';
import { Paper } from 'src/components/Paper';
import { Placeholder } from 'src/components/Placeholder/Placeholder';
import { reportException } from 'src/exceptionReporting';
-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,
ReduxStateProps,
WithLinodesTypesRegionsAndImages,
} from '../types';
-import { extendLinodes, getRegionIDFromLinodeID } from '../utilities';
import { StyledGrid } from './CommonTabbedContent.styles';
export interface LinodeWithBackups extends Linode {
@@ -76,17 +73,12 @@ export class FromBackupsContent extends React.Component {
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,30 +102,16 @@ export class FromBackupsContent extends React.Component {
) : (
- extendLinodes(
- linodes,
- imagesData,
- extendedTypes,
- regionsData
- ),
- filterLinodesWithBackups
- )(linodesData)}
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.`,
- },
+ `This newly created Linode will be created with
+ the same password and SSH Keys (if any) as the original Linode.`,
+ 'This Linode will need to be manually booted after it finishes provisioning.',
]}
disabled={disabled}
error={hasErrorFor('linode_id')}
handleSelection={this.handleLinodeSelect}
+ linodes={filterLinodesWithBackups(linodesData)}
selectedLinodeID={selectedLinodeID}
- updateFor={[selectedLinodeID, errors]}
/>
{
});
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 || '');
}
@@ -198,8 +175,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 a62df47226d..91fd233eea3 100644
--- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx
+++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx
@@ -9,13 +9,12 @@ 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,
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,
@@ -55,9 +53,9 @@ export const FromLinodeContent = (props: CombinedProps) => {
};
/** 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
+ (eachLinode) => eachLinode.id === linodeId
);
if (linode) {
@@ -96,25 +94,11 @@ export const FromLinodeContent = (props: CombinedProps) => {
) : (
{
error={hasErrorFor('linode_id')}
handleSelection={handleSelectLinode}
header={'Select Linode to Clone From'}
+ linodes={linodesData}
selectedLinodeID={selectedLinodeID}
- updateFor={[selectedLinodeID, errors]}
+ showPowerActions
/>
)}
diff --git a/packages/manager/src/features/Linodes/LinodesCreate/types.ts b/packages/manager/src/features/Linodes/LinodesCreate/types.ts
index 0907d08a61c..c59f5dc799e 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,
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];
};