From ef4637e94ecdf93c44c80d17b4b1fc72e9a52e7b Mon Sep 17 00:00:00 2001 From: carrillo-erik <119514965+carrillo-erik@users.noreply.github.com> Date: Thu, 25 Apr 2024 08:54:03 -0700 Subject: [PATCH 01/40] upcoming: [M3-7932] - Placement Groups copy updates (#10399) * upcoming: [M3-7932] - Placement Groups copy updates * Add changeset * Fix failing tests --- .../pr-10399-upcoming-features-1713888729255.md | 5 +++++ .../placement-groups-landing-page.spec.ts | 2 +- .../manager/src/components/DetailsPanel/DetailsPanel.tsx | 1 + .../Linodes/LinodeCreatev2/Details/Details.test.tsx | 4 +++- .../LinodeCreatev2/Details/PlacementGroupPanel.test.tsx | 4 +++- .../PlacementGroupsAffinityTypeSelect.tsx | 8 +++++--- .../PlacementGroupsAssignLinodesDrawer.tsx | 2 +- .../PlacementGroups/PlacementGroupsDetailPanel.test.tsx | 2 +- .../PlacementGroups/PlacementGroupsDetailPanel.tsx | 7 +++---- .../PlacementGroupsLanding.test.tsx | 2 +- .../PlacementGroupsLanding/PlacementGroupsLanding.tsx | 4 ++-- .../PlacementGroupsLandingEmptyState.tsx | 2 +- .../PlacementGroupsLandingEmptyStateData.ts | 2 +- .../manager/src/features/PlacementGroups/constants.ts | 9 ++++----- 14 files changed, 32 insertions(+), 22 deletions(-) create mode 100644 packages/manager/.changeset/pr-10399-upcoming-features-1713888729255.md diff --git a/packages/manager/.changeset/pr-10399-upcoming-features-1713888729255.md b/packages/manager/.changeset/pr-10399-upcoming-features-1713888729255.md new file mode 100644 index 00000000000..bcf107580cc --- /dev/null +++ b/packages/manager/.changeset/pr-10399-upcoming-features-1713888729255.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Placement Groups text copy updates ([#10399](https://github.com/linode/manager/pull/10399)) diff --git a/packages/manager/cypress/e2e/core/placementGroups/placement-groups-landing-page.spec.ts b/packages/manager/cypress/e2e/core/placementGroups/placement-groups-landing-page.spec.ts index 7709b82b298..74a3c4fab91 100644 --- a/packages/manager/cypress/e2e/core/placementGroups/placement-groups-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/placementGroups/placement-groups-landing-page.spec.ts @@ -39,7 +39,7 @@ describe('VM Placement landing page', () => { }); ui.button - .findByTitle('Create Placement Groups') + .findByTitle('Create Placement Group') .should('be.visible') .should('be.enabled') .click(); diff --git a/packages/manager/src/components/DetailsPanel/DetailsPanel.tsx b/packages/manager/src/components/DetailsPanel/DetailsPanel.tsx index e1e09ef6645..20dfcbb1a5c 100644 --- a/packages/manager/src/components/DetailsPanel/DetailsPanel.tsx +++ b/packages/manager/src/components/DetailsPanel/DetailsPanel.tsx @@ -58,6 +58,7 @@ export const DetailsPanel = (props: DetailsPanelProps) => { /> {tagsInputProps && } + {isPlacementGroupsEnabled && ( { await waitFor(() => { expect( - getByText('Select a region above to see available Placement Groups.') + getByText( + 'Select a Region for your Linode to see existing placement groups.' + ) ).toBeVisible(); }); }); diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Details/PlacementGroupPanel.test.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Details/PlacementGroupPanel.test.tsx index ad7ffd19860..a46491a903b 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/Details/PlacementGroupPanel.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Details/PlacementGroupPanel.test.tsx @@ -21,7 +21,9 @@ describe('PlacementGroupPanel', () => { }); expect( - getByText('Select a region above to see available Placement Groups.') + getByText( + 'Select a Region for your Linode to see existing placement groups.' + ) ).toBeVisible(); }); diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityTypeSelect.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityTypeSelect.tsx index fbc87558fc8..2c12a78b461 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityTypeSelect.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityTypeSelect.tsx @@ -74,11 +74,13 @@ export const PlacementGroupsAffinityTypeSelect = (props: Props) => { textFieldProps={{ tooltipText: ( - Linodes in a placement group that use ‘Affinity’ always exist on the - same host. This can help with performance. Linodes in a placement - group that use ‘Anti-affinity: Host’ are never on the same host. Use + Linodes in a placement group that use Affinity are physically closer + together, possibly on the same hardware. This can help with + performance. Linodes in a placement group that use Anti-affinity are + in separate fault domains, but still in the same data center. Use this to support a high-availability model.
+ {/* TODO VM_Placement: Add link path or determine if removal desired */} Learn more.
), diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.tsx index 8b34fdf99d7..592397e4807 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.tsx @@ -162,7 +162,7 @@ export const PlacementGroupsAssignLinodesDrawer = ( /> )} - A Linode can only be assigned to a single Placement Group. + A Linode can only be assigned to one placement group. { expect(getByRole('combobox')).toBeDisabled(); expect(getByTestId('notice-warning')).toHaveTextContent( - 'The selected region does not currently have Placement Group capabilities.' + 'Currently, only specific regions support placement groups.' ); expect( queryByRole('button', { name: /create placement group/i }) diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetailPanel.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetailPanel.tsx index 3e1d8fb1478..76e9b9c161a 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetailPanel.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetailPanel.tsx @@ -81,7 +81,7 @@ export const PlacementGroupsDetailPanel = (props: Props) => { variant="warning" > - Select a region above to see available Placement Groups. + Select a Region for your Linode to see existing placement groups. )} @@ -93,8 +93,7 @@ export const PlacementGroupsDetailPanel = (props: Props) => { variant="warning" > - The selected region does not currently have Placement Group - capabilities. Only these{' '} + Currently, only specific{' '} { displayText="regions" minWidth={225} />{' '} - support Placement Groups. + support placement groups. )} diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.test.tsx index 6e579a4552f..8a6f4e8d6f2 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.test.tsx @@ -89,7 +89,7 @@ describe('PlacementGroupsLanding', () => { expect( getByText( - 'Control the physical placement or distribution of virtual machines (VMs) instances within a data center or availability zone.' + 'Control the physical placement or distribution of Linode instances within a data center or availability zone.' ) ).toBeInTheDocument(); }); diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx index 3afc7fa6abb..4d28bdd8b04 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx @@ -194,9 +194,9 @@ export const PlacementGroupsLanding = React.memo(() => { }} debounceTime={250} hideLabel - label="Filter" + label="Search" onChange={(e) => setQuery(e.target.value)} - placeholder="Filter" + placeholder="Search Placement Groups" sx={{ mb: 4 }} value={query} /> diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLandingEmptyState.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLandingEmptyState.tsx index 51d5ab5902a..623719cbf02 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLandingEmptyState.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLandingEmptyState.tsx @@ -23,7 +23,7 @@ export const PlacementGroupsLandingEmptyState = ({ { sendEvent({ diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLandingEmptyStateData.ts b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLandingEmptyStateData.ts index 4b0993874c3..9555f6dbb20 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLandingEmptyStateData.ts +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLandingEmptyStateData.ts @@ -8,7 +8,7 @@ import type { export const headers: ResourcesHeaders = { description: - 'Control the physical placement or distribution of virtual machines (VMs) instances within a data center or availability zone.', + 'Control the physical placement or distribution of Linode instances within a data center or availability zone.', subtitle: '', title: PLACEMENT_GROUP_LABEL, }; diff --git a/packages/manager/src/features/PlacementGroups/constants.ts b/packages/manager/src/features/PlacementGroups/constants.ts index 7921b9d712a..96451573d93 100644 --- a/packages/manager/src/features/PlacementGroups/constants.ts +++ b/packages/manager/src/features/PlacementGroups/constants.ts @@ -7,12 +7,11 @@ export const MAX_NUMBER_OF_LINODES_IN_PLACEMENT_GROUP_MESSAGE = export const PLACEMENT_GROUP_LINODES_ERROR_MESSAGE = 'There was an error loading Linodes for this Placement Group.'; -export const PLACEMENT_GROUP_TOOLTIP_TEXT = `The Affinity Type and Region determine the maximum number of VMs per group.`; +export const PLACEMENT_GROUP_TOOLTIP_TEXT = `The Affinity Type and Region you selected determine the maximum number of Linodes per placement group.`; export const PLACEMENT_GROUP_SELECT_TOOLTIP_COPY = ` -Add your virtual machine (VM) to a group to best meet your needs. -You may want to group VMs closer together to help improve performance, or further apart to enable high-availability configurations. -Learn more.`; +Add your Linode to a group to best meet your needs. +You may want to group Linodes closer together to help improve performance, or further apart to enable high-availability configurations.`; export const PLACEMENT_GROUP_HAS_NO_CAPACITY = - 'This Placement Group does not have any capacity.'; + 'This placement group has reached the maximum Linode capacity.'; From 4da95530e3466ae2c412f87666d714bf9275a91d Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:05:36 -0400 Subject: [PATCH 02/40] upcoming: [M3-7874] - Linode Create Refactor - Marketplace - Part 1 (#10401) * initial marketplace UI * unit test * Added changeset: Linode Create Refactor - Marketplace - Part 1 * make categories dynamic - thanks @abailly-akamai --------- Co-authored-by: Banks Nussman --- ...r-10401-upcoming-features-1713899458134.md | 5 ++ .../Tabs/Marketplace/Marketplace.test.tsx | 74 +++++++++++++++++++ .../Tabs/Marketplace/Marketplace.tsx | 39 ++++++++++ .../Tabs/Marketplace/utilities.ts | 18 +++++ .../features/Linodes/LinodeCreatev2/index.tsx | 5 +- 5 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 packages/manager/.changeset/pr-10401-upcoming-features-1713899458134.md create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/Marketplace.test.tsx create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/Marketplace.tsx create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/utilities.ts diff --git a/packages/manager/.changeset/pr-10401-upcoming-features-1713899458134.md b/packages/manager/.changeset/pr-10401-upcoming-features-1713899458134.md new file mode 100644 index 00000000000..72701dda554 --- /dev/null +++ b/packages/manager/.changeset/pr-10401-upcoming-features-1713899458134.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Linode Create Refactor - Marketplace - Part 1 ([#10401](https://github.com/linode/manager/pull/10401)) diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/Marketplace.test.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/Marketplace.test.tsx new file mode 100644 index 00000000000..c743315a027 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/Marketplace.test.tsx @@ -0,0 +1,74 @@ +import { userEvent } from '@testing-library/user-event'; +import React from 'react'; + +import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; + +import { Marketplace } from './Marketplace'; +import { uniqueCategories } from './utilities'; + +describe('Marketplace', () => { + it('should render a header', () => { + const { getByText } = renderWithThemeAndHookFormContext({ + component: , + }); + + const heading = getByText('Select an App'); + + expect(heading).toBeVisible(); + expect(heading.tagName).toBe('H2'); + }); + + it('should render a search field', () => { + const { getByPlaceholderText } = renderWithThemeAndHookFormContext({ + component: , + }); + + const input = getByPlaceholderText('Search for app name'); + + expect(input).toBeVisible(); + expect(input).toBeEnabled(); + }); + + it('should render a category select', () => { + const { getByPlaceholderText } = renderWithThemeAndHookFormContext({ + component: , + }); + + const input = getByPlaceholderText('Select category'); + + expect(input).toBeVisible(); + expect(input).toBeEnabled(); + }); + + it('should allow the user to select a category', async () => { + const { + getByLabelText, + getByPlaceholderText, + getByText, + } = renderWithThemeAndHookFormContext({ + component: , + }); + + const select = getByPlaceholderText('Select category'); + + await userEvent.click(select); + + // Verify all categories are rendered in the dropdown list + for (const category of uniqueCategories) { + expect(getByText(category)).toBeVisible(); + } + + // Select a category + await userEvent.click(getByText('Databases')); + + // Verify value updates + expect(select).toHaveDisplayValue('Databases'); + + // Verify the category can be cleared + const clearButton = getByLabelText('Clear'); + + await userEvent.click(clearButton); + + expect(select).toHaveDisplayValue(''); + }); +}); diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/Marketplace.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/Marketplace.tsx new file mode 100644 index 00000000000..d00fadbedbe --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/Marketplace.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; +import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; +import { Paper } from 'src/components/Paper'; +import { Stack } from 'src/components/Stack'; +import { Typography } from 'src/components/Typography'; + +import { categoryOptions } from './utilities'; + +export const Marketplace = () => { + return ( + + + Select an App + + + + + + + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/utilities.ts new file mode 100644 index 00000000000..2845ee7a3f4 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/utilities.ts @@ -0,0 +1,18 @@ +import { oneClickApps } from 'src/features/OneClickApps/oneClickApps'; + +/** + * Get all categories from our marketplace apps list so + * we can generate a dynamic list of category options. + */ +const categories = oneClickApps.reduce((acc, app) => { + return [...acc, ...app.categories]; +}, []); + +export const uniqueCategories = Array.from(new Set(categories)); + +/** + * A list of unique categories to be shown in a Select/Autocomplete + */ +export const categoryOptions = uniqueCategories.map((category) => ({ + label: category, +})); diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx index a0b609a9237..edb58f92923 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx @@ -23,6 +23,7 @@ import { Region } from './Region'; import { Summary } from './Summary'; import { Distributions } from './Tabs/Distributions'; import { Images } from './Tabs/Images'; +import { Marketplace } from './Tabs/Marketplace/Marketplace'; import { StackScripts } from './Tabs/StackScripts/StackScripts'; import { UserData } from './UserData/UserData'; import { @@ -105,7 +106,9 @@ export const LinodeCreatev2 = () => { - Marketplace + + + From 1534a9c294d4b0bdcd0782b860d4fffe78868078 Mon Sep 17 00:00:00 2001 From: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Date: Fri, 26 Apr 2024 09:24:50 -0400 Subject: [PATCH 03/40] test: [various] - Cypress test cleanup and flake fixes (#10405) * Fix VPC subnet Linode assignment Cypress test flake * Fix OBJ E2E test flake by waiting for data to load before interacting * Fix OBJ access key test issue resulting in failures on accounts with many buckets * Refactor Linode config tests to be more resilient to kernel updates * Clean up power state tests * Mark old utils as deprecated and document preferred alternatives * Add changesets --- .../pr-10405-tests-1714056849877.md | 5 + .../pr-10405-tests-1714056884905.md | 5 + .../pr-10405-tests-1714056931602.md | 5 + .../pr-10405-tests-1714056955923.md | 5 + .../e2e/core/linodes/linode-config.spec.ts | 946 ++++++++++-------- .../core/linodes/switch-linode-state.spec.ts | 86 +- .../objectStorage/access-keys.smoke.spec.ts | 1 + .../objectStorage/object-storage.e2e.spec.ts | 8 +- .../e2e/core/vpc/vpc-linodes-update.spec.ts | 1 + .../manager/cypress/support/api/linodes.ts | 5 + packages/manager/cypress/support/helpers.ts | 45 + .../cypress/support/intercepts/linodes.ts | 38 +- .../manager/cypress/support/util/kernels.ts | 38 + .../manager/cypress/support/util/linodes.ts | 182 ++-- packages/manager/src/factories/index.ts | 1 + 15 files changed, 854 insertions(+), 517 deletions(-) create mode 100644 packages/manager/.changeset/pr-10405-tests-1714056849877.md create mode 100644 packages/manager/.changeset/pr-10405-tests-1714056884905.md create mode 100644 packages/manager/.changeset/pr-10405-tests-1714056931602.md create mode 100644 packages/manager/.changeset/pr-10405-tests-1714056955923.md create mode 100644 packages/manager/cypress/support/util/kernels.ts diff --git a/packages/manager/.changeset/pr-10405-tests-1714056849877.md b/packages/manager/.changeset/pr-10405-tests-1714056849877.md new file mode 100644 index 00000000000..2d81c4a2847 --- /dev/null +++ b/packages/manager/.changeset/pr-10405-tests-1714056849877.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Fix VPC subnet Linode assignment integration test failures ([#10405](https://github.com/linode/manager/pull/10405)) diff --git a/packages/manager/.changeset/pr-10405-tests-1714056884905.md b/packages/manager/.changeset/pr-10405-tests-1714056884905.md new file mode 100644 index 00000000000..12920d7c990 --- /dev/null +++ b/packages/manager/.changeset/pr-10405-tests-1714056884905.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Fix access key test failure when user has many OBJ buckets ([#10405](https://github.com/linode/manager/pull/10405)) diff --git a/packages/manager/.changeset/pr-10405-tests-1714056931602.md b/packages/manager/.changeset/pr-10405-tests-1714056931602.md new file mode 100644 index 00000000000..be0af4995a6 --- /dev/null +++ b/packages/manager/.changeset/pr-10405-tests-1714056931602.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Mitigate OBJ end-to-end test failure related to ACL select issue ([#10405](https://github.com/linode/manager/pull/10405)) diff --git a/packages/manager/.changeset/pr-10405-tests-1714056955923.md b/packages/manager/.changeset/pr-10405-tests-1714056955923.md new file mode 100644 index 00000000000..f124aad56cc --- /dev/null +++ b/packages/manager/.changeset/pr-10405-tests-1714056955923.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Refactor Linode config end-to-end tests ([#10405](https://github.com/linode/manager/pull/10405)) diff --git a/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts index 141495ccfdf..51b32e4ca54 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts @@ -1,13 +1,7 @@ -import { createLinode } from 'support/api/linodes'; -import { containsVisible } from 'support/helpers'; +import { createTestLinode } from 'support/util/linodes'; import { ui } from 'support/ui'; import { authenticate } from 'support/api/authentication'; import { cleanUp } from 'support/util/cleanup'; -import { - mockAppendFeatureFlags, - mockGetFeatureFlagClientstream, -} from 'support/intercepts/feature-flags'; -import { makeFeatureFlagData } from 'support/util/feature-flags'; import { mockGetVPC } from 'support/intercepts/vpc'; import { dcPricingMockLinodeTypes } from 'support/constants/dc-specific-pricing'; import { getRegionById } from 'support/util/regions'; @@ -16,9 +10,12 @@ import { interceptRebootLinode, mockGetLinodeDetails, mockGetLinodeDisks, + mockGetLinodeKernels, mockGetLinodeVolumes, + mockGetLinodeKernel, } from 'support/intercepts/linodes'; import { + interceptGetLinodeConfigs, interceptDeleteLinodeConfig, interceptCreateLinodeConfigs, interceptUpdateLinodeConfigs, @@ -26,519 +23,618 @@ import { mockCreateLinodeConfigs, mockUpdateLinodeConfigs, } from 'support/intercepts/configs'; +import { fetchLinodeConfigs } from 'support/util/linodes'; import { - createLinodeAndGetConfig, - createAndBootLinode, -} from 'support/util/linodes'; -import { + kernelFactory, vpcFactory, linodeFactory, linodeConfigFactory, VLANFactory, + LinodeConfigInterfaceFactory, + LinodeConfigInterfaceFactoryWithVPC, } from '@src/factories'; import { randomNumber, randomLabel } from 'support/util/random'; +import { fetchAllKernels, findKernelById } from 'support/util/kernels'; +import { NOT_NATTED_HELPER_TEXT } from 'src/features/VPCs/constants'; + +import type { CreateTestLinodeOptions } from 'support/util/linodes'; +import type { + Config, + CreateLinodeRequest, + InterfacePurpose, + Linode, + VLAN, + Region, + Kernel, +} from '@linode/api-v4'; + +/** + * Returns a Promise that resolves to a new test Linode and its first config object. + * + * @param interfaces - Interfaces with which to create test Linode. + * + * @throws If created Linode does not have any configs. + */ +const createLinodeAndGetConfig = async ( + payload?: Partial | null, + options?: Partial +) => { + const linode = await createTestLinode(payload, options); + + const config = (await fetchLinodeConfigs(linode.id))[0]; + if (!config) { + throw new Error( + `Linode '${linode.label}' (ID ${linode.id}) does not have any configs` + ); + } -import type { Config, Linode, VLAN, Disk, Region } from '@linode/api-v4'; + return [linode, config]; +}; +let kernels: Kernel[] = []; authenticate(); +describe('Linode Config management', () => { + describe('End-to-End', () => { + before(() => { + cleanUp('linodes'); + + // Fetch Linode kernel data from the API. + // We'll use this data in the tests to confirm that config labels are rendered correctly. + cy.defer(fetchAllKernels(), 'Fetching Linode kernels...').then( + (fetchedKernels) => { + kernels = fetchedKernels; + } + ); + }); -describe('Linode Config', () => { - const region: Region = getRegionById('us-southeast'); - const diskLabel: string = 'Debian 10 Disk'; - const mockConfig: Config = linodeConfigFactory.build({ - id: randomNumber(), - }); - const mockDisks: Disk[] = [ - { - id: 44311273, - status: 'ready', - label: diskLabel, - created: '2020-08-21T17:26:14', - updated: '2020-08-21T17:26:30', - filesystem: 'ext4', - size: 81408, - }, - { - id: 44311274, - status: 'ready', - label: '512 MB Swap Image', - created: '2020-08-21T17:26:14', - updated: '2020-08-21T17:26:31', - filesystem: 'swap', - size: 512, - }, - ]; - const mockVLANs: VLAN[] = VLANFactory.buildList(2); - - before(() => { - mockConfig.interfaces.splice(2, 1); - }); - - beforeEach(() => { - cleanUp(['linodes']); - }); - - it('Creates a new config and list all configs', () => { - createLinode().then((linode: Linode) => { - interceptCreateLinodeConfigs(linode.id).as('postLinodeConfigs'); - - cy.visitWithLogin(`/linodes/${linode.id}/configurations`); + /* + * - Tests Linode config creation end-to-end using real API requests. + * - Confirms that a config is listed after a Linode has been created. + * - Confirms that config creation can be initiated and completed successfully. + * - Confirms that new config is automatically listed after being created. + */ + it('Creates a config', () => { + // Wait for Linode to be created for kernel data to be retrieved. + cy.defer(createTestLinode(), 'Creating Linode').then((linode: Linode) => { + interceptCreateLinodeConfigs(linode.id).as('postLinodeConfigs'); + interceptGetLinodeConfigs(linode.id).as('getLinodeConfigs'); - cy.findByLabelText('List of Configurations').within(() => { - containsVisible('My Debian 10 Disk Profile – GRUB 2'); - }); - containsVisible('My Debian 10 Disk Profile – GRUB 2'); + cy.visitWithLogin(`/linodes/${linode.id}/configurations`); - cy.findByText('Add Configuration').click(); - ui.dialog - .findByTitle('Add Configuration') - .should('be.visible') - .within(() => { - cy.get('#label').type(`${linode.id}-test-config`); - ui.buttonGroup - .findButtonByTitle('Add Configuration') - .scrollIntoView() - .should('be.visible') - .should('be.enabled') - .click(); + // Confirm that initial config is listed in Linode configurations table. + cy.wait('@getLinodeConfigs'); + cy.defer(fetchLinodeConfigs(linode.id)).then((configs: Config[]) => { + cy.findByLabelText('List of Configurations').within(() => { + configs.forEach((config) => { + const kernel = findKernelById(kernels, config.kernel); + cy.findByText(`${config.label} – ${kernel.label}`).should( + 'be.visible' + ); + }); + }); }); - cy.wait('@postLinodeConfigs') - .its('response.statusCode') - .should('eq', 200); + // Add new configuration. + cy.findByText('Add Configuration').click(); + ui.dialog + .findByTitle('Add Configuration') + .should('be.visible') + .within(() => { + cy.get('#label').type(`${linode.id}-test-config`); + ui.buttonGroup + .findButtonByTitle('Add Configuration') + .scrollIntoView() + .should('be.visible') + .should('be.enabled') + .click(); + }); - cy.findByLabelText('List of Configurations').within(() => { - cy.get('tr').should('have.length', 2); - containsVisible( - `${linode.id}-test-config – Latest 64 bit (6.7.9-x86_64-linode163)` - ); - containsVisible('eth0 – Public Internet'); + // Confirm that config creation request was successful. + cy.wait('@postLinodeConfigs') + .its('response.statusCode') + .should('eq', 200); + + // Confirm that new config and existing config are both listed. + cy.wait('@getLinodeConfigs'); + cy.defer(fetchLinodeConfigs(linode.id)).then((configs: Config[]) => { + cy.findByLabelText('List of Configurations').within(() => { + configs.forEach((config) => { + const kernel = findKernelById(kernels, config.kernel); + cy.findByText(`${config.label} – ${kernel.label}`) + .should('be.visible') + .closest('tr') + .within(() => { + cy.findByText('eth0 – Public Internet').should('be.visible'); + }); + }); + }); + }); }); }); - }); - it('Creates a new config and assigns a VPC as a network interface', () => { - const mockLinode = linodeFactory.build({ - region: region.id, - type: dcPricingMockLinodeTypes[0].id, - }); + /** + * - Tests Linode config edit flow end-to-end using real API requests. + * - Confirms that an existing config can be edited. + * - Confirms that updated config data is automatically displayed after editing. + */ + it('Edits a config', () => { + // Config interfaces to use when creating test Linode. + const interfaces = [ + { + ipam_address: '', + label: '', + purpose: 'public' as InterfacePurpose, + }, + { + ipam_address: '', + label: 'testvlan', + purpose: 'vlan' as InterfacePurpose, + }, + ]; - const mockVPC = vpcFactory.build({ - id: 1, - label: randomLabel(), - }); + // Create a Linode and wait for its Config to be fetched before proceeding. + cy.defer( + createLinodeAndGetConfig({ interfaces }, { waitForDisks: true }), + 'creating a linode and getting its config' + ).then(([linode, config]: [Linode, Config]) => { + // Get kernel info for config. + const kernel = findKernelById(kernels, config.kernel); + const newIpamAddress = '192.0.2.0/25'; - mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); - mockAppendFeatureFlags({ - vpc: makeFeatureFlagData(true), - }).as('getFeatureFlags'); - mockGetFeatureFlagClientstream().as('getClientStream'); - - mockGetLinodeDisks(mockLinode.id, mockDisks).as('getDisks'); - mockGetLinodeConfigs(mockLinode.id, []); - mockGetVPC(mockVPC).as('getVPC'); - mockGetLinodeVolumes(mockLinode.id, []).as('getVolumes'); - - cy.visitWithLogin(`/linodes/${mockLinode.id}/configurations`); - cy.wait([ - '@getClientStream', - '@getFeatureFlags', - '@getLinode', - '@getDisks', - '@getVolumes', - ]); - - // Confirm that there is no configuration yet. - cy.findByLabelText('List of Configurations').within(() => { - cy.contains(`${mockConfig.label} – GRUB 2`).should('not.exist'); - }); + cy.visitWithLogin(`/linodes/${linode.id}/configurations`); + interceptUpdateLinodeConfigs(linode.id, config.id).as( + 'putLinodeConfigs' + ); - mockGetVLANs(mockVLANs); - mockGetVPC(mockVPC).as('getVPC'); - mockCreateLinodeConfigs(mockLinode.id, mockConfig).as('createLinodeConfig'); - mockGetLinodeConfigs(mockLinode.id, [mockConfig]).as('getLinodeConfigs'); - cy.findByText('Add Configuration').click(); - ui.dialog - .findByTitle('Add Configuration') - .should('be.visible') - .within(() => { - cy.get('#label').type(`${mockConfig.label}`); - // Confirm that "VPC" can be selected for either "eth0", "eth1", or "eth2". - // Add VPC to eth0 - cy.get('[data-qa-textfield-label="eth0"]') - .scrollIntoView() - .parent() - .parent() - .within(() => { - ui.select - .findByText('Public Internet') - .should('be.visible') - .click() - .type('VPC{enter}'); - }); - // Add VPC to eth1 - cy.get('[data-qa-textfield-label="eth1"]') - .scrollIntoView() - .parent() - .parent() - .within(() => { - ui.select - .findByText('None') - .should('be.visible') - .click() - .type('VPC{enter}'); - }); - // Add VPC to eth2 - cy.get('[data-qa-textfield-label="eth2"]') - .scrollIntoView() - .parent() - .parent() + // Confirm that config is listed as expected, then click "Edit". + cy.contains(`${config.label} – ${kernel.label}`).should('be.visible'); + cy.findByText('Edit').click(); + + // Enter a new IPAM address for eth1 (VLAN), then click "Save Changes" + ui.dialog + .findByTitle('Edit Configuration') + .should('be.visible') .within(() => { - ui.select - .findByText('None') + cy.get('#ipam-input-1').type(newIpamAddress); + ui.button + .findByTitle('Save Changes') + .scrollIntoView() .should('be.visible') - .click() - .type('VPC{enter}'); + .should('be.enabled') + .click(); }); - ui.buttonGroup - .findButtonByTitle('Add Configuration') - .scrollIntoView() - .should('be.visible') - .should('be.enabled') - .click(); - }); - cy.wait(['@createLinodeConfig', '@getLinodeConfigs', '@getVPC']); - - // Confirm that VLAN and VPC have been assigned. - cy.findByLabelText('List of Configurations').within(() => { - cy.get('tr').should('have.length', 2); - containsVisible(`${mockConfig.label} – GRUB 2`); - containsVisible('eth0 – Public Internet'); - containsVisible(`eth2 – VPC: ${mockVPC.label}`); - }); - }); - it('Edits an existing config', () => { - cy.defer( - createLinodeAndGetConfig({ - waitForLinodeToBeRunning: false, - linodeConfigRequestOverride: { - label: 'cy-test-edit-config-linode', - interfaces: [ - { - ipam_address: '', - label: '', - purpose: 'public', - }, - { - ipam_address: '', - label: 'testvlan', - purpose: 'vlan', - }, - ], - region: 'us-east', - }, - }), - 'creating a linode and getting its config' - ).then(([linode, config]: [Linode, Config]) => { - cy.visitWithLogin(`/linodes/${linode.id}/configurations`); - interceptUpdateLinodeConfigs(linode.id, config.id).as('putLinodeConfigs'); - - containsVisible('My Debian 10 Disk Profile – GRUB 2'); - cy.findByText('Edit').click(); + // Confirm that config update request succeeded and that toast appears. + cy.wait('@putLinodeConfigs') + .its('response.statusCode') + .should('eq', 200); + ui.toast.assertMessage( + `Configuration ${config.label} successfully updated` + ); - ui.dialog - .findByTitle('Edit Configuration') - .should('be.visible') - .within(() => { - cy.get('#ipam-input-1').type('192.0.2.0/25'); - ui.button - .findByTitle('Save Changes') - .scrollIntoView() + // Confirm that updated IPAM is automatically listed in config table. + cy.findByLabelText('List of Configurations').within(() => { + const configKernel = findKernelById(kernels, config.kernel); + cy.findByText(`${config.label} – ${configKernel.label}`) .should('be.visible') - .should('be.enabled') - .click(); + .closest('tr') + .within(() => { + cy.contains('eth0 – Public Internet').should('be.visible'); + cy.contains(`eth1 – VLAN: testvlan (${newIpamAddress})`).should( + 'be.visible' + ); + }); }); - - cy.wait('@putLinodeConfigs').its('response.statusCode').should('eq', 200); - - cy.findByLabelText('List of Configurations').within(() => { - containsVisible('eth0 – Public Internet'); - containsVisible('eth1 – VLAN: testvlan (192.0.2.0/25)'); }); }); - }); - it('Edits an existing config and assigns a VPC as a network interface', () => { - const mockLinode = linodeFactory.build({ - region: region.id, - type: dcPricingMockLinodeTypes[0].id, - }); - - const mockVPC = vpcFactory.build({ - id: 1, - label: randomLabel(), - }); + /* + * - Confirms Linode config boot flow end-to-end using real API requests. + * - Confirms that API reboot request succeeds and Cloud UI automatically updates to reflect reboot. + */ + it('Boots a config', () => { + cy.defer( + createLinodeAndGetConfig(null, { waitForBoot: true }), + 'Creating and booting test Linode' + ).then(([linode, config]: [Linode, Config]) => { + const kernel = findKernelById(kernels, config.kernel); - mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); - mockAppendFeatureFlags({ - vpc: makeFeatureFlagData(true), - }).as('getFeatureFlags'); - mockGetFeatureFlagClientstream().as('getClientStream'); - - mockGetLinodeDisks(mockLinode.id, mockDisks).as('getDisks'); - mockGetLinodeConfigs(mockLinode.id, [mockConfig]).as('getConfig'); - mockGetVPC(mockVPC).as('getVPC'); - mockGetLinodeVolumes(mockLinode.id, []).as('getVolumes'); - - cy.visitWithLogin(`/linodes/${mockLinode.id}/configurations`); - cy.wait([ - '@getClientStream', - '@getFeatureFlags', - '@getLinode', - '@getConfig', - '@getDisks', - '@getVolumes', - ]); - - cy.findByLabelText('List of Configurations').within(() => { - containsVisible(`${mockConfig.label} – GRUB 2`); - }); - cy.findByText('Edit').click(); + cy.visitWithLogin(`/linodes/${linode.id}/configurations`); + interceptRebootLinode(linode.id).as('rebootLinode'); - mockGetVLANs(mockVLANs); - mockGetVPC(mockVPC).as('getVPC'); - mockUpdateLinodeConfigs(mockLinode.id, mockConfig).as( - 'updateLinodeConfigs' - ); - mockGetLinodeConfigs(mockLinode.id, [mockConfig]).as('getLinodeConfigs'); - ui.dialog - .findByTitle('Edit Configuration') - .should('be.visible') - .within(() => { - // Change eth0 to VPC - cy.get('[data-qa-textfield-label="eth0"]') - .scrollIntoView() - .parent() - .parent() - .within(() => { - ui.select - .findByText('Public Internet') - .should('be.visible') - .click() - .type('VPC{enter}'); - }); - // Change eth1 to VPC - cy.get('[data-qa-textfield-label="eth1"]') - .scrollIntoView() - .parent() - .parent() + // Confirm that Linode config is listed, then click its "Boot" button. + cy.findByText(`${config.label} – ${kernel.label}`) + .should('be.visible') + .closest('tr') .within(() => { - ui.select - .findByText('VLAN') - .should('be.visible') - .click() - .type('VPC{enter}'); + cy.findByText('Boot').click(); }); - // Change eth2 to VPC - cy.get('[data-qa-textfield-label="eth2"]') - .scrollIntoView() - .parent() - .parent() + + // Proceed through boot confirmation dialog. + ui.dialog + .findByTitle('Confirm Boot') + .should('be.visible') .within(() => { - ui.select - .findByText('VPC') + cy.contains( + `Are you sure you want to boot "${config.label}"?` + ).should('be.visible'); + ui.button + .findByTitle('Boot') .should('be.visible') - .click() - .type('VPC{enter}'); + .should('be.enabled') + .click(); }); + + // Confirm that API request succeeds, toast appears, and UI updates to reflect reboot. + cy.wait('@rebootLinode').its('response.statusCode').should('eq', 200); + ui.toast.assertMessage(`Successfully booted config ${config.label}`); + cy.findByText('REBOOTING').should('be.visible'); + }); + }); + + /* + * - Confirms Linode config clone flow end-to-end using real API requests. + * - Confirms that API config clone requests succeed. + * - Confirms that Cloud UI automatically updates to reflect clone-in-progress. + */ + it('Clones a config', () => { + // Create clone source and destination Linodes. + const createCloneTestLinodes = async () => { + return Promise.all([ + createTestLinode(null, { waitForBoot: true }), + createTestLinode(), + ]); + }; + + // Create clone and source destination Linodes, then proceed with clone flow. + cy.defer( + createCloneTestLinodes(), + 'Waiting for 2 Linodes to be created' + ).then(([sourceLinode, destLinode]: [Linode, Linode]) => { + const kernel = findKernelById(kernels, 'linode/latest-64bit'); + const sharedConfigLabel = 'cy-test-sharable-config'; + + cy.visitWithLogin(`/linodes/${sourceLinode.id}/configurations`); + + // Add a new configuration that we can share across our Linodes. ui.button - .findByTitle('Save Changes') - .scrollIntoView() - .should('be.visible') + .findByTitle('Add Configuration') .should('be.enabled') + .should('be.visible') .click(); - }); - cy.wait(['@updateLinodeConfigs', '@getLinodeConfigs', '@getVPC']); - - // Confirm that VLAN and VPC have been assigned. - cy.findByLabelText('List of Configurations').within(() => { - cy.get('tr').should('have.length', 2); - containsVisible(`${mockConfig.label} – GRUB 2`); - containsVisible('eth0 – Public Internet'); - containsVisible(`eth2 – VPC: ${mockVPC.label}`); - }); - }); - it('Boots an existing config', () => { - cy.defer(createAndBootLinode()).then((linode: Linode) => { - cy.visitWithLogin(`/linodes/${linode.id}/configurations`); - interceptRebootLinode(linode.id).as('rebootLinode'); + ui.dialog + .findByTitle('Add Configuration') + .should('be.visible') + .within(() => { + cy.findByLabelText('Label', { exact: false }) + .should('be.visible') + .type(sharedConfigLabel); + + cy.findByText('Select a Kernel') + .scrollIntoView() + .click() + .type('Latest 64 bit{enter}'); - containsVisible('My Debian 10 Disk Profile – GRUB 2'); - cy.findByText('Boot').click(); + ui.buttonGroup + .findButtonByTitle('Add Configuration') + .scrollIntoView() + .should('be.visible') + .should('be.enabled') + .click(); + }); - ui.dialog - .findByTitle('Confirm Boot') - .should('be.visible') - .within(() => { - containsVisible( - 'Are you sure you want to boot "My Debian 10 Disk Profile"?' - ); - ui.button - .findByTitle('Boot') + // Confirm that new configuration is listed in table. + cy.findByLabelText('List of Configurations').within(() => { + cy.findByText(`${sharedConfigLabel} – ${kernel.label}`) .should('be.visible') - .should('be.enabled') - .click(); + .closest('tr') + .within(() => { + cy.findByText('eth0 – Public Internet').should('be.visible'); + }); }); - cy.wait('@rebootLinode').its('response.statusCode').should('eq', 200); - - ui.toast.assertMessage( - 'Successfully booted config My Debian 10 Disk Profile' - ); - cy.findByText('REBOOTING').should('be.visible'); - }); - }); + // Initiate configuration clone flow. + ui.actionMenu + .findByTitle(`Action menu for Linode Config ${sharedConfigLabel}`) + .should('be.visible') + .click(); - it('Clones an existing config', () => { - // Create a destination Linode to clone to - // And delete the default config - createLinode({ - label: 'cy-test-clone-destination-linode', - }).then((linode: Linode) => { - cy.visitWithLogin(`/linodes/${linode.id}/configurations`); + ui.actionMenuItem.findByTitle('Clone').should('be.visible').click(); - ui.actionMenu - .findByTitle('Action menu for Linode Config My Debian 10 Disk Profile') - .should('be.visible') - .click(); - ui.actionMenuItem.findByTitle('Delete').should('be.visible').click(); + cy.findByTestId('config-clone-selection-details') + .should('be.visible') + .within(() => { + ui.button.findByTitle('Clone').should('be.disabled'); + cy.findByLabelText('Linode').should('be.visible').click(); - ui.dialog - .findByTitle('Confirm Delete') - .should('be.visible') - .within(() => { - ui.button - .findByTitle('Delete') - .scrollIntoView() - .should('be.visible') - .should('be.enabled') - .click(); - }); + ui.select.findItemByText(destLinode.label).click(); + ui.button.findByTitle('Clone').should('be.enabled').click(); + }); - ui.toast.assertMessage( - 'Configuration My Debian 10 Disk Profile successfully deleted' - ); - cy.findByLabelText('List of Configurations').within(() => { - containsVisible('No data to display.'); + // Confirm toast message and that UI updates to reflect clone in progress. + ui.toast.assertMessage( + `Linode ${sourceLinode.label} successfully cloned to ${destLinode.label}.` + ); + cy.findByText(/CLONING \(\d+%\)/).should('be.visible'); }); + }); - // Create a source Linode to clone from + /* + * - Confirms Linode config delete flow end-to-end using real API requests. + * - Confirms that config can be deleted and related API requests succeed. + * - Confirms that Cloud Manager UI automatically updates to reflect deleted config. + */ + it('Deletes a config', () => { cy.defer( - createLinodeAndGetConfig({ - waitForLinodeToBeRunning: true, - linodeConfigRequestOverride: { - label: 'cy-test-clone-origin-linode', - }, - }), + createLinodeAndGetConfig(), 'creating a linode and getting its config' ).then(([linode, config]: [Linode, Config]) => { + // Get kernel info for config to be deleted. + const kernel = findKernelById(kernels, config.kernel); + interceptDeleteLinodeConfig(linode.id, config.id).as( 'deleteLinodeConfig' ); cy.visitWithLogin(`/linodes/${linode.id}/configurations`); - // Add a sharable config to the source Linode - cy.findByText('Add Configuration').click(); + // Confirm that config is listed and initiate deletion. + cy.findByText(`${config.label} – ${kernel.label}`).should('be.visible'); + ui.actionMenu + .findByTitle(`Action menu for Linode Config ${config.label}`) + .should('be.visible') + .click(); + + ui.actionMenuItem.findByTitle('Delete').should('be.visible').click(); + + // Confirm config deletion. ui.dialog - .findByTitle('Add Configuration') + .findByTitle('Confirm Delete') .should('be.visible') .within(() => { - cy.get('#label').type(`sharable-configuration`); - ui.buttonGroup - .findButtonByTitle('Add Configuration') + ui.button + .findByTitle('Delete') .scrollIntoView() .should('be.visible') .should('be.enabled') .click(); }); + // Confirm request succeeds, toast appears, and config is removed from list. + cy.wait('@deleteLinodeConfig') + .its('response.statusCode') + .should('eq', 200); + + ui.toast.assertMessage( + `Configuration ${config.label} successfully deleted` + ); + cy.findByLabelText('List of Configurations').within(() => { - cy.get('tr').should('have.length', 2); - containsVisible( - `sharable-configuration – Latest 64 bit (6.7.9-x86_64-linode163)` - ); - containsVisible('eth0 – Public Internet'); + cy.contains('No data to display.').should('be.visible'); }); + }); + }); + }); - // Clone the thing - ui.actionMenu - .findByTitle('Action menu for Linode Config sharable-configuration') - .should('be.visible') - .click(); - ui.actionMenuItem.findByTitle('Clone').should('be.visible').click(); + describe('Mocked', () => { + const region: Region = getRegionById('us-southeast'); + const mockKernel = kernelFactory.build(); + const mockVPC = vpcFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); - cy.findByTestId('config-clone-selection-details') - .should('be.visible') - .within(() => { - ui.button.findByTitle('Clone').should('be.disabled'); - cy.findByRole('combobox').should('be.visible').click(); - ui.select - .findItemByText('cy-test-clone-destination-linode') - .click(); - ui.button.findByTitle('Clone').should('be.enabled').click(); + // Mock config with public internet for eth0 and VLAN for eth1. + const mockConfig: Config = linodeConfigFactory.build({ + id: randomNumber(), + label: randomLabel(), + kernel: mockKernel.id, + interfaces: [ + LinodeConfigInterfaceFactory.build({ + ipam_address: null, + purpose: 'public', + label: null, + }), + LinodeConfigInterfaceFactory.build({ + label: randomLabel(), + purpose: 'vlan', + }), + ], + }); + + const mockVLANs: VLAN[] = VLANFactory.buildList(2); + + /* + * - Tests Linode config create and VPC interface assignment UI flows using mock API data. + * - Confirms that VPC can be assigned as eth0, eth1, and eth2. + * - Confirms public internet access/NAT helper text appears when VPC is set as eth0. + * - Confirms that "REBOOT NEEDED" status indicator appears upon creating VPC config. + */ + it('Creates a new config and assigns a VPC as a network interface', () => { + const mockLinode = linodeFactory.build({ + region: region.id, + type: dcPricingMockLinodeTypes[0].id, + }); + + // Mock config with VPC for eth0 and no other interfaces. + const mockConfigWithVpc: Config = { + ...mockConfig, + interfaces: [ + LinodeConfigInterfaceFactoryWithVPC.build({ + vpc_id: mockVPC.id, + active: false, + label: null, + }), + ], + }; + + // Mock a Linode with no existing configs, then visit its details page. + mockGetLinodeKernel(mockKernel.id, mockKernel); + mockGetLinodeKernels([mockKernel]); + mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); + mockGetLinodeDisks(mockLinode.id, []).as('getDisks'); + mockGetLinodeVolumes(mockLinode.id, []).as('getVolumes'); + mockGetLinodeConfigs(mockLinode.id, []).as('getConfigs'); + mockGetVPC(mockVPC).as('getVPC'); + mockGetVLANs(mockVLANs); + + cy.visitWithLogin(`/linodes/${mockLinode.id}/configurations`); + cy.wait(['@getConfigs', '@getDisks', '@getLinode', '@getVolumes']); + + // Confirm that there are no configurations displayed. + cy.findByLabelText('List of Configurations').within(() => { + cy.findByText('No data to display.').should('be.visible'); + }); + + // Mock requests to create new config and re-fetch configs. + mockCreateLinodeConfigs(mockLinode.id, mockConfigWithVpc).as( + 'createLinodeConfig' + ); + mockGetLinodeConfigs(mockLinode.id, [mockConfigWithVpc]).as( + 'getLinodeConfigs' + ); + + // Create new config. + cy.findByText('Add Configuration').click(); + ui.dialog + .findByTitle('Add Configuration') + .should('be.visible') + .within(() => { + cy.get('#label').type(`${mockConfigWithVpc.label}`); + + // Confirm that "VPC" can be selected for either "eth0", "eth1", or "eth2". + // Add VPC to eth0 + cy.get('[data-qa-textfield-label="eth0"]') + .scrollIntoView() + .click() + .type('VPC'); + + ui.select.findItemByText('VPC').should('be.visible').click(); + + // Confirm that internet access warning is displayed when eth0 is set + // to VPC. + cy.findByText(NOT_NATTED_HELPER_TEXT).should('be.visible'); + + // Confirm that VPC is an option for eth1 and eth2, but don't select them. + ['eth1', 'eth2'].forEach((interfaceName) => { + cy.get(`[data-qa-textfield-label="${interfaceName}"]`) + .scrollIntoView() + .click() + .type('VPC'); + + ui.select.findItemByText('VPC').should('be.visible'); + + cy.get(`[data-qa-textfield-label="${interfaceName}"]`).click(); }); - ui.toast.assertMessage( - 'Linode cy-test-clone-origin-linode successfully cloned to cy-test-clone-destination-linode.' + ui.buttonGroup + .findButtonByTitle('Add Configuration') + .scrollIntoView() + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait(['@createLinodeConfig', '@getLinodeConfigs', '@getVPC']); + + // Confirm that VPC has been assigned to eth0, and that "REBOOT NEEDED" + // status message is shown. + cy.findByLabelText('List of Configurations').within(() => { + cy.contains(`${mockConfig.label} – ${mockKernel.label}`).should( + 'be.visible' ); + cy.contains(`eth0 – VPC: ${mockVPC.label}`).should('be.visible'); }); + + cy.findByText('REBOOT NEEDED').should('be.visible'); }); - }); - it('Deletes an existing config', () => { - cy.defer( - createLinodeAndGetConfig({ - linodeConfigRequestOverride: { - label: 'cy-test-delete-config-linode', - }, - }), - 'creating a linode and getting its config' - ).then(([linode, config]: [Linode, Config]) => { - interceptDeleteLinodeConfig(linode.id, config.id).as( - 'deleteLinodeConfig' - ); - cy.visitWithLogin(`/linodes/${linode.id}/configurations`); + /* + * - Tests Linode config edit and VPC interface assignment UI flows using mock API data. + * - Confirms that VPC can be assigned as eth2 in addition to existing interfaces. + * - Confirms that "REBOOT NEEDED" status indicator appears upon creating VPC config. + */ + it('Edits an existing config and assigns a VPC as a network interface', () => { + const mockLinode = linodeFactory.build({ + region: region.id, + type: dcPricingMockLinodeTypes[0].id, + }); - containsVisible('My Debian 10 Disk Profile – GRUB 2'); - ui.actionMenu - .findByTitle('Action menu for Linode Config My Debian 10 Disk Profile') - .should('be.visible') - .click(); - ui.actionMenuItem.findByTitle('Delete').should('be.visible').click(); + // Mock config with public internet eth0, VLAN eth1, and VPC eth2. + const mockConfigWithVpc: Config = { + ...mockConfig, + interfaces: [ + ...mockConfig.interfaces, + LinodeConfigInterfaceFactoryWithVPC.build({ + label: undefined, + vpc_id: mockVPC.id, + active: false, + }), + ], + }; + + mockGetLinodeKernel(mockKernel.id, mockKernel); + mockGetLinodeKernels([mockKernel]); + mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); + + mockGetLinodeDisks(mockLinode.id, []).as('getDisks'); + mockGetLinodeConfigs(mockLinode.id, [mockConfig]).as('getConfig'); + mockGetVPC(mockVPC).as('getVPC'); + mockGetLinodeVolumes(mockLinode.id, []).as('getVolumes'); + + cy.visitWithLogin(`/linodes/${mockLinode.id}/configurations`); + cy.wait(['@getLinode', '@getConfig', '@getDisks', '@getVolumes']); + + // Find configuration in list and click its "Edit" button. + cy.findByLabelText('List of Configurations').within(() => { + cy.findByText(`${mockConfig.label} – ${mockKernel.label}`) + .should('be.visible') + .closest('tr') + .within(() => { + ui.button.findByTitle('Edit').click(); + }); + }); + + // Set up mocks for config update. + mockGetVLANs(mockVLANs); + mockGetVPC(mockVPC).as('getVPC'); + mockUpdateLinodeConfigs(mockLinode.id, mockConfigWithVpc).as( + 'updateLinodeConfigs' + ); + mockGetLinodeConfigs(mockLinode.id, [mockConfigWithVpc]).as( + 'getLinodeConfigs' + ); ui.dialog - .findByTitle('Confirm Delete') + .findByTitle('Edit Configuration') .should('be.visible') .within(() => { + // Set eth2 to VPC and submit. + cy.get('[data-qa-textfield-label="eth2"]') + .scrollIntoView() + .click() + .type('VPC{enter}'); + ui.button - .findByTitle('Delete') + .findByTitle('Save Changes') .scrollIntoView() .should('be.visible') .should('be.enabled') .click(); }); - cy.wait('@deleteLinodeConfig') - .its('response.statusCode') - .should('eq', 200); - ui.toast.assertMessage( - 'Configuration My Debian 10 Disk Profile successfully deleted' - ); + cy.wait(['@updateLinodeConfigs', '@getLinodeConfigs', '@getVPC']); + + // Confirm that VLAN and VPC have been assigned. cy.findByLabelText('List of Configurations').within(() => { - containsVisible('No data to display.'); + cy.contains(`${mockConfig.label} – ${mockKernel.label}`).should( + 'be.visible' + ); + cy.contains('eth0 – Public Internet').should('be.visible'); + cy.contains(`eth2 – VPC: ${mockVPC.label}`).should('be.visible'); }); + + cy.findByText('REBOOT NEEDED').should('be.visible'); }); }); }); diff --git a/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts b/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts index 72bd950181b..848aeb2fc87 100644 --- a/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts @@ -1,8 +1,8 @@ -import { createLinode } from 'support/api/linodes'; -import { containsVisible, fbtVisible } from 'support/helpers'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; import { authenticate } from 'support/api/authentication'; +import { createTestLinode } from 'support/util/linodes'; +import type { Linode } from '@linode/api-v4'; authenticate(); describe('switch linode state', () => { @@ -10,13 +10,19 @@ describe('switch linode state', () => { cleanUp(['linodes']); }); + /* + * - Confirms that a Linode can be shut down from the Linodes landing page. + * - Confirms flow end-to-end using real API requests. + * - Confirms that landing page UI updates to reflect Linode power state. + * - Does not wait for Linode to finish being shut down before succeeding. + */ it('powers off a linode from landing page', () => { - createLinode().then((linode) => { + cy.defer(createTestLinode()).then((linode: Linode) => { cy.visitWithLogin('/linodes'); cy.get(`[data-qa-linode="${linode.label}"]`) .should('be.visible') .within(() => { - containsVisible('Running'); + cy.contains('Running').should('be.visible'); }); ui.actionMenu @@ -40,17 +46,22 @@ describe('switch linode state', () => { cy.get(`[data-qa-linode="${linode.label}"]`) .should('be.visible') .within(() => { - containsVisible('Shutting Down'); - cy.contains('Offline', { timeout: 300000 }).should('be.visible'); + cy.contains('Shutting Down').should('be.visible'); }); }); }); + /* + * - Confirms that a Linode can be shut down from its details page. + * - Confirms flow end-to-end using real API requests. + * - Confirms that details page UI updates to reflect Linode power state. + * - Waits for Linode to fully shut down before succeeding. + */ it('powers off a linode from details page', () => { - createLinode().then((linode) => { + cy.defer(createTestLinode()).then((linode: Linode) => { cy.visitWithLogin(`/linodes/${linode.id}`); - containsVisible('RUNNING'); - fbtVisible(linode.label); + cy.contains('RUNNING').should('be.visible'); + cy.findByText(linode.label).should('be.visible'); cy.findByText('Power Off').should('be.visible').click(); ui.dialog @@ -63,18 +74,24 @@ describe('switch linode state', () => { .should('be.enabled') .click(); }); - containsVisible('SHUTTING DOWN'); + cy.contains('SHUTTING DOWN').should('be.visible'); cy.contains('OFFLINE', { timeout: 300000 }).should('be.visible'); }); }); + /* + * - Confirms that a Linode can be booted from the Linode landing page. + * - Confirms flow end-to-end using real API requests. + * - Confirms that landing page UI updates to reflect Linode power state. + * - Waits for Linode to finish booting up before succeeding. + */ it('powers on a linode from landing page', () => { - createLinode({ booted: false }).then((linode) => { + cy.defer(createTestLinode({ booted: false })).then((linode: Linode) => { cy.visitWithLogin('/linodes'); cy.get(`[data-qa-linode="${linode.label}"]`) .should('be.visible') .within(() => { - containsVisible('Offline'); + cy.contains('Offline').should('be.visible'); }); ui.actionMenu @@ -98,17 +115,23 @@ describe('switch linode state', () => { cy.get(`[data-qa-linode="${linode.label}"]`) .should('be.visible') .within(() => { - containsVisible('Booting'); + cy.contains('Booting').should('be.visible'); cy.contains('Running', { timeout: 300000 }).should('be.visible'); }); }); }); + /* + * - Confirms that a Linode can be booted from its details page. + * - Confirms flow end-to-end using real API requests. + * - Confirms that details page UI updates to reflect Linode power state. + * - Does not wait for Linode to finish booting up before succeeding. + */ it('powers on a linode from details page', () => { - createLinode({ booted: false }).then((linode) => { + cy.defer(createTestLinode({ booted: false })).then((linode: Linode) => { cy.visitWithLogin(`/linodes/${linode.id}`); - containsVisible('OFFLINE'); - fbtVisible(linode.label); + cy.contains('OFFLINE').should('be.visible'); + cy.findByText(linode.label).should('be.visible'); cy.findByText('Power On').should('be.visible').click(); ui.dialog @@ -121,18 +144,24 @@ describe('switch linode state', () => { .should('be.enabled') .click(); }); - containsVisible('BOOTING'); - cy.contains('RUNNING', { timeout: 300000 }).should('be.visible'); + + cy.contains('BOOTING').should('be.visible'); }); }); + /* + * - Confirms that a Linode can be rebooted from the Linode landing page. + * - Confirms flow end-to-end using real API requests. + * - Confirms that landing page UI updates to reflect Linode power state. + * - Does not wait for Linode to finish rebooting before succeeding. + */ it('reboots a linode from landing page', () => { - createLinode().then((linode) => { + cy.defer(createTestLinode()).then((linode: Linode) => { cy.visitWithLogin('/linodes'); cy.get(`[data-qa-linode="${linode.label}"]`) .should('be.visible') .within(() => { - containsVisible('Running'); + cy.contains('Running').should('be.visible'); }); ui.actionMenu @@ -156,17 +185,22 @@ describe('switch linode state', () => { cy.get(`[data-qa-linode="${linode.label}"]`) .should('be.visible') .within(() => { - containsVisible('Rebooting'); - cy.contains('Running', { timeout: 300000 }).should('be.visible'); + cy.contains('Rebooting').should('be.visible'); }); }); }); + /* + * - Confirms that a Linode can be rebooted from its details page. + * - Confirms flow end-to-end using real API requests. + * - Confirms that details page UI updates to reflect Linode power state. + * - Waits for Linode to finish rebooting before succeeding. + */ it('reboots a linode from details page', () => { - createLinode().then((linode) => { + cy.defer(createTestLinode()).then((linode: Linode) => { cy.visitWithLogin(`/linodes/${linode.id}`); - containsVisible('RUNNING'); - fbtVisible(linode.label); + cy.contains('RUNNING').should('be.visible'); + cy.findByText(linode.label).should('be.visible'); cy.findByText('Reboot').should('be.visible').click(); ui.dialog @@ -179,7 +213,7 @@ describe('switch linode state', () => { .should('be.enabled') .click(); }); - containsVisible('REBOOTING'); + cy.contains('REBOOTING').should('be.visible'); cy.contains('RUNNING', { timeout: 300000 }).should('be.visible'); }); }); diff --git a/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts index 37841935c51..84d67db2cb4 100644 --- a/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts @@ -69,6 +69,7 @@ describe('object storage access keys smoke tests', () => { cy.findByLabelText('Label').click().type(mockAccessKey.label); ui.buttonGroup .findButtonByTitle('Create Access Key') + .scrollIntoView() .should('be.visible') .should('be.enabled') .click(); diff --git a/packages/manager/cypress/e2e/core/objectStorage/object-storage.e2e.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/object-storage.e2e.spec.ts index 1bd55fd7712..1f21b408147 100644 --- a/packages/manager/cypress/e2e/core/objectStorage/object-storage.e2e.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorage/object-storage.e2e.spec.ts @@ -343,8 +343,10 @@ describe('object storage end-to-end tests', () => { assertStatusForUrlAtAlias('@bucketObjectUrl', 403); // Make object public, confirm it can be accessed, then close drawer. - cy.findByText('Access Control List (ACL)') + cy.findByLabelText('Access Control List (ACL)') .should('be.visible') + .should('not.have.value', 'Loading access...') + .should('have.value', 'Private') .click() .type('Public Read'); @@ -417,8 +419,10 @@ describe('object storage end-to-end tests', () => { cy.wait('@getBucketAccess'); // Make object public, confirm it can be accessed. - cy.findByText('Access Control List (ACL)') + cy.findByLabelText('Access Control List (ACL)') .should('be.visible') + .should('not.have.value', 'Loading access...') + .should('have.value', 'Private') .click() .type('Public Read'); diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts index 43da412e2f0..cf132d44e2c 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts @@ -59,6 +59,7 @@ describe('VPC assign/unassign flows', () => { const mockSubnet = subnetFactory.build({ id: randomNumber(2), label: randomLabel(), + linodes: [], }); const mockVPC = vpcFactory.build({ diff --git a/packages/manager/cypress/support/api/linodes.ts b/packages/manager/cypress/support/api/linodes.ts index 7b8135af09b..6865519b8ac 100644 --- a/packages/manager/cypress/support/api/linodes.ts +++ b/packages/manager/cypress/support/api/linodes.ts @@ -46,6 +46,11 @@ export const requestBody = (data: Partial) => { return linodeRequest({ label, ...defaultLinodeRequestBody, ...data }); }; +/** + * Deprecated. Use `createTestLinode()` with `cy.defer()` instead. + * + * @deprecated + */ export const createLinode = (data = {}) => { return requestBody(data).then((resp) => { apiCheckErrors(resp); diff --git a/packages/manager/cypress/support/helpers.ts b/packages/manager/cypress/support/helpers.ts index eec339af751..4cbccea82dd 100644 --- a/packages/manager/cypress/support/helpers.ts +++ b/packages/manager/cypress/support/helpers.ts @@ -2,38 +2,83 @@ finding and asserting visible without having to chain. They don't chain off of cy */ const visible = 'be.visible'; +/** + * Deprecated. Use `cy.contains(text).should('be.visible')` instead. + * + * @deprecated + */ export const containsVisible = (text: string) => { return cy.contains(text).should(visible); }; +/** + * Deprecated. Use `cy.contains(text).click()` instead. + * + * @deprecated + */ export const containsClick = (text: string) => { return cy.contains(text).click(); }; +/** + * Deprecated. Use `cy.findByPlaceholderText(text).click()` instead. + * + * @deprecated + */ export const containsPlaceholderClick = (text: string) => { return cy.get(`[placeholder="${text}"]`).click(); }; +/** + * Deprecated. Use `cy.get(element).should('be.visible')` instead. + * + * @deprecated + */ export const getVisible = (element: string) => { return cy.get(element).should(visible); }; +/** + * Deprecated. Use `cy.get(element).click()` instead. + * + * @deprecated + */ export const getClick = (element: string) => { return cy.get(element).click(); }; +/** + * Deprecated. Use `cy.findByText(text).should('be.visible')` instead. + * + * @deprecated + */ export const fbtVisible = (text: string) => { return cy.findByText(text).should(visible); }; +/** + * Deprecated. Use `cy.findByText(text).click()` instead. + * + * @deprecated + */ export const fbtClick = (text: string) => { return cy.findByText(text).click(); }; +/** + * Deprecated. Use `cy.findByLabelText(text).should('be.visible')` instead. + * + * @deprecated + */ export const fbltVisible = (text: string) => { return cy.findByLabelText(text).should(visible); }; +/** + * Deprecated. Use `cy.findByLabelText(text).click()` instead. + * + * @deprecated + */ export const fbltClick = (text: string) => { return cy.findByLabelText(text).click(); }; diff --git a/packages/manager/cypress/support/intercepts/linodes.ts b/packages/manager/cypress/support/intercepts/linodes.ts index 66e88b4a096..89d5f525be0 100644 --- a/packages/manager/cypress/support/intercepts/linodes.ts +++ b/packages/manager/cypress/support/intercepts/linodes.ts @@ -6,7 +6,7 @@ import { apiMatcher } from 'support/util/intercepts'; import { paginateResponse } from 'support/util/paginate'; import { makeResponse } from 'support/util/response'; -import type { Disk, Linode, LinodeType, Volume } from '@linode/api-v4'; +import type { Disk, Linode, LinodeType, Kernel, Volume } from '@linode/api-v4'; import { makeErrorResponse } from 'support/util/errors'; /** @@ -374,3 +374,39 @@ export const mockMigrateLinode = ( {} ); }; + +/** + * Intercepts GET request to fetch Linode kernels and mocks response. + * + * @param mockKernels - Array of Kernel objects with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetLinodeKernels = ( + mockKernels: Kernel[] +): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher('linode/kernels*'), + paginateResponse(mockKernels) + ); +}; + +/** + * Intercepts GET request to fetch a Linode kernel and mocks response. + * + * @param kernelId - ID of Kernel for which to mock response. + * @param mockKernel - Kernel object with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetLinodeKernel = ( + kernelId: string, + mockKernel: Kernel +): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher(`linode/kernels/${kernelId}`), + makeResponse(mockKernel) + ); +}; diff --git a/packages/manager/cypress/support/util/kernels.ts b/packages/manager/cypress/support/util/kernels.ts new file mode 100644 index 00000000000..0df35d498ea --- /dev/null +++ b/packages/manager/cypress/support/util/kernels.ts @@ -0,0 +1,38 @@ +/** + * @file Utilities for Linode kernel retrieval and management. + */ + +import { getLinodeKernels } from '@linode/api-v4'; +import { depaginate } from './paginate'; +import { pageSize } from 'support/constants/api'; + +import type { Kernel } from '@linode/api-v4'; + +/** + * Fetches all Linode kernels. + * + * @returns Promise that resolves to an array of `Kernel` instances. + */ +export const fetchAllKernels = async (): Promise => { + return depaginate((page) => + getLinodeKernels({ page, page_size: pageSize }) + ); +}; + +/** + * Finds a `Kernel` in an array of `Kernel`s by its ID. + * + * @param kernels - Array of Kernels from which to search. + * @param kernelId - ID of Kernel to find. + * + * @throws When a Kernel with ID `kernelId` does not exist in `kernels`. + * + * @return Kernel instance with given ID. + */ +export const findKernelById = (kernels: Kernel[], kernelId: string) => { + const kernel = kernels.find((kernel) => kernel.id === kernelId); + if (!kernel) { + throw new Error(`Unable to find a Linode kernel with ID '${kernelId}'`); + } + return kernel; +}; diff --git a/packages/manager/cypress/support/util/linodes.ts b/packages/manager/cypress/support/util/linodes.ts index 3a0cb15f843..68d33007a7b 100644 --- a/packages/manager/cypress/support/util/linodes.ts +++ b/packages/manager/cypress/support/util/linodes.ts @@ -4,91 +4,147 @@ import { SimpleBackoffMethod } from 'support/util/backoff'; import { pollLinodeDiskStatuses, pollLinodeStatus } from 'support/util/polling'; import { randomLabel } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; +import { depaginate } from './paginate'; +import { pageSize } from 'support/constants/api'; -import type { Config, Linode, LinodeConfigCreationData } from '@linode/api-v4'; +import type { Config, Linode } from '@linode/api-v4'; import type { CreateLinodeRequest } from '@linode/api-v4'; /** - * Creates a Linode and waits for it to be in "running" state. - * - * @param createPayload - Optional Linode create payload options. - * - * @returns Promis that resolves when Linode is created and booted. + * Options to control the behavior of test Linode creation. */ -export const createAndBootLinode = async ( - createPayload?: Partial -): Promise => { - const payload = createLinodeRequestFactory.build({ - label: randomLabel(), - region: chooseRegion().id, - ...(createPayload ?? {}), - }); - const linode = await createLinode(payload); +export interface CreateTestLinodeOptions { + /** Whether to wait for created Linode disks to be available before resolving. */ + waitForDisks: boolean; - await pollLinodeStatus( - linode.id, - 'running', - new SimpleBackoffMethod(5000, { - initialDelay: 15000, - maxAttempts: 25, - }) - ); + /** Whether to wait for created Linode to boot before resolving. */ + waitForBoot: boolean; +} - return linode; +/** + * Default test Linode creation options. + */ +export const defaultCreateTestLinodeOptions = { + waitForDisks: false, + waitForBoot: false, }; -interface LinodeConfigRequestOverride - extends Omit, - LinodeConfigCreationData {} - /** - * Creates a Linode and returns the first config for that Linode. + * Creates a Linode to use during tests. + * + * @param createRequestPayload - Partial Linode request payload to override default payload. + * @param options - Linode create and polling options. + * + * @returns Promise that resolves to the created Linode. */ -export const createLinodeAndGetConfig = async ({ - linodeConfigRequestOverride = {}, - waitForLinodeToBeRunning = false, -}: { - linodeConfigRequestOverride?: Partial; - waitForLinodeToBeRunning?: boolean; -}): Promise<[Linode, Config]> => { - const createPayload = createLinodeRequestFactory.build({ - label: randomLabel(), - region: chooseRegion().id, - }); - const linode = await createLinode({ - ...createPayload, - ...linodeConfigRequestOverride, - }); +export const createTestLinode = async ( + createRequestPayload?: Partial | null, + options?: Partial +): Promise => { + const resolvedOptions = { + ...defaultCreateTestLinodeOptions, + ...(options || {}), + }; - const { data: configs } = await getLinodeConfigs(linode.id); + const resolvedCreatePayload = { + ...createLinodeRequestFactory.build({ + label: randomLabel(), + image: 'linode/debian11', + region: chooseRegion().id, + }), + ...(createRequestPayload || {}), + }; + + // Display warnings for certain combinations of options/request payloads... + if (resolvedOptions.waitForDisks && resolvedOptions.waitForBoot) { + console.warn( + 'Ignoring `waitForDisks` option because `waitForBoot` takes precedence.' + ); + } + + if (!resolvedCreatePayload.booted && resolvedOptions.waitForBoot) { + console.warn( + 'Using `waitForBoot` option when Linode payload `booted` is false will cause a timeout.' + ); + } - // we may want the linode to be booted to interact with the config - waitForLinodeToBeRunning && - (await pollLinodeStatus( + const linode = await createLinode(resolvedCreatePayload); + + // Wait for disks to become available if `waitForDisks` option is set. + // We skip this step if `waitForBoot` is set, however, because waiting for boot + // implicitly waits for disks. + // + if (resolvedOptions.waitForDisks && !resolvedOptions.waitForBoot) { + // Wait 7.5 seconds before initial check, then poll again every 5 seconds. + await pollLinodeDiskStatuses( linode.id, - 'running', + 'ready', new SimpleBackoffMethod(5000, { - initialDelay: 15000, + initialDelay: 7500, maxAttempts: 25, }) - )); + ); + } - // If we don't wait for the Linode to boot, we wait for the disks to be ready. - // Wait 7.5 seconds, then poll the Linode disks every 5 seconds until they are ready. - !waitForLinodeToBeRunning && - (await pollLinodeDiskStatuses( + // Wait for Linode status to be 'running' if `waitForBoot` is true. + if (resolvedOptions.waitForBoot) { + // Wait 15 seconds before initial check, then poll again every 5 seconds. + await pollLinodeStatus( linode.id, - 'ready', + 'running', new SimpleBackoffMethod(5000, { - initialDelay: 7500, + initialDelay: 15000, maxAttempts: 25, }) - )); - - // Throw if Linode has no config. - if (!configs[0] || !linode.id) { - throw new Error('Created Linode does not have any config'); + ); } - return [linode, configs[0]]; + Cypress.log({ + name: 'createTestLinode', + message: `Create Linode '${linode.label}' (ID ${linode.id})`, + consoleProps: () => { + return { + options: resolvedOptions, + payload: resolvedCreatePayload, + linode, + }; + }, + }); + + return linode; +}; + +/** + * Creates a Linode and waits for it to be in "running" state. + * + * Deprecated. Use `createTestLinode` with `waitForBoot` set to `true`. + * + * @param createPayload - Optional Linode create payload options. + * + * @deprecated + * + * @returns Promis that resolves when Linode is created and booted. + */ +export const createAndBootLinode = async ( + createPayload?: Partial +): Promise => { + console.warn( + '`createAndBootLinode()` is deprecated. Use `createTestLinode()` instead.' + ); + return createTestLinode(createPayload, { waitForBoot: true }); +}; + +/** + * Retrieves all Config objects belonging to a Linode. + * + * @param linodeId - ID of Linode for which to retrieve Configs. + * + * @returns Promise that resolves to an array of Config objects for the given Linode. + */ +export const fetchLinodeConfigs = async ( + linodeId: number +): Promise => { + return depaginate((page) => + getLinodeConfigs(linodeId, { page, page_size: pageSize }) + ); }; diff --git a/packages/manager/src/factories/index.ts b/packages/manager/src/factories/index.ts index eaa3e21ba2f..f416b8d53ea 100644 --- a/packages/manager/src/factories/index.ts +++ b/packages/manager/src/factories/index.ts @@ -20,6 +20,7 @@ export * from './featureFlags'; export * from './firewalls'; export * from './grants'; export * from './images'; +export * from './kernels'; export * from './kubernetesCluster'; export * from './linodeConfigInterfaceFactory'; export * from './linodeConfigs'; From a0dad153a1120c90963ec36037388b6672a159ab Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Fri, 26 Apr 2024 12:10:58 -0400 Subject: [PATCH 04/40] upcoming: [M3-7980] - Update PlacementGroups linodes tooltip and SelectPlacementGroup option label (#10408) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Small PG UX updates * Added changeset: Update Placement GroupTable Row linodes tooltip and SelectPlacementGroup option label * Feedback @bnussman-akamai & @mjac0bs * 💩 --- .../pr-10408-upcoming-features-1714066606151.md | 5 +++++ .../PlacementGroupSelectOption.tsx | 16 ++++++++++++++-- .../PlacementGroupsSelect.test.tsx | 4 ++-- .../PlacementGroupsSelect.tsx | 10 +++------- .../src/components/TextTooltip/TextTooltip.tsx | 7 +++++++ .../PlacementGroupsDetailPanel.tsx | 8 +++++++- .../PlacementGroupsRow.tsx | 15 ++++++++++----- 7 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 packages/manager/.changeset/pr-10408-upcoming-features-1714066606151.md diff --git a/packages/manager/.changeset/pr-10408-upcoming-features-1714066606151.md b/packages/manager/.changeset/pr-10408-upcoming-features-1714066606151.md new file mode 100644 index 00000000000..f860095a1bd --- /dev/null +++ b/packages/manager/.changeset/pr-10408-upcoming-features-1714066606151.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Update Placement Group Table Row linodes tooltip and SelectPlacementGroup option label ([#10408](https://github.com/linode/manager/pull/10408)) diff --git a/packages/manager/src/components/PlacementGroupsSelect/PlacementGroupSelectOption.tsx b/packages/manager/src/components/PlacementGroupsSelect/PlacementGroupSelectOption.tsx index 3dc11d5ecda..bc9c85d1c94 100644 --- a/packages/manager/src/components/PlacementGroupsSelect/PlacementGroupSelectOption.tsx +++ b/packages/manager/src/components/PlacementGroupsSelect/PlacementGroupSelectOption.tsx @@ -1,8 +1,9 @@ -import { PlacementGroup } from '@linode/api-v4'; +import { AFFINITY_TYPES, PlacementGroup } from '@linode/api-v4'; import { visuallyHidden } from '@mui/utils'; import React from 'react'; import { Box } from 'src/components/Box'; +import { Stack } from 'src/components/Stack'; import { Tooltip } from 'src/components/Tooltip'; import { PLACEMENT_GROUP_HAS_NO_CAPACITY } from 'src/features/PlacementGroups/constants'; @@ -63,7 +64,18 @@ export const PlacementGroupSelectOption = ({ aria-disabled={undefined} > - {label} + + {label} + + + ({AFFINITY_TYPES[value.affinity_type]}) + + {disabled && ( { fireEvent.focus(select); fireEvent.change(select, { - target: { value: 'my-placement-group (Affinity)' }, + target: { value: 'my-placement-group' }, }); - const selectedRegionOption = getByText('my-placement-group (Affinity)'); + const selectedRegionOption = getByText('my-placement-group'); fireEvent.click(selectedRegionOption); expect( diff --git a/packages/manager/src/components/PlacementGroupsSelect/PlacementGroupsSelect.tsx b/packages/manager/src/components/PlacementGroupsSelect/PlacementGroupsSelect.tsx index 8ac18bd25ad..b82e05e7571 100644 --- a/packages/manager/src/components/PlacementGroupsSelect/PlacementGroupsSelect.tsx +++ b/packages/manager/src/components/PlacementGroupsSelect/PlacementGroupsSelect.tsx @@ -1,6 +1,4 @@ -import { AFFINITY_TYPES } from '@linode/api-v4'; import { APIError } from '@linode/api-v4/lib/types'; -import { SxProps } from '@mui/system'; import * as React from 'react'; import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; @@ -11,6 +9,7 @@ import { useAllPlacementGroupsQuery } from 'src/queries/placementGroups'; import { PlacementGroupSelectOption } from './PlacementGroupSelectOption'; import type { PlacementGroup, Region } from '@linode/api-v4'; +import type { SxProps } from '@mui/system'; export interface PlacementGroupsSelectProps { clearable?: boolean; @@ -71,9 +70,6 @@ export const PlacementGroupsSelect = (props: PlacementGroupsSelectProps) => { return null; } - const formatLabel = (placementGroup: PlacementGroup) => - `${placementGroup.label} (${AFFINITY_TYPES[placementGroup.affinity_type]})`; - const placementGroupsOptions: PlacementGroup[] = placementGroups.filter( (placementGroup) => placementGroup.region === selectedRegion?.id ); @@ -96,7 +92,7 @@ export const PlacementGroupsSelect = (props: PlacementGroupsSelectProps) => { { disableClearable={!clearable} disabled={Boolean(!selectedRegion?.id) || disabled} errorText={errorText} - getOptionLabel={formatLabel} + getOptionLabel={(placementGroup: PlacementGroup) => placementGroup.label} id={id} label={label} loading={isLoading || loading} diff --git a/packages/manager/src/components/TextTooltip/TextTooltip.tsx b/packages/manager/src/components/TextTooltip/TextTooltip.tsx index fb51b2a4b5e..d9cb98a03e2 100644 --- a/packages/manager/src/components/TextTooltip/TextTooltip.tsx +++ b/packages/manager/src/components/TextTooltip/TextTooltip.tsx @@ -9,6 +9,10 @@ import type { TooltipProps } from '@mui/material/Tooltip'; import type { TypographyProps } from 'src/components/Typography'; export interface TextTooltipProps { + /** + * Props to pass to the Popper component + */ + PopperProps?: TooltipProps['PopperProps']; /** The text to hover on to display the tooltip */ displayText: string; /** If true, the tooltip will not have a min-width of 375px @@ -36,6 +40,7 @@ export interface TextTooltipProps { */ export const TextTooltip = (props: TextTooltipProps) => { const { + PopperProps, displayText, minWidth, placement, @@ -47,7 +52,9 @@ export const TextTooltip = (props: TextTooltipProps) => { return ( div': { minWidth: minWidth ? minWidth : 375, }, diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetailPanel.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetailPanel.tsx index 76e9b9c161a..e2b4bdbabea 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetailPanel.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetailPanel.tsx @@ -123,12 +123,15 @@ export const PlacementGroupsDetailPanel = (props: Props) => { mb: 1, width: '100%', }} + textFieldProps={{ + tooltipPosition: 'right', + tooltipText: PLACEMENT_GROUP_SELECT_TOOLTIP_COPY, + }} disabled={isPlacementGroupSelectDisabled} label={placementGroupSelectLabel} noOptionsMessage="There are no Placement Groups in this region." selectedPlacementGroup={selectedPlacementGroup} selectedRegion={selectedRegion} - textFieldProps={{ tooltipText: PLACEMENT_GROUP_SELECT_TOOLTIP_COPY }} /> {selectedRegion && hasRegionPlacementGroupCapability && ( - + + ); }); From c2dd6eed09a6ae9123f67253299e153107ccb02f Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Fri, 3 May 2024 19:02:09 -0400 Subject: [PATCH 29/40] upcoming: [M3 -8049] - Last round of placement group copy updates (#10434) * Affinity type enforcement * other updates * Added changeset: Placement Groups final copy updates --- packages/api-v4/src/placement-groups/types.ts | 2 +- ...r-10434-upcoming-features-1714746439358.md | 5 ++++ ...entGroupsAffinityEnforcementRadioGroup.tsx | 24 ++++++++++--------- .../PlacementGroupsAffinityTypeSelect.tsx | 4 +--- .../PlacementGroupsAssignLinodesDrawer.tsx | 2 +- .../PlacementGroupsCreateDrawer.test.tsx | 2 +- .../PlacementGroupsCreateDrawer.tsx | 6 ++--- .../PlacementGroupsEditDrawer.tsx | 4 ++-- .../features/PlacementGroups/utils.test.ts | 2 +- .../src/features/PlacementGroups/utils.ts | 6 ++--- 10 files changed, 31 insertions(+), 26 deletions(-) create mode 100644 packages/manager/.changeset/pr-10434-upcoming-features-1714746439358.md diff --git a/packages/api-v4/src/placement-groups/types.ts b/packages/api-v4/src/placement-groups/types.ts index dc69fa56402..e092d401ff3 100644 --- a/packages/api-v4/src/placement-groups/types.ts +++ b/packages/api-v4/src/placement-groups/types.ts @@ -6,7 +6,7 @@ export const AFFINITY_TYPES = { } as const; export type AffinityType = keyof typeof AFFINITY_TYPES; -export type AffinityEnforcement = 'Strict' | 'Flexible'; +export type AffinityTypeEnforcement = 'Strict' | 'Flexible'; export interface PlacementGroup { id: number; diff --git a/packages/manager/.changeset/pr-10434-upcoming-features-1714746439358.md b/packages/manager/.changeset/pr-10434-upcoming-features-1714746439358.md new file mode 100644 index 00000000000..af0b6e98b98 --- /dev/null +++ b/packages/manager/.changeset/pr-10434-upcoming-features-1714746439358.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Placement Groups final copy updates ([#10434](https://github.com/linode/manager/pull/10434)) diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityEnforcementRadioGroup.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityEnforcementRadioGroup.tsx index 1ce623874b5..6a25fe2747b 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityEnforcementRadioGroup.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityEnforcementRadioGroup.tsx @@ -17,7 +17,9 @@ interface Props { value: boolean; } -export const PlacementGroupsAffinityEnforcementRadioGroup = (props: Props) => { +export const PlacementGroupsAffinityTypeEnforcementRadioGroup = ( + props: Props +) => { const { disabledPlacementGroupCreateButton, handleChange, @@ -27,27 +29,27 @@ export const PlacementGroupsAffinityEnforcementRadioGroup = (props: Props) => { return ( - - Affinity Enforcement + + Affinity Type Enforcement { handleChange(event); setFieldValue('is_strict', event.target.value === 'true'); }} - id="affinity-enforcement-radio-group" + id="affinity-type-enforcement-radio-group" name="is_strict" value={value} > - Strict. You cannot assign a Linode to your - placement group if it will violate the policy of your selected - Affinity Type (best practice). + Strict. You can’t assign Linodes if the preferred + container defined by your Affinity Type lacks capacity or is + unavailable (best practice). } control={} @@ -57,9 +59,9 @@ export const PlacementGroupsAffinityEnforcementRadioGroup = (props: Props) => { - Flexible. You can assign a Linode to your - placement group, even if it violates the policy of your selected - Affinity Type. + Flexible. You can assign Linodes, even if they’re + not in the preferred container defined by your Affinity Type, but + your placement group will be non-compliant. } control={} diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityTypeSelect.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityTypeSelect.tsx index c24de83637f..65262a69776 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityTypeSelect.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityTypeSelect.tsx @@ -35,7 +35,7 @@ export const PlacementGroupsAffinityTypeSelect = (props: Props) => { title={ isDisabledMenuItem ? ( - Only supporting Anti-affinity host placement groups for Beta.{' '} + Currently, only Anti-affinity placement groups are supported.{' '} Learn more. ) : ( @@ -80,8 +80,6 @@ export const PlacementGroupsAffinityTypeSelect = (props: Props) => { performance. Linodes in a placement group that use Anti-affinity are in separate fault domains, but still in the same data center. Use this to support a high-availability model. -
- Learn more. ), }} diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.tsx index f779dbff49c..f912ab9d0c0 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.tsx @@ -181,7 +181,7 @@ export const PlacementGroupsAssignLinodesDrawer = ( placement="right" status="help" sxTooltipIcon={{ position: 'relative', top: 4 }} - text="Only displaying Linodes that aren’t assigned to a Placement Group" + text="Only displaying Linodes that aren’t assigned to a Placement Group." />
{ expect(getByLabelText('Label')).toBeEnabled(); expect(getByLabelText('Region')).toBeEnabled(); expect(getByLabelText('Affinity Type')).toBeEnabled(); - expect(getByText('Affinity Enforcement')).toBeInTheDocument(); + expect(getByText('Affinity Type Enforcement')).toBeInTheDocument(); const radioInputs = getAllByRole('radio'); expect(radioInputs).toHaveLength(2); diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index 356517acba2..c41a8e07a08 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -24,7 +24,7 @@ import { getFormikErrorsFromAPIErrors } from 'src/utilities/formikErrorUtils'; import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { MAXIMUM_NUMBER_OF_PLACEMENT_GROUPS_IN_REGION } from './constants'; -import { PlacementGroupsAffinityEnforcementRadioGroup } from './PlacementGroupsAffinityEnforcementRadioGroup'; +import { PlacementGroupsAffinityTypeEnforcementRadioGroup } from './PlacementGroupsAffinityEnforcementRadioGroup'; import { PlacementGroupsAffinityTypeSelect } from './PlacementGroupsAffinityTypeSelect'; import { getMaxPGsPerCustomer, @@ -212,7 +212,7 @@ export const PlacementGroupsCreateDrawer = ( helperText={values.region && pgRegionLimitHelperText} regions={regions ?? []} selectedId={selectedRegionId ?? values.region} - tooltipText="Only regions supporting Placement Groups are listed." + tooltipText="Only Linode data center regions that support placement groups are listed." /> )} - { }); }); -describe('getAffinityEnforcement', () => { +describe('getAffinityTypeEnforcement', () => { it('returns "Strict" if `is_strict` is true', () => { expect(getAffinityTypeEnforcement(true)).toBe('Strict'); }); diff --git a/packages/manager/src/features/PlacementGroups/utils.ts b/packages/manager/src/features/PlacementGroups/utils.ts index 580ba011f2c..2e30c1264ff 100644 --- a/packages/manager/src/features/PlacementGroups/utils.ts +++ b/packages/manager/src/features/PlacementGroups/utils.ts @@ -4,7 +4,7 @@ import { useFlags } from 'src/hooks/useFlags'; import { useAccount } from 'src/queries/account/account'; import type { - AffinityEnforcement, + AffinityTypeEnforcement, CreatePlacementGroupPayload, Linode, PlacementGroup, @@ -12,11 +12,11 @@ import type { } from '@linode/api-v4'; /** - * Helper to get the affinity enforcement readable string. + * Helper to get the affinity type enforcement readable string. */ export const getAffinityTypeEnforcement = ( is_strict: boolean -): AffinityEnforcement => { +): AffinityTypeEnforcement => { return is_strict ? 'Strict' : 'Flexible'; }; From ee202fe212373c2f70e1e3261dab91aa72809112 Mon Sep 17 00:00:00 2001 From: Hussain Khalil <122488130+hkhalil-akamai@users.noreply.github.com> Date: Mon, 6 May 2024 13:41:10 -0400 Subject: [PATCH 30/40] upcoming: [M3-7983] - Use 'edge'-class plans for edge regions (#10415) * Use 'edge' plans instead of dedicated * Add edge plans to plan selections util * Refactor showTransfer to showLimits * Added changeset: Use 'edge'-class plans in edge regions * Added changeset: 'edge' Linode type class * Fix crash due to missing plan selection * Fix failing test * Fix ghost tab * Feedback @bnussman-akamai --- .../pr-10415-added-1714498681248.md | 5 ++++ packages/api-v4/src/linodes/types.ts | 3 +- ...r-10415-upcoming-features-1714498636128.md | 5 ++++ .../features/Linodes/LinodeCreatev2/Plan.tsx | 2 +- .../Linodes/LinodesCreate/LinodeCreate.tsx | 2 +- .../components/PlansPanel/PlanContainer.tsx | 30 +++++++++---------- .../components/PlansPanel/PlanSelection.tsx | 22 +++++++++----- .../PlansPanel/PlanSelectionTable.tsx | 8 ++--- .../components/PlansPanel/PlansPanel.tsx | 15 ++++++---- .../features/components/PlansPanel/utils.ts | 2 +- 10 files changed, 57 insertions(+), 37 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-10415-added-1714498681248.md create mode 100644 packages/manager/.changeset/pr-10415-upcoming-features-1714498636128.md diff --git a/packages/api-v4/.changeset/pr-10415-added-1714498681248.md b/packages/api-v4/.changeset/pr-10415-added-1714498681248.md new file mode 100644 index 00000000000..a586e5ea52e --- /dev/null +++ b/packages/api-v4/.changeset/pr-10415-added-1714498681248.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Added +--- + +'edge' Linode type class ([#10415](https://github.com/linode/manager/pull/10415)) diff --git a/packages/api-v4/src/linodes/types.ts b/packages/api-v4/src/linodes/types.ts index 9692eebde78..05f52e39d96 100644 --- a/packages/api-v4/src/linodes/types.ts +++ b/packages/api-v4/src/linodes/types.ts @@ -332,7 +332,8 @@ export type LinodeTypeClass = | 'gpu' | 'metal' | 'prodedicated' - | 'premium'; + | 'premium' + | 'edge'; export interface IPAllocationRequest { type: 'ipv4'; diff --git a/packages/manager/.changeset/pr-10415-upcoming-features-1714498636128.md b/packages/manager/.changeset/pr-10415-upcoming-features-1714498636128.md new file mode 100644 index 00000000000..ef23535b517 --- /dev/null +++ b/packages/manager/.changeset/pr-10415-upcoming-features-1714498636128.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Use 'edge'-class plans in edge regions ([#10415](https://github.com/linode/manager/pull/10415)) diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Plan.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Plan.tsx index 07a89224742..97040979cc3 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/Plan.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Plan.tsx @@ -47,7 +47,7 @@ export const Plan = () => { regionsData={regions} // @todo move this query deeper if possible selectedId={field.value} selectedRegionID={regionId} - showTransfer + showLimits types={types?.map(extendType) ?? []} // @todo don't extend type /> ); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx index 2d04cad246c..3a4f54fc7d1 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx @@ -644,7 +644,7 @@ export class LinodeCreate extends React.PureComponent< regionsData={regionsData!} selectedId={this.props.selectedTypeID} selectedRegionID={selectedRegionID} - showTransfer + showLimits types={this.filterTypes()} />
diff --git a/packages/manager/src/features/components/PlansPanel/PlanContainer.tsx b/packages/manager/src/features/components/PlansPanel/PlanContainer.tsx index 3f04734249f..b4770a6f93d 100644 --- a/packages/manager/src/features/components/PlansPanel/PlanContainer.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlanContainer.tsx @@ -27,13 +27,12 @@ export interface PlanContainerProps { selectedDiskSize?: number; selectedId?: string; selectedRegionId?: Region['id']; - showTransfer?: boolean; + showLimits?: boolean; wholePanelIsDisabled?: boolean; } export const PlanContainer = (props: PlanContainerProps) => { const { - allDisabledPlans, currentPlanHeading, hasMajorityOfPlansDisabled, isCreate, @@ -44,20 +43,19 @@ export const PlanContainer = (props: PlanContainerProps) => { selectedDiskSize, selectedId, selectedRegionId, - showTransfer, + showLimits, wholePanelIsDisabled, } = props; const location = useLocation(); const flags = useFlags(); // Show the Transfer column if, for any plan, the api returned data and we're not in the Database Create flow - const shouldShowTransfer = - showTransfer && plans.some((plan: PlanWithAvailability) => plan.transfer); + const showTransfer = + showLimits && plans.some((plan: PlanWithAvailability) => plan.transfer); // Show the Network throughput column if, for any plan, the api returned data (currently Bare Metal does not) - const shouldShowNetwork = - showTransfer && - plans.some((plan: PlanWithAvailability) => plan.network_out); + const showNetwork = + showLimits && plans.some((plan: PlanWithAvailability) => plan.network_out); // DC Dynamic price logic - DB creation and DB resize flows are currently out of scope const isDatabaseCreateFlow = location.pathname.includes('/databases/create'); @@ -120,6 +118,7 @@ export const PlanContainer = (props: PlanContainerProps) => { selectedDiskSize={selectedDiskSize} selectedId={selectedId} selectedRegionId={selectedRegionId} + showNetwork={showNetwork} showTransfer={showTransfer} wholePanelIsDisabled={wholePanelIsDisabled} /> @@ -127,18 +126,17 @@ export const PlanContainer = (props: PlanContainerProps) => { }); }, [ - allDisabledPlans, - hasMajorityOfPlansDisabled, plans, - selectedRegionId, - wholePanelIsDisabled, currentPlanHeading, + hasMajorityOfPlansDisabled, isCreate, linodeID, onSelect, selectedDiskSize, selectedId, + selectedRegionId, showTransfer, + wholePanelIsDisabled, ] ); @@ -202,8 +200,8 @@ export const PlanContainer = (props: PlanContainerProps) => { } key={`plan-filter-${idx}`} planFilter={table.planFilter} - shouldShowNetwork={shouldShowNetwork} - shouldShowTransfer={shouldShowTransfer} + showNetwork={showNetwork} + showTransfer={showTransfer} /> ) ); @@ -215,8 +213,8 @@ export const PlanContainer = (props: PlanContainerProps) => { } key={planType} renderPlanSelection={renderPlanSelection} - shouldShowNetwork={shouldShowNetwork} - shouldShowTransfer={shouldShowTransfer} + showNetwork={showNetwork} + showTransfer={showTransfer} /> ) )} diff --git a/packages/manager/src/features/components/PlansPanel/PlanSelection.tsx b/packages/manager/src/features/components/PlansPanel/PlanSelection.tsx index 049d85bfff5..f1a69df32d0 100644 --- a/packages/manager/src/features/components/PlansPanel/PlanSelection.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlanSelection.tsx @@ -38,6 +38,7 @@ export interface PlanSelectionProps { selectedDiskSize?: number; selectedId?: string; selectedRegionId?: Region['id']; + showNetwork?: boolean; showTransfer?: boolean; wholePanelIsDisabled?: boolean; } @@ -54,6 +55,7 @@ export const PlanSelection = (props: PlanSelectionProps) => { selectedDiskSize, selectedId, selectedRegionId, + showNetwork, showTransfer, wholePanelIsDisabled, } = props; @@ -67,8 +69,6 @@ export const PlanSelection = (props: PlanSelectionProps) => { const planIsTooSmall = diskSize > plan.disk; const isSamePlan = plan.heading === currentPlanHeading; const isGPU = plan.class === 'gpu'; - const shouldShowTransfer = showTransfer && plan.transfer; - const shouldShowNetwork = showTransfer && plan.network_out; const { data: linode } = useLinodeQuery( linodeID ?? -1, @@ -198,16 +198,22 @@ export const PlanSelection = (props: PlanSelectionProps) => { {convertMegabytesTo(plan.disk, true)} - {shouldShowTransfer && plan.transfer ? ( + {showTransfer ? ( - {plan.transfer / 1000} TB + {plan.transfer ? <>{plan.transfer / 1000} TB : ''} ) : null} - {shouldShowNetwork && plan.network_out ? ( + {showNetwork ? ( - {LINODE_NETWORK_IN} Gbps{' '} - /{' '} - {plan.network_out / 1000} Gbps + {plan.network_out ? ( + <> + {LINODE_NETWORK_IN} Gbps{' '} + /{' '} + {plan.network_out / 1000} Gbps + + ) : ( + '' + )} ) : null} diff --git a/packages/manager/src/features/components/PlansPanel/PlanSelectionTable.tsx b/packages/manager/src/features/components/PlansPanel/PlanSelectionTable.tsx index 97113ab43d6..ee13cbe28b0 100644 --- a/packages/manager/src/features/components/PlansPanel/PlanSelectionTable.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlanSelectionTable.tsx @@ -21,8 +21,8 @@ interface PlanSelectionTableProps { filterOptions?: PlanSelectionFilterOptionsTable | undefined ) => React.JSX.Element[]; shouldDisplayNoRegionSelectedMessage: boolean; - shouldShowNetwork?: boolean; - shouldShowTransfer?: boolean; + showNetwork?: boolean; + showTransfer?: boolean; } const tableCells = [ @@ -47,8 +47,8 @@ export const PlanSelectionTable = (props: PlanSelectionTableProps) => { filterOptions, renderPlanSelection, shouldDisplayNoRegionSelectedMessage, - shouldShowNetwork, - shouldShowTransfer, + showNetwork: shouldShowNetwork, + showTransfer: shouldShowTransfer, } = props; return ( diff --git a/packages/manager/src/features/components/PlansPanel/PlansPanel.tsx b/packages/manager/src/features/components/PlansPanel/PlansPanel.tsx index aaef14f767c..17a37f72059 100644 --- a/packages/manager/src/features/components/PlansPanel/PlansPanel.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlansPanel.tsx @@ -44,7 +44,7 @@ export interface PlansPanelProps { regionsData?: Region[]; selectedId?: string; selectedRegionID?: string; - showTransfer?: boolean; + showLimits?: boolean; tabDisabledMessage?: string; tabbedPanelInnerClass?: string; types: PlanSelectionType[]; @@ -67,7 +67,7 @@ export const PlansPanel = (props: PlansPanelProps) => { regionsData, selectedId, selectedRegionID, - showTransfer, + showLimits, types, } = props; @@ -94,8 +94,14 @@ export const PlansPanel = (props: PlansPanelProps) => { getIsEdgeRegion(regionsData ?? [], selectedRegionID ?? ''); const getDedicatedEdgePlanType = () => { + const edgePlans = types.filter((type) => type.class === 'edge'); + if (edgePlans.length) { + return edgePlans; + } + + // @TODO Remove fallback once edge plans are activated // 256GB and 512GB plans will not be supported for Edge - const plansUpTo128GB = _plans.dedicated.filter( + const plansUpTo128GB = (_plans.dedicated ?? []).filter( (planType) => !['Dedicated 256 GB', 'Dedicated 512 GB'].includes( planType.formattedLabel @@ -114,7 +120,6 @@ export const PlansPanel = (props: PlansPanelProps) => { }); }; - // @TODO Gecko: Get plan data from API when it's available instead of hardcoding const plans = showEdgePlanTable ? { dedicated: getDedicatedEdgePlanType(), @@ -181,7 +186,7 @@ export const PlansPanel = (props: PlansPanelProps) => { selectedDiskSize={disableSmallerPlans?.selectedDiskSize} selectedId={selectedId} selectedRegionId={selectedRegionID} - showTransfer={showTransfer} + showLimits={showLimits} wholePanelIsDisabled={disabled || isPlanPanelDisabled(plan)} /> diff --git a/packages/manager/src/features/components/PlansPanel/utils.ts b/packages/manager/src/features/components/PlansPanel/utils.ts index 9549845dbd3..85c95f3e980 100644 --- a/packages/manager/src/features/components/PlansPanel/utils.ts +++ b/packages/manager/src/features/components/PlansPanel/utils.ts @@ -58,7 +58,7 @@ export const getPlanSelectionsByPlanType = < T extends { class: LinodeTypeClass } >( types: T[] -): PlansByType => { +): Partial> => { const plansByType: PlansByType = planTypeOrder.reduce((acc, key) => { acc[key] = []; return acc; From 423d84fb22c8b2335f4d96059615cfcce6857308 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos <125309814+jaalah-akamai@users.noreply.github.com> Date: Mon, 6 May 2024 14:00:09 -0400 Subject: [PATCH 31/40] feat: [M3-7921] - Added Dialog to Refresh Proxy Tokens as Time Expires (#10361) Co-authored-by: Jaalah Ramos --- ...r-10361-upcoming-features-1712850056835.md | 5 + packages/manager/src/MainContent.tsx | 173 ++++++++-------- .../src/components/Button/StyledLinkButton.ts | 6 +- .../src/context/sessionExpirationContext.ts | 7 + .../src/features/Account/AccountLanding.tsx | 14 +- .../features/Account/SwitchAccountDrawer.tsx | 139 ++++++------- .../SwitchAccounts/ChildAccountList.tsx | 27 ++- .../SessionExpirationDialog.test.tsx | 93 +++++++++ .../SessionExpirationDialog.tsx | 195 ++++++++++++++++++ .../useIsParentTokenExpired.test.tsx | 97 +++++++++ ...gement.tsx => useIsParentTokenExpired.tsx} | 4 +- .../useParentChildAuthentication.tsx | 80 +++++++ .../useParentTokenManagement.test.tsx | 67 ------ .../features/Account/SwitchAccounts/utils.ts | 104 +++++++--- .../manager/src/features/Account/utils.ts | 90 +------- .../GlobalNotifications.tsx | 20 +- .../features/TopMenu/UserMenu/UserMenu.tsx | 19 +- .../manager/src/hooks/useInterval.test.tsx | 61 ++++++ packages/manager/src/hooks/useInterval.tsx | 100 +++++++++ .../hooks/usePendingRevocationToken.test.ts | 90 -------- .../src/hooks/usePendingRevocationToken.ts | 55 ----- .../manager/src/queries/account/account.ts | 10 +- 22 files changed, 920 insertions(+), 536 deletions(-) create mode 100644 packages/manager/.changeset/pr-10361-upcoming-features-1712850056835.md create mode 100644 packages/manager/src/context/sessionExpirationContext.ts create mode 100644 packages/manager/src/features/Account/SwitchAccounts/SessionExpirationDialog.test.tsx create mode 100644 packages/manager/src/features/Account/SwitchAccounts/SessionExpirationDialog.tsx create mode 100644 packages/manager/src/features/Account/SwitchAccounts/useIsParentTokenExpired.test.tsx rename packages/manager/src/features/Account/SwitchAccounts/{useParentTokenManagement.tsx => useIsParentTokenExpired.tsx} (77%) create mode 100644 packages/manager/src/features/Account/SwitchAccounts/useParentChildAuthentication.tsx delete mode 100644 packages/manager/src/features/Account/SwitchAccounts/useParentTokenManagement.test.tsx create mode 100644 packages/manager/src/hooks/useInterval.test.tsx create mode 100644 packages/manager/src/hooks/useInterval.tsx delete mode 100644 packages/manager/src/hooks/usePendingRevocationToken.test.ts delete mode 100644 packages/manager/src/hooks/usePendingRevocationToken.ts diff --git a/packages/manager/.changeset/pr-10361-upcoming-features-1712850056835.md b/packages/manager/.changeset/pr-10361-upcoming-features-1712850056835.md new file mode 100644 index 00000000000..b3c88fdfbad --- /dev/null +++ b/packages/manager/.changeset/pr-10361-upcoming-features-1712850056835.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Dialog to Refresh Proxy Tokens as Time Expires ([#10361](https://github.com/linode/manager/pull/10361)) diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index 1c997953f47..ed77b696fc1 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -28,6 +28,7 @@ import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; import { ENABLE_MAINTENANCE_MODE } from './constants'; import { complianceUpdateContext } from './context/complianceUpdateContext'; +import { sessionExpirationContext } from './context/sessionExpirationContext'; import { switchAccountSessionContext } from './context/switchAccountSessionContext'; import { useIsACLBEnabled } from './features/LoadBalancers/utils'; import { useIsPlacementGroupsEnabled } from './features/PlacementGroups/utils'; @@ -197,6 +198,11 @@ export const MainContent = () => { isOpen: false, }); + const SessionExpirationProvider = sessionExpirationContext.Provider; + const sessionExpirationContextValue = useDialogContext({ + isOpen: false, + }); + const [menuIsOpen, toggleMenu] = React.useState(false); const { _isManagedAccount, @@ -281,94 +287,99 @@ export const MainContent = () => { return (
- - - - toggleMenu(false)} - collapse={desktopMenuIsOpen || false} - open={menuIsOpen} - /> -
- - toggleMenu(true)} - username={username} + + + + + toggleMenu(false)} + collapse={desktopMenuIsOpen || false} + open={menuIsOpen} /> -
- - - - }> - - - {isPlacementGroupsEnabled && ( + + toggleMenu(true)} + username={username} + /> +
+ + + + }> + + + {isPlacementGroupsEnabled && ( + + )} + + + {isACLBEnabled && ( + + )} + + + + + - )} - - - {isACLBEnabled && ( - )} - - - - - - - - - - - - - - - {showDatabases && ( - - )} - {flags.selfServeBetas && ( - - )} - - - {/** We don't want to break any bookmarks. This can probably be removed eventually. */} - - - - + + + + + + + + {showDatabases && ( + + )} + {flags.selfServeBetas && ( + + )} + + + {/** We don't want to break any bookmarks. This can probably be removed eventually. */} + + + + + - -
-
-
-
- - + +
+ +