Skip to content

Commit

Permalink
upcoming: [M3-7927] - Linode Create Refactor - Part 6 - Add-ons (#10319)
Browse files Browse the repository at this point in the history
* initial work

* add changesets and other fixes

* handle permissions in more places

* diable create button if user does not have permission

* add more unit testing

* Apply suggestions from code review

Co-authored-by: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com>

* memoize selected region

---------

Co-authored-by: Banks Nussman <banks@nussman.us>
Co-authored-by: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com>
  • Loading branch information
3 people authored Mar 27, 2024
1 parent a5d9483 commit d4e0d0a
Show file tree
Hide file tree
Showing 20 changed files with 696 additions and 2 deletions.
5 changes: 5 additions & 0 deletions packages/api-v4/.changeset/pr-10319-changed-1711487054721.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Changed
---

Added jsdoc style comments to `CreateLinodeRequest` based on API documentation ([#10319](https://github.com/linode/manager/pull/10319))
5 changes: 5 additions & 0 deletions packages/api-v4/.changeset/pr-10319-changed-1711487087221.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Changed
---

Allows `firewall_id` to be `null` in `CreateLinodeRequest` ([#10319](https://github.com/linode/manager/pull/10319))
82 changes: 81 additions & 1 deletion packages/api-v4/src/linodes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,24 +350,104 @@ export interface CreateLinodePlacementGroupPayload {
}

export interface CreateLinodeRequest {
/**
* The Linode Type of the Linode you are creating.
*/
type: string;
/**
* The Region where the Linode will be located.
*/
region: string;
/**
* A StackScript ID that will cause the referenced StackScript to be run during deployment of this Linode.
*
* This field cannot be used when deploying from a Backup or a Private Image.
*/
stackscript_id?: number;
/**
* A Backup ID from another Linode’s available backups.
*
* Your User must have read_write access to that Linode,
* the Backup must have a status of successful,
* and the Linode must be deployed to the same region as the Backup.
*
* This field and the image field are mutually exclusive.
*/
backup_id?: number;
/**
* When deploying from an Image, this field is optional, otherwise it is ignored.
* This is used to set the swap disk size for the newly-created Linode.
* @default 512
*/
swap_size?: number;
/**
* An Image ID to deploy the Linode Disk from.
*/
image?: string | null;
/**
* This sets the root user’s password on a newly-created Linode Disk when deploying from an Image.
*/
root_pass?: string;
/**
* A list of public SSH keys that will be automatically appended to the root user’s
* `~/.ssh/authorized_keys`file when deploying from an Image.
*/
authorized_keys?: string[];
/**
* If this field is set to true, the created Linode will automatically be enrolled in the Linode Backup service.
* This will incur an additional charge. The cost for the Backup service is dependent on the Type of Linode deployed.
*
* This option is always treated as true if the account-wide backups_enabled setting is true.
*
* @default false
*/
backups_enabled?: boolean;
/**
* This field is required only if the StackScript being deployed requires input data from the User for successful completion
*/
stackscript_data?: any;
/**
* If it is deployed from an Image or a Backup and you wish it to remain offline after deployment, set this to false.
* @default true if the Linode is created with an Image or from a Backup.
*/
booted?: boolean;
/**
* The Linode’s label is for display purposes only.
* If no label is provided for a Linode, a default will be assigned.
*/
label?: string;
/**
* An array of tags applied to this object.
*
* Tags are for organizational purposes only.
*/
tags?: string[];
/**
* If true, the created Linode will have private networking enabled and assigned a private IPv4 address.
* @default false
*/
private_ip?: boolean;
/**
* A list of usernames. If the usernames have associated SSH keys,
* the keys will be appended to the root users `~/.ssh/authorized_keys`
* file automatically when deploying from an Image.
*/
authorized_users?: string[];
/**
* An array of Network Interfaces to add to this Linode’s Configuration Profile.
*/
interfaces?: InterfacePayload[];
/**
* An object containing user-defined data relevant to the creation of Linodes.
*/
metadata?: UserData;
firewall_id?: number;
/**
* The `id` of the Firewall to attach this Linode to upon creation.
*/
firewall_id?: number | null;
/**
* An object that assigns this the Linode to a placment group upon creation.
*/
placement_group?: CreateLinodePlacementGroupPayload;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Linode Create Refactor - Part 6 - Add-ons ([#10319](https://github.com/linode/manager/pull/10319))
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';

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

import { Addons } from './Addons';

describe('Linode Create v2 Addons', () => {
it('should render an "Add-ons" heading', () => {
const { getByText } = renderWithThemeAndHookFormContext({
component: <Addons />,
});

const heading = getByText('Add-ons');

expect(heading).toBeVisible();
expect(heading.tagName).toBe('H2');
});

it('renders a warning if an edge region is selected', async () => {
const region = regionFactory.build({ site_type: 'edge' });

server.use(
http.get('*/v4/regions', () => {
return HttpResponse.json(makeResourcePage([region]));
})
);

const { findByText } = renderWithThemeAndHookFormContext({
component: <Addons />,
useFormOptions: { defaultValues: { region: region.id } },
});

await findByText(
'Backups and Private IP are currently not available for Edge regions.'
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, { useMemo } from 'react';
import { useWatch } from 'react-hook-form';

import { Divider } from 'src/components/Divider';
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 { useRegionsQuery } from 'src/queries/regions/regions';

import { Backups } from './Backups';
import { PrivateIP } from './PrivateIP';

import type { CreateLinodeRequest } from '@linode/api-v4';

export const Addons = () => {
const regionId = useWatch<CreateLinodeRequest, 'region'>({ name: 'region' });

const { data: regions } = useRegionsQuery();

const selectedRegion = useMemo(
() => regions?.find((r) => r.id === regionId),
[regions, regionId]
);

const isEdgeRegionSelected = selectedRegion?.site_type === 'edge';

return (
<Paper>
<Stack spacing={2}>
<Typography variant="h2">Add-ons</Typography>
{isEdgeRegionSelected && (
<Notice
text="Backups and Private IP are currently not available for Edge regions."
variant="warning"
/>
)}
<Stack divider={<Divider />} spacing={2}>
<Backups />
<PrivateIP />
</Stack>
</Stack>
</Paper>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { waitFor } from '@testing-library/react';
import React from 'react';

import {
accountSettingsFactory,
profileFactory,
regionFactory,
} from 'src/factories';
import { grantsFactory } from 'src/factories/grants';
import { makeResourcePage } from 'src/mocks/serverHandlers';
import { HttpResponse, http, server } from 'src/mocks/testServer';
import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';

import { Backups } from './Backups';

import type { CreateLinodeRequest } from '@linode/api-v4';

describe('Linode Create V2 Backups Addon', () => {
it('should render a label and checkbox', () => {
const { getByLabelText } = renderWithThemeAndHookFormContext({
component: <Backups />,
});

const checkbox = getByLabelText('Backups', { exact: false });

expect(checkbox).toBeEnabled();
expect(checkbox).not.toBeChecked();
});

it('should get its value from the form context', () => {
const {
getByRole,
} = renderWithThemeAndHookFormContext<CreateLinodeRequest>({
component: <Backups />,
useFormOptions: { defaultValues: { backups_enabled: true } },
});

const checkbox = getByRole('checkbox');

expect(checkbox).toBeEnabled();
expect(checkbox).toBeChecked();
});

it('should render special copy, be checked, and be disabled if account backups are enabled', async () => {
server.use(
http.get('*/v4/account/settings', () => {
return HttpResponse.json(
accountSettingsFactory.build({ backups_enabled: true })
);
})
);

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

const checkbox = getByRole('checkbox');

await findByText('You have enabled automatic backups for your account.', {
exact: false,
});

expect(checkbox).toBeDisabled();
expect(checkbox).toBeChecked();
});

it('should be disabled if an edge region is selected', async () => {
const region = regionFactory.build({ site_type: 'edge' });

server.use(
http.get('*/v4/regions', () => {
return HttpResponse.json(makeResourcePage([region]));
})
);

const {
getByRole,
} = renderWithThemeAndHookFormContext<CreateLinodeRequest>({
component: <Backups />,
useFormOptions: { defaultValues: { region: region.id } },
});

const checkbox = getByRole('checkbox');

await waitFor(() => {
expect(checkbox).toBeDisabled();
});
});

it('should be disabled if the user does not have permission to create a linode', async () => {
server.use(
http.get('*/v4/profile', () => {
return HttpResponse.json(profileFactory.build({ restricted: true }));
}),
http.get('*/v4/profile/grants', () => {
return HttpResponse.json(
grantsFactory.build({ global: { add_linodes: false } })
);
})
);

const {
getByRole,
} = renderWithThemeAndHookFormContext<CreateLinodeRequest>({
component: <Backups />,
});

const checkbox = getByRole('checkbox');

await waitFor(() => {
expect(checkbox).toBeDisabled();
});
});
});
Loading

0 comments on commit d4e0d0a

Please sign in to comment.