Skip to content

Commit

Permalink
upcoming: [M3-8840] - Add Cluster Type selection to LKE Create Cluste…
Browse files Browse the repository at this point in the history
…r flow (#11322)

* Display new Cluster Type panel in create flow when LKE-E is enabled

* Disable LKE-E card without the account capability

* Require feature enablement to create LKE-E cluster

* Fix divider margin

* Change placement of tooltip

* Improve responsive styling

* Update useIsLkeEnterpriseEnabled hook test coverage

* Add test coverage to lke-create.spec.ts

* Add changeset

* Fix top margin of docs link

* Use the correct accountbeta query for the ClusterTypePanel

* clean up

---------

Co-authored-by: Hana Xu <hxu@akamai.com>
  • Loading branch information
mjac0bs and hana-akamai authored Dec 2, 2024
1 parent b39d3e7 commit 35fffca
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add Cluster Type section to Create Cluster flow for LKE-E ([#11322](https://github.com/linode/manager/pull/11322))
108 changes: 108 additions & 0 deletions packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
dcPricingDocsUrl,
} from 'support/constants/dc-specific-pricing';
import { mockGetLinodeTypes } from 'support/intercepts/linodes';
import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags';

/**
* Gets the label for an LKE plan as shown in creation plan table.
Expand Down Expand Up @@ -826,3 +827,110 @@ describe('LKE Cluster Creation with ACL', () => {
});
});
});

describe('LKE Cluster Creation with LKE-E', () => {
/**
* - Confirms LKE-E flow does not exist if account doesn't have the corresponding capability
* @todo LKE-E: Remove this test once LKE-E is fully rolled out
*/
it('does not show the LKE-E flow with the feature flag off', () => {
mockAppendFeatureFlags({
lkeEnterprise: { enabled: false, la: false },
}).as('getFeatureFlags');
cy.visitWithLogin('/kubernetes/clusters');

ui.button
.findByTitle('Create Cluster')
.should('be.visible')
.should('be.enabled')
.click();

cy.url().should('endWith', '/kubernetes/create');

cy.contains('Cluster Type').should('not.exist');
});

describe('shows the LKE-E flow with the feature flag on', () => {
beforeEach(() => {
// Mock feature flag -- @TODO LKE-E: Remove feature flag once LKE-E is fully rolled out
mockAppendFeatureFlags({
lkeEnterprise: { enabled: true, la: true },
}).as('getFeatureFlags');
});

/**
* - Mocks the LKE-E capability
* - Confirms the Cluster Type selection can be made
* - Confirms that HA is enabled by default with LKE-E selection
* @todo LKE-E: Add onto this test as the LKE-E changes to the Create flow are built out
*/
it('creates an LKE-E cluster with the account capability', () => {
mockGetAccount(
accountFactory.build({
capabilities: ['Kubernetes Enterprise'],
})
).as('getAccount');

cy.visitWithLogin('/kubernetes/clusters');

ui.button
.findByTitle('Create Cluster')
.should('be.visible')
.should('be.enabled')
.click();

cy.url().should('endWith', '/kubernetes/create');

cy.findByText('Cluster Type').should('be.visible');

// Confirm both cluster types exist and the LKE card is selected by default
cy.get(`[data-qa-select-card-heading="LKE"]`)
.closest('[data-qa-selection-card]')
.should('be.visible')
.should('have.attr', 'data-qa-selection-card-checked', 'true');

cy.get(`[data-qa-select-card-heading="LKE Enterprise"]`)
.closest('[data-qa-selection-card]')
.should('be.visible')
.should('have.attr', 'data-qa-selection-card-checked', 'false')
.click();

// Select LKE-E as the cluster type
cy.get(`[data-qa-select-card-heading="LKE Enterprise"]`)
.closest('[data-qa-selection-card]')
.should('be.visible')
.should('have.attr', 'data-qa-selection-card-checked', 'true');

// Confirm HA section is hidden since LKE-E includes HA by default
cy.findByText('HA Control Plane').should('not.exist');

// TODO: finish the rest of this test in subsequent PRs
});

it('disables the Cluster Type selection without the LKE-E account capability', () => {
cy.visitWithLogin('/kubernetes/clusters');

ui.button
.findByTitle('Create Cluster')
.should('be.visible')
.should('be.enabled')
.click();

cy.url().should('endWith', '/kubernetes/create');

// Confirm the Cluster Type selection can be made when the LKE-E feature is enabled
cy.findByText('Cluster Type').should('be.visible');

// Confirm both tiers exist and the LKE card is selected by default
cy.get(`[data-qa-select-card-heading="LKE"]`)
.closest('[data-qa-selection-card]')
.should('be.visible')
.should('have.attr', 'data-qa-selection-card-checked', 'true');

cy.get(`[data-qa-select-card-heading="LKE Enterprise"]`)
.closest('[data-qa-selection-card]')
.should('be.visible')
.should('have.attr', 'disabled');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as React from 'react';

import { CardBase } from './CardBase';

import type { TooltipProps } from '@linode/ui';
import type { SxProps, Theme } from '@mui/material/styles';

export interface SelectionCardProps {
Expand Down Expand Up @@ -87,6 +88,11 @@ export interface SelectionCardProps {
* Optional text to set in a tooltip when hovering over the card.
*/
tooltip?: JSX.Element | string;
/**
* The placement of the tooltip
* @default top
*/
tooltipPlacement?: TooltipProps['placement'];
}

/**
Expand Down Expand Up @@ -114,6 +120,7 @@ export const SelectionCard = React.memo((props: SelectionCardProps) => {
sxGrid,
sxTooltip,
tooltip,
tooltipPlacement = 'top',
} = props;

const handleKeyPress = (e: React.KeyboardEvent<HTMLElement>) => {
Expand Down Expand Up @@ -171,7 +178,7 @@ export const SelectionCard = React.memo((props: SelectionCardProps) => {
componentsProps={{
tooltip: { sx: sxTooltip },
}}
placement="top"
placement={tooltipPlacement}
title={tooltip}
>
{cardGrid}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Stack } from '@linode/ui';
import { Typography, useMediaQuery } from '@mui/material';
import React from 'react';

import { DocsLink } from 'src/components/DocsLink/DocsLink';
import { SelectionCard } from 'src/components/SelectionCard/SelectionCard';
import { useAccountBeta } from 'src/queries/account/account';

import { StyledDocsLinkContainer } from './CreateCluster.styles';

import type { KubernetesTier } from '@linode/api-v4';
import type { Theme } from '@mui/material/styles';

interface Props {
handleClusterTypeSelection: (tier: KubernetesTier) => void;
selectedTier: KubernetesTier;
}

export const ClusterTypePanel = (props: Props) => {
const { handleClusterTypeSelection, selectedTier } = props;

const { data: account } = useAccountBeta();

const mdDownBreakpoint = useMediaQuery((theme: Theme) =>
theme.breakpoints.down('md')
);
const smDownBreakpoint = useMediaQuery((theme: Theme) =>
theme.breakpoints.down('sm')
);

const isLkeEnterpriseSelectionDisabled = !account?.capabilities?.includes(
'Kubernetes Enterprise'
);

return (
<Stack>
<Stack flexDirection={mdDownBreakpoint ? 'column' : 'row'}>
<Stack>
<Typography variant="h3">Cluster Type</Typography>
<Typography sx={{ marginTop: 1, maxWidth: 700 }}>
Choose from a managed solution for smaller deployments or enterprise
grade clusters with enhanced ingress, networking, and security.
</Typography>
</Stack>
<StyledDocsLinkContainer>
<DocsLink href="/" label="Full Cluster Features" />
</StyledDocsLinkContainer>
</Stack>

<Stack
flexDirection={smDownBreakpoint ? 'column' : 'row'}
gap={2}
marginTop={2}
>
<SelectionCard
subheadings={[
'Up to 250 nodes, 1000 pods',
'Shared control plane',
'HA control plane (optional)',
]}
checked={selectedTier === 'standard'}
heading="LKE"
onClick={() => handleClusterTypeSelection('standard')}
/>
<SelectionCard
subheadings={[
'Up to 500 nodes, 5000 pods',
'Dedicated control plane',
'HA control plane (included)',
]}
tooltip={
isLkeEnterpriseSelectionDisabled
? 'LKE Enterprise is not currently enabled on this contract. To inquire, fill out the Cloud Computing Sales form or email sales@linode.com.'
: undefined
}
checked={selectedTier === 'enterprise'}
disabled={isLkeEnterpriseSelectionDisabled}
heading="LKE Enterprise"
onClick={() => handleClusterTypeSelection('enterprise')}
tooltipPlacement={smDownBreakpoint ? 'bottom' : 'right'}
/>
</Stack>
</Stack>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,10 @@ export const StyledFieldWithDocsStack = styled(Stack, {
}));

export const StyledDocsLinkContainer = styled(Box, {
label: 'StyledRegionSelectStack',
label: 'StyledDocsLinkContainer',
})(({ theme }) => ({
alignSelf: 'flex-start',
marginLeft: 'auto',
marginTop: theme.spacing(2),
[theme.breakpoints.down('md')]: {
marginLeft: 'unset',
marginTop: theme.spacing(2),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getKubeHighAvailability,
getLatestVersion,
useAPLAvailability,
useIsLkeEnterpriseEnabled,
} from 'src/features/Kubernetes/kubeUtils';
import { useAccount } from 'src/queries/account/account';
import {
Expand All @@ -38,13 +39,14 @@ import { extendType } from 'src/utilities/extendType';
import { filterCurrentTypes } from 'src/utilities/filterCurrentLinodeTypes';
import { stringToExtendedIP } from 'src/utilities/ipUtils';
import { plansNoticesUtils } from 'src/utilities/planNotices';
import { DOCS_LINK_LABEL_DC_PRICING } from 'src/utilities/pricing/constants';
import { UNKNOWN_PRICE } from 'src/utilities/pricing/constants';
import { DOCS_LINK_LABEL_DC_PRICING } from 'src/utilities/pricing/constants';
import { getDCSpecificPriceByType } from 'src/utilities/pricing/dynamicPricing';
import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2';

import KubeCheckoutBar from '../KubeCheckoutBar';
import { ApplicationPlatform } from './ApplicationPlatform';
import { ClusterTypePanel } from './ClusterTypePanel';
import { ControlPlaneACLPane } from './ControlPlaneACLPane';
import {
StyledDocsLinkContainer,
Expand All @@ -58,6 +60,7 @@ import type {
CreateKubeClusterPayload,
CreateNodePoolData,
KubeNodePoolResponse,
KubernetesTier,
} from '@linode/api-v4/lib/kubernetes';
import type { APIError } from '@linode/api-v4/lib/types';
import type { ExtendedIP } from 'src/utilities/ipUtils';
Expand Down Expand Up @@ -92,13 +95,23 @@ export const CreateCluster = () => {
const [ipV6Addr, setIPv6Addr] = React.useState<ExtendedIP[]>([
stringToExtendedIP(''),
]);
const [selectedTier, setSelectedTier] = React.useState<KubernetesTier>(
'standard'
);

const {
data: kubernetesHighAvailabilityTypesData,
isError: isErrorKubernetesTypes,
isLoading: isLoadingKubernetesTypes,
} = useKubernetesTypesQuery();

const handleClusterTypeSelection = (tier: KubernetesTier) => {
if (tier === 'enterprise') {
setHighAvailability(false);
}
setSelectedTier(tier);
};

const lkeHAType = kubernetesHighAvailabilityTypesData?.find(
(type) => type.id === 'lke-ha'
);
Expand All @@ -125,6 +138,11 @@ export const CreateCluster = () => {
isError: versionLoadError,
} = useKubernetesVersionQuery();

const {
isLkeEnterpriseLAFeatureEnabled,
isLkeEnterpriseLAFlagEnabled,
} = useIsLkeEnterpriseEnabled();

const versions = (versionData ?? []).map((thisVersion) => ({
label: thisVersion.id,
value: thisVersion.id,
Expand Down Expand Up @@ -195,9 +213,10 @@ export const CreateCluster = () => {
payload = { ...payload, apl_enabled };
}

const createClusterFn = showAPL
? createKubernetesClusterBeta
: createKubernetesCluster;
const createClusterFn =
showAPL || isLkeEnterpriseLAFeatureEnabled
? createKubernetesClusterBeta
: createKubernetesCluster;

createClusterFn(payload)
.then((cluster) => {
Expand Down Expand Up @@ -300,6 +319,15 @@ export const CreateCluster = () => {
label="Cluster Label"
value={label || ''}
/>
{isLkeEnterpriseLAFlagEnabled && (
<>
<Divider sx={{ marginBottom: 2, marginTop: 4 }} />
<ClusterTypePanel
handleClusterTypeSelection={handleClusterTypeSelection}
selectedTier={selectedTier}
/>
</>
)}
<Divider sx={{ marginTop: 4 }} />
<StyledFieldWithDocsStack>
<Stack>
Expand All @@ -316,7 +344,9 @@ export const CreateCluster = () => {
value={selectedRegionId}
/>
</Stack>
<StyledDocsLinkContainer>
<StyledDocsLinkContainer
sx={(theme) => ({ marginTop: theme.spacing(2) })}
>
<DocsLink
href="https://www.linode.com/pricing"
label={DOCS_LINK_LABEL_DC_PRICING}
Expand Down Expand Up @@ -349,7 +379,7 @@ export const CreateCluster = () => {
</>
)}
<Divider sx={{ marginTop: showAPL ? 1 : 4 }} />
{showHighAvailability && (
{showHighAvailability && selectedTier !== 'enterprise' && (
<Box data-testid="ha-control-plane">
<HAControlPlane
highAvailabilityPrice={
Expand All @@ -367,7 +397,7 @@ export const CreateCluster = () => {
)}
{showControlPlaneACL && (
<>
<Divider />
{selectedTier !== 'enterprise' && <Divider />}
<ControlPlaneACLPane
handleIPv4Change={(newIpV4Addr: ExtendedIP[]) => {
setIPv4Addr(newIpV4Addr);
Expand Down
Loading

0 comments on commit 35fffca

Please sign in to comment.