Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

upcoming: [M3-8840] - Add Cluster Type selection to LKE Create Cluster flow #11322

Merged
merged 12 commits into from
Dec 2, 2024
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;
/**
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added this prop to be able to control the placement of the tooltip - it was covering the helper text and we could avoid that by placing elsewhere.

* 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',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changes to use the same styling for both the cluster type and DC pricing docs links.

})(({ 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