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 16 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
@@ -0,0 +1,60 @@
import { HttpResponse, http } from 'msw';
import React from 'react';

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

import { LinodeSelectTable } from './LinodeSelectTable';

beforeAll(() => mockMatchMedia());

describe('Linode Select Table', () => {
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: <LinodeSelectTable />,
});

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: <LinodeSelectTable />,
useFormOptions: {
defaultValues: { linode: selectedLinode },
},
});

const radio = await findByLabelText(selectedLinode.label);

expect(radio).toBeEnabled();
expect(radio).toBeChecked();
});
});
bnussman-akamai marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Grid from '@mui/material/Unstable_Grid2';
import useMediaQuery from '@mui/material/useMediaQuery';
import React, { useState } from 'react';
import { useController, useFormContext, useWatch } from 'react-hook-form';
import { useFormContext, useWatch } from 'react-hook-form';

import { Box } from 'src/components/Box';
import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField';
Expand All @@ -18,9 +18,11 @@ import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'
import { TableSortCell } from 'src/components/TableSortCell';
import { Typography } from 'src/components/Typography';
import { SelectLinodeCard } from 'src/features/Linodes/LinodesCreate/SelectLinodePanel/SelectLinodeCard';
import { PowerActionsDialog } from 'src/features/Linodes/PowerActionsDialogOrDrawer';
import { useOrder } from 'src/hooks/useOrder';
import { usePagination } from 'src/hooks/usePagination';
import { useLinodesQuery } from 'src/queries/linodes/linodes';
import { privateIPRegex } from 'src/utilities/ipUtils';
import { isNumeric } from 'src/utilities/stringUtils';

import {
Expand All @@ -32,15 +34,24 @@ import { LinodeSelectTableRow } from './LinodeSelectTableRow';
import type { Linode } from '@linode/api-v4';
import type { Theme } from '@mui/material';

export const LinodeSelectTable = () => {
interface Props {
/**
* Adds an extra column that will dispay a "power off" option when the row is selected
*/
enablePowerOff?: boolean;
}

export const LinodeSelectTable = (props: Props) => {
const { enablePowerOff } = props;

const matchesMdUp = useMediaQuery((theme: Theme) =>
theme.breakpoints.up('md')
);

const { setValue } = useFormContext<LinodeCreateFormValues>();
const linode = useWatch<LinodeCreateFormValues, 'linode'>({ name: 'linode' });
const { control, reset } = useFormContext<LinodeCreateFormValues>();

const { field } = useController<LinodeCreateFormValues, 'linode'>({
const selectedLinode = useWatch<LinodeCreateFormValues>({
control,
name: 'linode',
});

Expand All @@ -50,7 +61,8 @@ export const LinodeSelectTable = () => {
params.linodeID
);

const [query, setQuery] = useState(linode?.label ?? '');
const [query, setQuery] = useState(selectedLinode?.label ?? '');
const [linodeToPowerOff, setLinodeToPowerOff] = useState<Linode>();

const pagination = usePagination();
const order = useOrder();
Expand All @@ -76,14 +88,19 @@ export const LinodeSelectTable = () => {
);

const handleSelect = (linode: Linode) => {
setValue('backup_id', null);
setValue('region', linode.region);
if (linode.type) {
setValue('type', linode.type);
}
field.onChange(linode);
const hasPrivateIP = linode.ipv4.some((ipv4) => privateIPRegex.test(ipv4));
reset((prev) => ({
...prev,
backup_id: null,
linode,
private_ip: hasPrivateIP,
region: linode.region,
type: linode.type ?? '',
}));
};

const columns = enablePowerOff ? 6 : 5;

return (
<Stack pt={1} spacing={2}>
<DebouncedSearchTextField
Expand All @@ -94,16 +111,16 @@ export const LinodeSelectTable = () => {
}
setQuery(value ?? '');
},
value: preselectedLinodeId ? linode?.label ?? '' : query,
value: preselectedLinodeId ? selectedLinode?.label ?? '' : query,
}}
clearable
hideLabel
isSearching={isFetching}
label="Search"
placeholder="Search"
/>
{matchesMdUp ? (
<>
<Box>
{matchesMdUp ? (
<Table>
<TableHead>
<TableRow>
Expand All @@ -126,46 +143,60 @@ export const LinodeSelectTable = () => {
>
Region
</TableSortCell>
{enablePowerOff && <TableCell></TableCell>}
bnussman-akamai marked this conversation as resolved.
Show resolved Hide resolved
</TableRow>
</TableHead>
<TableBody>
{isLoading && <TableRowLoading columns={5} rows={10} />}
{error && <TableRowError colSpan={5} message={error[0].reason} />}
{data?.results === 0 && <TableRowEmpty colSpan={5} />}
{isLoading && <TableRowLoading columns={columns} rows={10} />}
{error && (
<TableRowError colSpan={columns} message={error[0].reason} />
)}
{data?.results === 0 && <TableRowEmpty colSpan={columns} />}
{data?.data.map((linode) => (
<LinodeSelectTableRow
onPowerOff={
enablePowerOff
? () => setLinodeToPowerOff(linode)
: undefined
}
key={linode.id}
linode={linode}
onSelect={() => handleSelect(linode)}
selected={linode.id === field.value?.id}
selected={linode.id === selectedLinode?.id}
/>
))}
</TableBody>
</Table>
<PaginationFooter
count={data?.results ?? 0}
handlePageChange={pagination.handlePageChange}
handleSizeChange={pagination.handlePageSizeChange}
page={pagination.page}
pageSize={pagination.pageSize}
/>
</>
) : (
<Box>
) : (
<Grid container spacing={2}>
{data?.data.map((linode) => (
<SelectLinodeCard
handleSelection={() => handleSelect(linode)}
key={linode.id}
linode={linode}
selected={linode.id === field.value?.id}
selected={linode.id === selectedLinode?.id}
/>
))}
{data?.results === 0 && (
<Typography padding={1}>No results</Typography>
)}
</Grid>
</Box>
)}
<PaginationFooter
count={data?.results ?? 0}
handlePageChange={pagination.handlePageChange}
handleSizeChange={pagination.handlePageSizeChange}
page={pagination.page}
pageSize={pagination.pageSize}
/>
</Box>
{enablePowerOff && (
<PowerActionsDialog
action="Power Off"
isOpen={Boolean(linodeToPowerOff)}
linodeId={linodeToPowerOff?.id}
onClose={() => setLinodeToPowerOff(undefined)}
/>
Copy link
Contributor

@abailly-akamai abailly-akamai May 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this visual regression with the power off button rendering. I think there should be a default column with set width at all times to account for when the buttons renders?

also adding a couple issues with the way things are currently

new defect:

Screen.Recording.2024-05-02.at.09.39.23.mov

already in prod (but we could fix here)
we should use white-space: nowrap; on the button
Screenshot 2024-05-02 at 09 42 20

we don't have reboot for the cards. Is is by design?
Screenshot 2024-05-02 at 09 42 42

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hkhalil-akamai Any context on no "Power Off" in card view?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Power Off" button wrapping / column width issues should be fixed in 7f27bad (cc @hkhalil-akamai )

)}
</Stack>
);
Expand Down
Loading
Loading