Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

upcoming: [M3-7876] - Linode Create Refactor - Clone #10421

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Linode Create Refactor - Cloning ([#10421](https://github.com/linode/manager/pull/10421))
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ describe('Linode Create Details', () => {
).toBeNull();
});

it('does not render the tag select when cloning', () => {
const { queryByText } = renderWithThemeAndHookFormContext({
component: <Details />,
options: {
MemoryRouter: {
initialEntries: ['/linodes/create?type=Clone+Linode'],
},
},
});

expect(queryByText('Tags')).toBeNull();
});

it('should disable the label and tag TextFields if the user does not have permission to create a linode', async () => {
server.use(
http.get('*/v4/profile', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import { Typography } from 'src/components/Typography';
import { useIsPlacementGroupsEnabled } from 'src/features/PlacementGroups/utils';
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';

import { useLinodeCreateQueryParams } from '../utilities';
import { PlacementGroupPanel } from './PlacementGroupPanel';

export const Details = () => {
const { control } = useFormContext<CreateLinodeRequest>();
const { isPlacementGroupsEnabled } = useIsPlacementGroupsEnabled();

const { params } = useLinodeCreateQueryParams();

const isCreateLinodeRestricted = useRestrictedGlobalGrantCheck({
globalGrantType: 'add_linodes',
});
Expand All @@ -37,20 +40,22 @@ export const Details = () => {
control={control}
name="label"
/>
<Controller
render={({ field, fieldState }) => (
<TagsInput
value={
field.value?.map((tag) => ({ label: tag, value: tag })) ?? []
}
disabled={isCreateLinodeRestricted}
onChange={(item) => field.onChange(item.map((i) => i.value))}
tagError={fieldState.error?.message}
/>
)}
control={control}
name="tags"
/>
{params.type !== 'Clone Linode' && (
<Controller
render={({ field, fieldState }) => (
<TagsInput
value={
field.value?.map((tag) => ({ label: tag, value: tag })) ?? []
}
disabled={isCreateLinodeRestricted}
onChange={(item) => field.onChange(item.map((i) => i.value))}
tagError={fieldState.error?.message}
/>
)}
control={control}
name="tags"
/>
)}
{isPlacementGroupsEnabled && <PlacementGroupPanel />}
</Paper>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import React from 'react';

import { linodeFactory } from 'src/factories';
import { makeResourcePage } from 'src/mocks/serverHandlers';
import { HttpResponse, http, server } from 'src/mocks/testServer';
import {
mockMatchMedia,
renderWithThemeAndHookFormContext,
} from 'src/utilities/testHelpers';
import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';

import { LinodeSelect } from './LinodeSelect';

beforeAll(() => mockMatchMedia());

describe('LinodeSelect', () => {
it('should render a heading', () => {
const { getByText } = renderWithThemeAndHookFormContext({
Expand All @@ -23,48 +15,4 @@ describe('LinodeSelect', () => {
expect(heading).toBeVisible();
expect(heading.tagName).toBe('H2');
});

it('should render Linodes from the API', async () => {
const linodes = linodeFactory.buildList(10);

server.use(
http.get('*/linode/instances*', () => {
return HttpResponse.json(makeResourcePage(linodes));
})
);

const { findByText } = renderWithThemeAndHookFormContext({
component: <LinodeSelect />,
});

for (const linode of linodes) {
// eslint-disable-next-line no-await-in-loop
await findByText(linode.label);
}
});

it('should select a linode based on form state', async () => {
const selectedLinode = linodeFactory.build({
id: 1,
label: 'my-selected-linode',
});

server.use(
http.get('*/linode/instances*', () => {
return HttpResponse.json(makeResourcePage([selectedLinode]));
})
);

const { findByLabelText } = renderWithThemeAndHookFormContext({
component: <LinodeSelect />,
useFormOptions: {
defaultValues: { linode: selectedLinode },
},
});

const radio = await findByLabelText(selectedLinode.label);

expect(radio).toBeEnabled();
expect(radio).toBeChecked();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Stack } from 'src/components/Stack';
import { Typography } from 'src/components/Typography';

import { BackupsWarning } from './BackupsWarning';
import { LinodeSelectTable } from './LinodeSelectTable';
import { LinodeSelectTable } from '../../shared/LinodeSelectTable';

export const LinodeSelect = () => {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';

import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';

import { Clone } from './Clone';

describe('Clone', () => {
it('should render a heading', () => {
const { getByText } = renderWithThemeAndHookFormContext({
component: <Clone />,
});

const heading = getByText('Select Linode to Clone From');

expect(heading).toBeVisible();
expect(heading.tagName).toBe('H2');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { useFormContext } from 'react-hook-form';

import { Notice } from 'src/components/Notice/Notice';
import { Paper } from 'src/components/Paper';
import { Stack } from 'src/components/Stack';
import { Typography } from 'src/components/Typography';

import { LinodeCreateFormValues } from '../../utilities';
import { LinodeSelectTable } from '../../shared/LinodeSelectTable';
import { CloneWarning } from './CloneWarning';

export const Clone = () => {
const {
formState: { errors },
} = useFormContext<LinodeCreateFormValues>();

return (
<Paper>
<Stack spacing={1}>
<Typography variant="h2">Select Linode to Clone From</Typography>
{errors.linode?.message && (
<Notice text={errors.linode.message} variant="error" />
)}
<CloneWarning />
<LinodeSelectTable enablePowerOff />
</Stack>
</Paper>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ListItem } from '@mui/material';
import React from 'react';

import { List } from 'src/components/List';
import { Notice } from 'src/components/Notice/Notice';

export const CloneWarning = () => {
return (
<Notice variant="warning">
<List sx={{ listStyleType: 'disc', pl: 2.5 }}>
<ListItem sx={{ display: 'list-item', pl: 1, py: 0.5 }}>
This newly created Linode will be created with the same password and
SSH Keys (if any) as the original Linode.
</ListItem>
<ListItem sx={{ display: 'list-item', pl: 1, py: 0.5 }}>
To help avoid data corruption during the cloning process, we recommend
powering off your Compute Instance prior to cloning.
</ListItem>
</List>
</Notice>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';

import { renderWithTheme } from 'src/utilities/testHelpers';

import { LinodeCreatev2 } from '.';

describe('Linode Create', () => {
it('Should not render VLANs when cloning', () => {
const { queryByText } = renderWithTheme(<LinodeCreatev2 />, {
MemoryRouter: { initialEntries: ['/linodes/create?type=Clone+Linode'] },
});

expect(queryByText('VLAN')).toBeNull();
});

it('Should not render access panel items when cloning', () => {
const { queryByText } = renderWithTheme(<LinodeCreatev2 />, {
MemoryRouter: { initialEntries: ['/linodes/create?type=Clone+Linode'] },
});

expect(queryByText('Root Password')).toBeNull();
expect(queryByText('SSH Keys')).toBeNull();
});

it('Should not render the region select when creating from a backup', () => {
const { queryByText } = renderWithTheme(<LinodeCreatev2 />, {
MemoryRouter: { initialEntries: ['/linodes/create?type=Backups'] },
});

expect(queryByText('Region')).toBeNull();
});
});
51 changes: 32 additions & 19 deletions packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import { Tab } from 'src/components/Tabs/Tab';
import { TabList } from 'src/components/Tabs/TabList';
import { TabPanels } from 'src/components/Tabs/TabPanels';
import { Tabs } from 'src/components/Tabs/Tabs';
import { useCreateLinodeMutation } from 'src/queries/linodes/linodes';
import {
useCloneLinodeMutation,
useCreateLinodeMutation,
} from 'src/queries/linodes/linodes';

import { Access } from './Access';
import { Actions } from './Actions';
Expand All @@ -20,8 +23,10 @@ import { Error } from './Error';
import { Firewall } from './Firewall';
import { Plan } from './Plan';
import { Region } from './Region';
import { linodeCreateResolvers } from './resolvers';
import { Summary } from './Summary';
import { Backups } from './Tabs/Backups/Backups';
import { Clone } from './Tabs/Clone/Clone';
import { Distributions } from './Tabs/Distributions';
import { Images } from './Tabs/Images';
import { Marketplace } from './Tabs/Marketplace/Marketplace';
Expand All @@ -33,7 +38,6 @@ import {
defaultValuesMap,
getLinodeCreatePayload,
getTabIndex,
resolver,
tabs,
useLinodeCreateQueryParams,
} from './utilities';
Expand All @@ -43,21 +47,40 @@ import { VPC } from './VPC/VPC';
import type { SubmitHandler } from 'react-hook-form';

export const LinodeCreatev2 = () => {
const { params, setParams } = useLinodeCreateQueryParams();

const methods = useForm<LinodeCreateFormValues>({
defaultValues,
mode: 'onBlur',
resolver,
resolver: linodeCreateResolvers[params.type ?? 'Distributions'],
});

const history = useHistory();

const { mutateAsync: createLinode } = useCreateLinodeMutation();
const { mutateAsync: cloneLinode } = useCloneLinodeMutation();

const currentTabIndex = getTabIndex(params.type);

const onTabChange = (index: number) => {
const newTab = tabs[index];
// Update tab "type" query param. (This changes the selected tab)
setParams({ type: newTab });
// Reset the form values
methods.reset(defaultValuesMap[newTab]);
};

const onSubmit: SubmitHandler<LinodeCreateFormValues> = async (values) => {
const payload = getLinodeCreatePayload(values);
alert(JSON.stringify(payload, null, 2));
try {
const linode = await createLinode(payload);
const linode =
params.type === 'Clone Linode'
? await cloneLinode({
sourceLinodeId: values.linode?.id ?? -1,
...payload,
})
: await createLinode(payload);

history.push(`/linodes/${linode.id}`);
} catch (errors) {
Expand All @@ -71,18 +94,6 @@ export const LinodeCreatev2 = () => {
}
};

const { params, setParams } = useLinodeCreateQueryParams();

const currentTabIndex = getTabIndex(params.type);

const onTabChange = (index: number) => {
const newTab = tabs[index];
// Update tab "type" query param. (This changes the selected tab)
setParams({ type: newTab });
// Reset the form values
methods.reset(defaultValuesMap[newTab]);
};

return (
<FormProvider {...methods}>
<DocumentTitleSegment segment="Create a Linode" />
Expand Down Expand Up @@ -119,16 +130,18 @@ export const LinodeCreatev2 = () => {
<SafeTabPanel index={4}>
<Backups />
</SafeTabPanel>
<SafeTabPanel index={5}>Clone Linode</SafeTabPanel>
<SafeTabPanel index={5}>
<Clone />
</SafeTabPanel>
</TabPanels>
</Tabs>
{params.type !== 'Backups' && <Region />}
<Plan />
<Details />
<Access />
{params.type !== 'Clone Linode' && <Access />}
<VPC />
<Firewall />
<VLAN />
{params.type !== 'Clone Linode' && <VLAN />}
<UserData />
<Addons />
<Summary />
Expand Down
Loading
Loading