From 4c4b61343f4fa647aa35739f79d393b12d6b996b Mon Sep 17 00:00:00 2001 From: Sam Mans Date: Wed, 25 Sep 2024 11:56:32 -0400 Subject: [PATCH 1/6] feat: [UIE-8089] - DBaaS Resize --- packages/api-v4/src/databases/types.ts | 3 +- .../DatabaseCreate/DatabaseCreate.tsx | 2 +- .../DatabaseResize/DatabaseResize.test.tsx | 158 ++++++++++ .../DatabaseResize/DatabaseResize.tsx | 273 ++++++++++++++++-- .../src/features/Databases/utilities.ts | 4 + .../features/components/PlansPanel/types.ts | 6 +- 6 files changed, 424 insertions(+), 22 deletions(-) diff --git a/packages/api-v4/src/databases/types.ts b/packages/api-v4/src/databases/types.ts index feb7987fde2..686c0b50fbd 100644 --- a/packages/api-v4/src/databases/types.ts +++ b/packages/api-v4/src/databases/types.ts @@ -12,7 +12,7 @@ export interface DatabaseClusterSizeObject { price: DatabasePriceObject; } -type Engines = Record; +export type Engines = Record; export interface DatabaseType extends BaseType { class: DatabaseTypeClass; engines: Engines; @@ -197,6 +197,7 @@ export type Database = BaseDatabase & Partial; export interface UpdateDatabasePayload { + cluster_size?: number; label?: string; allow_list?: string[]; updates?: UpdatesSchedule; diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx index 3fbae61e3b9..fdc5aed1074 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx @@ -188,7 +188,7 @@ const getEngineOptions = (engines: DatabaseEngine[]) => { ); }; -interface NodePricing { +export interface NodePricing { double: DatabasePriceObject | undefined; multi: DatabasePriceObject | undefined; single: DatabasePriceObject | undefined; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.test.tsx index 0362763350c..034e85de12d 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.test.tsx @@ -133,6 +133,164 @@ describe('database resize', () => { }); }); + describe('on rendering of page ', () => { + describe('and isDatabasesGAEnabled is true', () => { + describe('and the Shared CPU tab is preselected', () => { + it('should render set node section', async () => { + const flags = { + dbaasV2: { + beta: false, + enabled: true, + }, + }; + const mockDatabase = databaseFactory.build({ + cluster_size: 3, + type: 'g6-nanode-1', + }); + const { getByTestId, getByText } = renderWithTheme( + , + { flags } + ); + + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + expect(getByText('Set Number of Nodes')).toBeDefined(); + expect( + getByText('Please select a plan or set the number of nodes.') + ).toBeDefined(); + }); + + it('should render the correct number of node radio buttons and associated costs', async () => { + const flags = { + dbaasV2: { + beta: false, + enabled: true, + }, + }; + const mockDatabase = databaseFactory.build({ + cluster_size: 3, + type: 'g6-nanode-1', + }); + const { getByTestId } = renderWithTheme( + , + { flags } + ); + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + const nodeRadioBtns = getByTestId('database-nodes'); + expect(nodeRadioBtns.children.length).toBe(2); + expect(nodeRadioBtns).toHaveTextContent('$60/month $0.09/hr'); + expect(nodeRadioBtns).toHaveTextContent('$140/month $0.21/hr'); + }); + + it('should preselect cluster size in Set Number of Nodes', async () => { + const flags = { + dbaasV2: { + beta: false, + enabled: true, + }, + }; + const mockDatabase = databaseFactory.build({ + cluster_size: 3, + type: 'g6-nanode-1', + }); + const { getByTestId } = renderWithTheme( + , + { flags } + ); + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + const selectedNodeRadioButton = getByTestId( + `database-node-${mockDatabase.cluster_size}` + ).children[0].children[0] as HTMLInputElement; + expect(selectedNodeRadioButton).toBeChecked(); + }); + + it('should disable visible lower node selections', async () => { + const flags = { + dbaasV2: { + beta: false, + enabled: true, + }, + }; + const mockDatabase = databaseFactory.build({ + cluster_size: 3, + type: 'g6-nanode-1', + }); + const { getByTestId } = renderWithTheme( + , + { flags } + ); + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + const selectedNodeRadioButton = getByTestId(`database-node-1`) + .children[0].children[0] as HTMLInputElement; + expect(selectedNodeRadioButton).toBeDisabled(); + }); + + it('should set price enable the resize button when a new number of nodes is selected', async () => { + const flags = { + dbaasV2: { + beta: false, + enabled: true, + }, + }; + const mockDatabase = databaseFactory.build({ + cluster_size: 1, + type: 'g6-nanode-1', + }); + const { getByTestId, getByText } = renderWithTheme( + , + { flags } + ); + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + // Mock clicking 3 Nodes option + const selectedNodeRadioButton = getByTestId(`database-node-3`) + .children[0].children[0] as HTMLInputElement; + fireEvent.click(selectedNodeRadioButton); + const resizeButton = getByText(/Resize Database Cluster/i).closest( + 'button' + ); + expect(resizeButton).toBeEnabled(); + + const expectedSummaryText = + 'Nanode 1 GB 3 Nodes: $140/month or $0.21/hour'; + const summary = getByTestId(`summary`); + expect(summary).toHaveTextContent(expectedSummaryText); + }); + + it('should disable the resize button if node selection is set back to current', async () => { + const flags = { + dbaasV2: { + beta: false, + enabled: true, + }, + }; + const mockDatabase = databaseFactory.build({ + cluster_size: 1, + type: 'g6-nanode-1', + }); + const { getByTestId, getByText } = renderWithTheme( + , + { flags } + ); + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + // Mock clicking 3 Nodes option + const threeNodesRadioButton = getByTestId(`database-node-3`) + .children[0].children[0] as HTMLInputElement; + fireEvent.click(threeNodesRadioButton); + const resizeButton = getByText(/Resize Database Cluster/i).closest( + 'button' + ); + expect(resizeButton).toBeEnabled(); + // Mock clicking 1 Node option + const oneNodeRadioButton = getByTestId(`database-node-1`).children[0] + .children[0] as HTMLInputElement; + fireEvent.click(oneNodeRadioButton); + expect(resizeButton).toBeDisabled(); + }); + }); + }); + }); + describe('should be disabled smaller plans', () => { const database = databaseFactory.build({ type: 'g6-dedicated-8', diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx index 79a566b77a7..1b8532cfc76 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx @@ -23,13 +23,35 @@ import { import { DatabaseResizeCurrentConfiguration } from './DatabaseResizeCurrentConfiguration'; import type { + ClusterSize, Database, DatabaseClusterSizeObject, DatabasePriceObject, DatabaseType, Engine, + UpdateDatabasePayload, } from '@linode/api-v4'; -import type { PlanSelectionType } from 'src/features/components/PlansPanel/types'; +import type { PlanSelectionWithDatabaseType } from 'src/features/components/PlansPanel/types'; +import { determineInitialPlanCategoryTab } from 'src/features/components/PlansPanel/utils'; +import { NodePricing } from '../../DatabaseCreate/DatabaseCreate'; +import { useIsDatabasesEnabled } from '../../utilities'; +import { FormControlLabel } from 'src/components/FormControlLabel'; +import { Radio } from 'src/components/Radio/Radio'; +import { Divider } from 'src/components/Divider'; +import { RadioGroup } from 'src/components/RadioGroup'; +import { StyledChip } from 'src/features/components/PlansPanel/PlanSelection.styles'; +import { makeStyles } from 'tss-react/mui'; +import { Theme } from '@mui/material/styles'; + +const useStyles = makeStyles()((theme: Theme) => ({ + formControlLabel: { + marginBottom: theme.spacing(), + }, + disabledOptionLabel: { + color: + theme.palette.mode === 'dark' ? theme.color.grey6 : theme.color.grey1, + }, +})); interface Props { database: Database; @@ -37,6 +59,7 @@ interface Props { } export const DatabaseResize = ({ database, disabled = false }: Props) => { + const { classes } = useStyles(); const history = useHistory(); const [planSelected, setPlanSelected] = React.useState(); @@ -45,6 +68,7 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { plan: string; price: string; }>(); + const [nodePricing, setNodePricing] = React.useState(); // This will be set to `false` once one of the configuration is selected from available plan. This is used to disable the // "Resize" button unless there have been changes to the form. const [ @@ -57,6 +81,15 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { setIsResizeConfirmationDialogOpen, ] = React.useState(false); + const [selectedTab, setSelectedTab] = React.useState(0); + const { + isDatabasesV2Enabled, + isDatabasesGAEnabled, + } = useIsDatabasesEnabled(); + const [clusterSize, setClusterSize] = React.useState( + database.cluster_size + ); + const { error: resizeError, isPending: submitInProgress, @@ -72,9 +105,17 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { const { enqueueSnackbar } = useSnackbar(); const onResize = () => { - updateDatabase({ - type: planSelected, - }).then(() => { + const payload: UpdateDatabasePayload = {}; + + if (clusterSize > database.cluster_size && isDatabasesGAEnabled) { + payload.cluster_size = clusterSize; + } + + if (planSelected) { + payload.type = planSelected; + } + + updateDatabase(payload).then(() => { enqueueSnackbar(`Database cluster ${database.label} is being resized.`, { variant: 'info', }); @@ -82,6 +123,10 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { }); }; + const handleTabChange = (index: number) => { + setSelectedTab(index); + }; + const resizeDescription = ( <> Resize a Database Cluster @@ -107,6 +152,8 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { {summaryText.numberOfNodes} Node {summaryText.numberOfNodes > 1 ? 's' : ''}: {summaryText.price} + ) : isDatabasesGAEnabled ? ( + 'Please select a plan or set the number of nodes.' ) : ( 'Please select a plan.' )} @@ -136,14 +183,15 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { ); - React.useEffect(() => { - if (!planSelected || !dbTypes) { - return; - } - + const setSummaryAndPrices = ( + databaseTypeId: string, + engine: Engine, + dbTypes: DatabaseType[] + ) => { const selectedPlanType = dbTypes.find( - (type: DatabaseType) => type.id === planSelected + (type: DatabaseType) => type.id === databaseTypeId ); + if (!selectedPlanType) { setPlanSelected(undefined); setSummaryText(undefined); @@ -151,30 +199,67 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { return; } - const engineType = database.engine.split('/')[0] as Engine; - const price = selectedPlanType.engines[engineType].find( - (cluster: DatabaseClusterSizeObject) => - cluster.quantity === database.cluster_size + const price = selectedPlanType.engines[engine].find( + (cluster: DatabaseClusterSizeObject) => cluster.quantity === clusterSize )?.price as DatabasePriceObject; - setShouldSubmitBeDisabled(false); - setSummaryText({ - numberOfNodes: database.cluster_size, + numberOfNodes: clusterSize, plan: formatStorageUnits(selectedPlanType.label), price: `$${price?.monthly}/month or $${price?.hourly}/hour`, }); + + const nodePricingDetails = { + double: selectedPlanType.engines[engine]?.find( + (cluster: DatabaseClusterSizeObject) => cluster.quantity === 2 + )?.price, + multi: selectedPlanType.engines[engine]?.find( + (cluster: DatabaseClusterSizeObject) => cluster.quantity === 3 + )?.price, + single: selectedPlanType.engines[engine]?.find( + (cluster: DatabaseClusterSizeObject) => cluster.quantity === 1 + )?.price, + }; + + setNodePricing(nodePricingDetails); + setShouldSubmitBeDisabled(false); + return; + }; + + React.useEffect(() => { + const nodeSelected = clusterSize > database.cluster_size; + if (!dbTypes) { + return; + } + // Set default message and disable submit when no new selection is made + if (!nodeSelected && !planSelected) { + setShouldSubmitBeDisabled(true); + setSummaryText(undefined); + return; + } + // When only a higher node selection is made and plan has not changed + if (isDatabasesGAEnabled && nodeSelected && !planSelected) { + setSummaryAndPrices(database.type, database.engine, dbTypes); + } + + if (!planSelected) { + return; + } + // When a new plan is selected + const engineType = database.engine.split('/')[0] as Engine; + setSummaryAndPrices(planSelected, engineType, dbTypes); }, [ dbTypes, database.engine, database.type, planSelected, database.cluster_size, + clusterSize, ]); const selectedEngine = database.engine.split('/')[0] as Engine; - const displayTypes: PlanSelectionType[] = React.useMemo(() => { + const displayTypes: PlanSelectionWithDatabaseType[] = React.useMemo(() => { if (!dbTypes) { return []; } @@ -213,6 +298,122 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { const isDisabledSharedTab = database.cluster_size === 2; + React.useEffect(() => { + const initialTab = determineInitialPlanCategoryTab( + displayTypes, + planSelected, + currentPlan?.heading + ); + setSelectedTab(initialTab); + + if (isDatabasesGAEnabled) { + const engineType = database.engine.split('/')[0] as Engine; + const nodePricingDetails = { + double: currentPlan?.engines[engineType]?.find( + (cluster: DatabaseClusterSizeObject) => cluster.quantity === 2 + )?.price, + multi: currentPlan?.engines[engineType]?.find( + (cluster: DatabaseClusterSizeObject) => cluster.quantity === 3 + )?.price, + single: currentPlan?.engines[engineType]?.find( + (cluster: DatabaseClusterSizeObject) => cluster.quantity === 1 + )?.price, + }; + setNodePricing(nodePricingDetails); + } + }, [dbTypes, displayTypes]); + + const handleNodeChange = ( + event: React.ChangeEvent + ): void => { + const size = Number(event.currentTarget.value) as ClusterSize; + setClusterSize(size); + }; + + const handlePlanSelect = (selected: string) => { + setPlanSelected(selected); + }; + + const nodeOptions = React.useMemo(() => { + const hasDedicated = displayTypes.some( + (type) => type.class === 'dedicated' + ); + + const currentChip = ( + + ); + + const isDisabled = (nodeSize: ClusterSize) => { + return nodeSize < database.cluster_size; + }; + + const options = [ + { + label: ( + + 1 Node {` `} + {database.cluster_size === 1 && currentChip} +
+ + {`$${nodePricing?.single?.monthly || 0}/month $${ + nodePricing?.single?.hourly || 0 + }/hr`} + +
+ ), + value: 1, + }, + ]; + + if (hasDedicated && selectedTab === 0 && isDatabasesV2Enabled) { + options.push({ + label: ( + + 2 Nodes - High Availability + {database.cluster_size === 2 && currentChip} +
+ + {`$${nodePricing?.double?.monthly || 0}/month $${ + nodePricing?.double?.hourly || 0 + }/hr`} + +
+ ), + value: 2, + }); + } + + options.push({ + label: ( + + 3 Nodes - High Availability (recommended) + {database.cluster_size === 3 && currentChip} +
+ + {`$${nodePricing?.multi?.monthly || 0}/month $${ + nodePricing?.multi?.hourly || 0 + }/hr`} + +
+ ), + value: 3, + }); + + return options; + }, [selectedTab, nodePricing, displayTypes, isDatabasesV2Enabled]); + if (typesLoading) { return ; } @@ -236,11 +437,45 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { disabledSmallerPlans={disabledPlans} disabledTabs={isDisabledSharedTab ? ['shared'] : []} header="Choose a Plan" - onSelect={(selected: string) => setPlanSelected(selected)} + onSelect={handlePlanSelect} + handleTabChange={handleTabChange} selectedId={planSelected} tabDisabledMessage="Resizing a 2-nodes cluster is only allowed with Dedicated plans." types={displayTypes} /> + {isDatabasesGAEnabled && ( + <> + + + + Set Number of Nodes{' '} + + + We recommend 3 nodes in a database cluster to avoid downtime + during upgrades and maintenance. + + + + {nodeOptions.map((nodeOption) => ( + } + data-testid={`database-node-${nodeOption.value}`} + data-qa-radio={nodeOption.label} + key={nodeOption.value} + label={nodeOption.label} + value={nodeOption.value} + disabled={nodeOption.value < database.cluster_size} + /> + ))} + + + )} {summaryPanel} diff --git a/packages/manager/src/features/Databases/utilities.ts b/packages/manager/src/features/Databases/utilities.ts index 7272b939185..2842a0d96a5 100644 --- a/packages/manager/src/features/Databases/utilities.ts +++ b/packages/manager/src/features/Databases/utilities.ts @@ -60,8 +60,12 @@ export const useIsDatabasesEnabled = () => { account?.capabilities ?? [] ); + const isDatabasesGA = + flags.dbaasV2?.enabled && flags.dbaasV2.beta === false; + return { isDatabasesEnabled: isDatabasesV1Enabled || isDatabasesV2Enabled, + isDatabasesGAEnabled: isDatabasesV1Enabled && isDatabasesGA, isDatabasesV1Enabled, isDatabasesV2Beta: isDatabasesV2Enabled && flags.dbaasV2?.beta, isDatabasesV2Enabled, diff --git a/packages/manager/src/features/components/PlansPanel/types.ts b/packages/manager/src/features/components/PlansPanel/types.ts index f29ef849c0a..6e96161c110 100644 --- a/packages/manager/src/features/components/PlansPanel/types.ts +++ b/packages/manager/src/features/components/PlansPanel/types.ts @@ -1,6 +1,10 @@ -import type { BaseType, RegionPriceObject } from '@linode/api-v4'; +import type { BaseType, Engines, RegionPriceObject } from '@linode/api-v4'; import type { ExtendedType } from 'src/utilities/extendType'; +export interface PlanSelectionWithDatabaseType extends PlanSelectionType { + engines: Engines; +} + export interface PlanSelectionType extends BaseType { class: ExtendedType['class']; formattedLabel: ExtendedType['formattedLabel']; From ad3481d2dcbfa2803ee7716e60a0c414e9b33e36 Mon Sep 17 00:00:00 2001 From: Sam Mans Date: Wed, 2 Oct 2024 19:18:19 -0400 Subject: [PATCH 2/6] Added changeset: Number of Nodes selector for DBaaS GA Resize --- packages/manager/.changeset/pr-11040-added-1727911099315.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-11040-added-1727911099315.md diff --git a/packages/manager/.changeset/pr-11040-added-1727911099315.md b/packages/manager/.changeset/pr-11040-added-1727911099315.md new file mode 100644 index 00000000000..188deb77757 --- /dev/null +++ b/packages/manager/.changeset/pr-11040-added-1727911099315.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +Number of Nodes selector for DBaaS GA Resize ([#11040](https://github.com/linode/manager/pull/11040)) From f058c7730d3c7cf18372df28a20f810caa961f20 Mon Sep 17 00:00:00 2001 From: Sam Mans Date: Wed, 2 Oct 2024 19:23:02 -0400 Subject: [PATCH 3/6] Added changeset: Databases types to have UpdateDatabasePayload include cluster_size and export the Engines type --- packages/api-v4/.changeset/pr-11040-changed-1727911382574.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/api-v4/.changeset/pr-11040-changed-1727911382574.md diff --git a/packages/api-v4/.changeset/pr-11040-changed-1727911382574.md b/packages/api-v4/.changeset/pr-11040-changed-1727911382574.md new file mode 100644 index 00000000000..de7ef295625 --- /dev/null +++ b/packages/api-v4/.changeset/pr-11040-changed-1727911382574.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Changed +--- + +Databases types to have UpdateDatabasePayload include cluster_size and export the Engines type ([#11040](https://github.com/linode/manager/pull/11040)) From 21f23eca9be0a8c37cbc7ca401520578ee1e9748 Mon Sep 17 00:00:00 2001 From: Sam Mans Date: Fri, 4 Oct 2024 11:19:21 -0400 Subject: [PATCH 4/6] Applying requested selection behavior --- .../DatabaseResize/DatabaseResize.tsx | 97 ++++++++++++------- 1 file changed, 63 insertions(+), 34 deletions(-) diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx index 1b8532cfc76..9f5c01ce74d 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx @@ -62,13 +62,17 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { const { classes } = useStyles(); const history = useHistory(); - const [planSelected, setPlanSelected] = React.useState(); + const [planSelected, setPlanSelected] = React.useState( + database.type + ); const [summaryText, setSummaryText] = React.useState<{ - numberOfNodes: number; + numberOfNodes: ClusterSize; plan: string; price: string; }>(); - const [nodePricing, setNodePricing] = React.useState(); + const [nodePricing, setNodePricing] = React.useState< + NodePricing | undefined + >(); // This will be set to `false` once one of the configuration is selected from available plan. This is used to disable the // "Resize" button unless there have been changes to the form. const [ @@ -86,7 +90,7 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { isDatabasesV2Enabled, isDatabasesGAEnabled, } = useIsDatabasesEnabled(); - const [clusterSize, setClusterSize] = React.useState( + const [clusterSize, setClusterSize] = React.useState( database.cluster_size ); @@ -107,7 +111,11 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { const onResize = () => { const payload: UpdateDatabasePayload = {}; - if (clusterSize > database.cluster_size && isDatabasesGAEnabled) { + if ( + clusterSize && + clusterSize > database.cluster_size && + isDatabasesGAEnabled + ) { payload.cluster_size = clusterSize; } @@ -123,10 +131,6 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { }); }; - const handleTabChange = (index: number) => { - setSelectedTab(index); - }; - const resizeDescription = ( <> Resize a Database Cluster @@ -191,9 +195,26 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { const selectedPlanType = dbTypes.find( (type: DatabaseType) => type.id === databaseTypeId ); - - if (!selectedPlanType) { + if (selectedPlanType) { + // When plan is found, set node pricing + const nodePricingDetails = { + double: selectedPlanType.engines[engine]?.find( + (cluster: DatabaseClusterSizeObject) => cluster.quantity === 2 + )?.price, + multi: selectedPlanType.engines[engine]?.find( + (cluster: DatabaseClusterSizeObject) => cluster.quantity === 3 + )?.price, + single: selectedPlanType.engines[engine]?.find( + (cluster: DatabaseClusterSizeObject) => cluster.quantity === 1 + )?.price, + }; + setNodePricing(nodePricingDetails); + } else { + // If plan is not found, clear plan selection setPlanSelected(undefined); + } + + if (!selectedPlanType || !clusterSize) { setSummaryText(undefined); setShouldSubmitBeDisabled(true); return; @@ -209,40 +230,28 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { price: `$${price?.monthly}/month or $${price?.hourly}/hour`, }); - const nodePricingDetails = { - double: selectedPlanType.engines[engine]?.find( - (cluster: DatabaseClusterSizeObject) => cluster.quantity === 2 - )?.price, - multi: selectedPlanType.engines[engine]?.find( - (cluster: DatabaseClusterSizeObject) => cluster.quantity === 3 - )?.price, - single: selectedPlanType.engines[engine]?.find( - (cluster: DatabaseClusterSizeObject) => cluster.quantity === 1 - )?.price, - }; - - setNodePricing(nodePricingDetails); setShouldSubmitBeDisabled(false); return; }; React.useEffect(() => { - const nodeSelected = clusterSize > database.cluster_size; + const nodeSelected = clusterSize && clusterSize > database.cluster_size; + const isSamePlanSelected = planSelected === database.type; if (!dbTypes) { return; } // Set default message and disable submit when no new selection is made - if (!nodeSelected && !planSelected) { + if (!nodeSelected && (!planSelected || isSamePlanSelected)) { setShouldSubmitBeDisabled(true); setSummaryText(undefined); return; } - // When only a higher node selection is made and plan has not changed - if (isDatabasesGAEnabled && nodeSelected && !planSelected) { + // When only a higher node selection is made and plan has not been changed + if (isDatabasesGAEnabled && nodeSelected && isSamePlanSelected) { setSummaryAndPrices(database.type, database.engine, dbTypes); } - - if (!planSelected) { + // No plan selection or plan selection is unchanged + if (!planSelected || isSamePlanSelected) { return; } // When a new plan is selected @@ -327,11 +336,31 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { event: React.ChangeEvent ): void => { const size = Number(event.currentTarget.value) as ClusterSize; + const selectedPlanTab = determineInitialPlanCategoryTab( + displayTypes, + planSelected + ); + // If 2 Nodes is selected for an incompatible plan, clear selected plan and related information + if (size === 2 && selectedPlanTab !== 0) { + setNodePricing(undefined); + setPlanSelected(undefined); + setSummaryText(undefined); + } setClusterSize(size); }; - const handlePlanSelect = (selected: string) => { - setPlanSelected(selected); + const handleTabChange = (index: number) => { + if (selectedTab === index) { + return; + } + // Clear plan and related info when when 2 nodes option is selected for incompatible plan. + if (isDatabasesGAEnabled && selectedTab === 0 && clusterSize === 2) { + setClusterSize(undefined); + setPlanSelected(undefined); + setNodePricing(undefined); + setSummaryText(undefined); + } + setSelectedTab(index); }; const nodeOptions = React.useMemo(() => { @@ -437,7 +466,7 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { disabledSmallerPlans={disabledPlans} disabledTabs={isDisabledSharedTab ? ['shared'] : []} header="Choose a Plan" - onSelect={handlePlanSelect} + onSelect={(selected: string) => setPlanSelected(selected)} handleTabChange={handleTabChange} selectedId={planSelected} tabDisabledMessage="Resizing a 2-nodes cluster is only allowed with Dedicated plans." @@ -457,7 +486,7 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { From d4fc3d85565d1cf2c1fff770dcd07a0ebf6c5ae4 Mon Sep 17 00:00:00 2001 From: Sam Mans Date: Fri, 4 Oct 2024 17:56:32 -0400 Subject: [PATCH 5/6] Applying design changes to summary and updating unit tests --- .../DatabaseResize/DatabaseResize.test.tsx | 377 +++++++++++------- .../DatabaseResize/DatabaseResize.tsx | 103 ++++- 2 files changed, 321 insertions(+), 159 deletions(-) diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.test.tsx index 034e85de12d..2141221dadb 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.test.tsx @@ -133,162 +133,249 @@ describe('database resize', () => { }); }); - describe('on rendering of page ', () => { - describe('and isDatabasesGAEnabled is true', () => { - describe('and the Shared CPU tab is preselected', () => { - it('should render set node section', async () => { - const flags = { - dbaasV2: { - beta: false, - enabled: true, - }, - }; - const mockDatabase = databaseFactory.build({ - cluster_size: 3, - type: 'g6-nanode-1', - }); - const { getByTestId, getByText } = renderWithTheme( - , - { flags } - ); + describe('on rendering of page and isDatabasesGAEnabled is true and the Shared CPU tab is preselected ', () => { + it('should render set node section', async () => { + const flags = { + dbaasV2: { + beta: false, + enabled: true, + }, + }; + const mockDatabase = databaseFactory.build({ + cluster_size: 3, + type: 'g6-nanode-1', + }); + const { getByTestId, getByText } = renderWithTheme( + , + { flags } + ); - expect(getByTestId(loadingTestId)).toBeInTheDocument(); - await waitForElementToBeRemoved(getByTestId(loadingTestId)); + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + await waitForElementToBeRemoved(getByTestId(loadingTestId)); - expect(getByText('Set Number of Nodes')).toBeDefined(); - expect( - getByText('Please select a plan or set the number of nodes.') - ).toBeDefined(); - }); + expect(getByText('Set Number of Nodes')).toBeDefined(); + expect( + getByText('Please select a plan or set the number of nodes.') + ).toBeDefined(); + }); - it('should render the correct number of node radio buttons and associated costs', async () => { - const flags = { - dbaasV2: { - beta: false, - enabled: true, - }, - }; - const mockDatabase = databaseFactory.build({ - cluster_size: 3, - type: 'g6-nanode-1', - }); - const { getByTestId } = renderWithTheme( - , - { flags } - ); - await waitForElementToBeRemoved(getByTestId(loadingTestId)); - const nodeRadioBtns = getByTestId('database-nodes'); - expect(nodeRadioBtns.children.length).toBe(2); - expect(nodeRadioBtns).toHaveTextContent('$60/month $0.09/hr'); - expect(nodeRadioBtns).toHaveTextContent('$140/month $0.21/hr'); - }); + it('should render the correct number of node radio buttons, associated costs, and summary', async () => { + const flags = { + dbaasV2: { + beta: false, + enabled: true, + }, + }; + const mockDatabase = databaseFactory.build({ + cluster_size: 3, + type: 'g6-nanode-1', + }); + const { getByTestId } = renderWithTheme( + , + { flags } + ); + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + const nodeRadioBtns = getByTestId('database-nodes'); + expect(nodeRadioBtns.children.length).toBe(2); + expect(nodeRadioBtns).toHaveTextContent('$60/month $0.09/hr'); + expect(nodeRadioBtns).toHaveTextContent('$140/month $0.21/hr'); - it('should preselect cluster size in Set Number of Nodes', async () => { - const flags = { - dbaasV2: { - beta: false, - enabled: true, - }, - }; - const mockDatabase = databaseFactory.build({ - cluster_size: 3, - type: 'g6-nanode-1', - }); - const { getByTestId } = renderWithTheme( - , - { flags } - ); - await waitForElementToBeRemoved(getByTestId(loadingTestId)); - const selectedNodeRadioButton = getByTestId( - `database-node-${mockDatabase.cluster_size}` - ).children[0].children[0] as HTMLInputElement; - expect(selectedNodeRadioButton).toBeChecked(); - }); + const expectedCurrentSummary = + 'Current Cluster: Nanode 1 GB $60/month 3 Nodes - HA $140/month'; + const currentSummary = getByTestId('currentSummary'); + expect(currentSummary).toHaveTextContent(expectedCurrentSummary); - it('should disable visible lower node selections', async () => { - const flags = { - dbaasV2: { - beta: false, - enabled: true, - }, - }; - const mockDatabase = databaseFactory.build({ - cluster_size: 3, - type: 'g6-nanode-1', - }); - const { getByTestId } = renderWithTheme( - , - { flags } - ); - await waitForElementToBeRemoved(getByTestId(loadingTestId)); - const selectedNodeRadioButton = getByTestId(`database-node-1`) - .children[0].children[0] as HTMLInputElement; - expect(selectedNodeRadioButton).toBeDisabled(); - }); + const expectedResizeSummary = + 'Resized Cluster: Please select a plan or set the number of nodes.'; + const resizeSummary = getByTestId('resizeSummary'); + expect(resizeSummary).toHaveTextContent(expectedResizeSummary); + }); - it('should set price enable the resize button when a new number of nodes is selected', async () => { - const flags = { - dbaasV2: { - beta: false, - enabled: true, - }, - }; - const mockDatabase = databaseFactory.build({ - cluster_size: 1, - type: 'g6-nanode-1', - }); - const { getByTestId, getByText } = renderWithTheme( - , - { flags } - ); - await waitForElementToBeRemoved(getByTestId(loadingTestId)); - // Mock clicking 3 Nodes option - const selectedNodeRadioButton = getByTestId(`database-node-3`) - .children[0].children[0] as HTMLInputElement; - fireEvent.click(selectedNodeRadioButton); - const resizeButton = getByText(/Resize Database Cluster/i).closest( - 'button' - ); - expect(resizeButton).toBeEnabled(); + it('should preselect cluster size in Set Number of Nodes', async () => { + const flags = { + dbaasV2: { + beta: false, + enabled: true, + }, + }; + const mockDatabase = databaseFactory.build({ + cluster_size: 3, + type: 'g6-nanode-1', + }); + const { getByTestId } = renderWithTheme( + , + { flags } + ); + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + const selectedNodeRadioButton = getByTestId( + `database-node-${mockDatabase.cluster_size}` + ).children[0].children[0] as HTMLInputElement; + expect(selectedNodeRadioButton).toBeChecked(); + }); - const expectedSummaryText = - 'Nanode 1 GB 3 Nodes: $140/month or $0.21/hour'; - const summary = getByTestId(`summary`); - expect(summary).toHaveTextContent(expectedSummaryText); - }); + it('should disable visible lower node selections', async () => { + const flags = { + dbaasV2: { + beta: false, + enabled: true, + }, + }; + const mockDatabase = databaseFactory.build({ + cluster_size: 3, + type: 'g6-nanode-1', + }); + const { getByTestId } = renderWithTheme( + , + { flags } + ); + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + const selectedNodeRadioButton = getByTestId('database-node-1').children[0] + .children[0] as HTMLInputElement; + expect(selectedNodeRadioButton).toBeDisabled(); + }); - it('should disable the resize button if node selection is set back to current', async () => { - const flags = { - dbaasV2: { - beta: false, - enabled: true, - }, - }; - const mockDatabase = databaseFactory.build({ - cluster_size: 1, - type: 'g6-nanode-1', - }); - const { getByTestId, getByText } = renderWithTheme( - , - { flags } - ); - await waitForElementToBeRemoved(getByTestId(loadingTestId)); - // Mock clicking 3 Nodes option - const threeNodesRadioButton = getByTestId(`database-node-3`) - .children[0].children[0] as HTMLInputElement; - fireEvent.click(threeNodesRadioButton); - const resizeButton = getByText(/Resize Database Cluster/i).closest( - 'button' - ); - expect(resizeButton).toBeEnabled(); - // Mock clicking 1 Node option - const oneNodeRadioButton = getByTestId(`database-node-1`).children[0] - .children[0] as HTMLInputElement; - fireEvent.click(oneNodeRadioButton); - expect(resizeButton).toBeDisabled(); + it('should set price, enable resize button, and update resize summary when a new number of nodes is selected', async () => { + const flags = { + dbaasV2: { + beta: false, + enabled: true, + }, + }; + const mockDatabase = databaseFactory.build({ + cluster_size: 1, + type: 'g6-nanode-1', + }); + const { getByTestId, getByText } = renderWithTheme( + , + { flags } + ); + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + // Mock clicking 3 Nodes option + const selectedNodeRadioButton = getByTestId('database-node-3').children[0] + .children[0] as HTMLInputElement; + fireEvent.click(selectedNodeRadioButton); + const resizeButton = getByText(/Resize Database Cluster/i).closest( + 'button' + ) as HTMLButtonElement; + expect(resizeButton.disabled).toBeFalsy(); + + const expectedSummaryText = + 'Resized Cluster: Nanode 1 GB $60/month 3 Nodes - HA $140/month'; + const summary = getByTestId('resizeSummary'); + expect(summary).toHaveTextContent(expectedSummaryText); + }); + + it('should disable the resize button if node selection is set back to current', async () => { + const flags = { + dbaasV2: { + beta: false, + enabled: true, + }, + }; + const mockDatabase = databaseFactory.build({ + cluster_size: 1, + type: 'g6-nanode-1', + }); + const { getByTestId, getByText } = renderWithTheme( + , + { flags } + ); + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + // Mock clicking 3 Nodes option + const threeNodesRadioButton = getByTestId('database-node-3').children[0] + .children[0] as HTMLInputElement; + fireEvent.click(threeNodesRadioButton); + const resizeButton = getByText(/Resize Database Cluster/i).closest( + 'button' + ); + expect(resizeButton).toBeEnabled(); + // Mock clicking 1 Node option + const oneNodeRadioButton = getByTestId('database-node-1').children[0] + .children[0] as HTMLInputElement; + fireEvent.click(oneNodeRadioButton); + expect(resizeButton).toBeDisabled(); + }); + + describe('on rendering of page and isDatabasesGAEnabled is true and the Dedicated CPU tab is preselected', () => { + beforeEach(() => { + // Mock database types + const mockDedicatedTypes = [ + databaseTypeFactory.build({ + class: 'dedicated', + disk: 81920, + id: 'g6-dedicated-2', + label: 'Dedicated 4 GB', + memory: 4096, + }), + databaseTypeFactory.build({ + class: 'dedicated', + disk: 163840, + id: 'g6-dedicated-4', + label: 'Dedicated 8 GB', + memory: 8192, + }), + ]; + server.use( + http.get('*/databases/types', () => { + return HttpResponse.json(makeResourcePage([...mockDedicatedTypes])); + }) + ); + }); + it('should render node selection for dedicated tab with default summary', async () => { + const mockDatabase = databaseFactory.build({ + type: 'g6-dedicated-2', + cluster_size: 3, }); + + const flags = { + dbaasV2: { + beta: false, + enabled: true, + }, + }; + + const { getByTestId } = renderWithTheme( + , + { flags } + ); + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + expect(getByTestId('database-nodes')).toBeDefined(); + expect(getByTestId('database-node-1')).toBeDefined(); + expect(getByTestId('database-node-2')).toBeDefined(); + expect(getByTestId('database-node-3')).toBeDefined(); }); }); + + it('should disable lower node selections', async () => { + const mockDatabase = databaseFactory.build({ + type: 'g6-dedicated-2', + cluster_size: 3, + }); + + const flags = { + dbaasV2: { + beta: false, + enabled: true, + }, + }; + + const { getByTestId } = renderWithTheme( + , + { flags } + ); + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + expect( + getByTestId('database-node-1').children[0].children[0] + ).toBeDisabled(); + expect( + getByTestId('database-node-2').children[0].children[0] + ).toBeDisabled(); + expect( + getByTestId('database-node-3').children[0].children[0] + ).toBeEnabled(); + }); }); describe('should be disabled smaller plans', () => { diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx index 9f5c01ce74d..01f12089124 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx @@ -51,6 +51,16 @@ const useStyles = makeStyles()((theme: Theme) => ({ color: theme.palette.mode === 'dark' ? theme.color.grey6 : theme.color.grey1, }, + summarySpanBorder: { + borderRight: `1px solid ${theme.borderColors.borderTypography}`, + color: theme.textColors.tableStatic, + paddingRight: '10px', + marginRight: '10px;', + marginLeft: '10px;', + }, + nodeSpanSpacing: { + marginRight: '10px', + }, })); interface Props { @@ -69,6 +79,7 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { numberOfNodes: ClusterSize; plan: string; price: string; + basePrice: string; }>(); const [nodePricing, setNodePricing] = React.useState< NodePricing | undefined @@ -141,23 +152,45 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { ); - const summaryPanel = ( + const resizeSummary = ( <> - Summary ({ marginTop: theme.spacing(2), })} - data-testid="summary" + data-testid="resizeSummary" > {summaryText ? ( <> - {summaryText.plan}{' '} - {summaryText.numberOfNodes} Node - {summaryText.numberOfNodes > 1 ? 's' : ''}: {summaryText.price} + + {isDatabasesGAEnabled + ? 'Resized Cluster: ' + summaryText.plan + : summaryText.plan} + {' '} + {isDatabasesGAEnabled ? ( + + {summaryText.basePrice} + + ) : null} + + {' '} + {summaryText.numberOfNodes} Node + {summaryText.numberOfNodes > 1 ? 's' : ''} + {!isDatabasesGAEnabled ? ': ' : ' - HA '} + + {summaryText.price} ) : isDatabasesGAEnabled ? ( - 'Please select a plan or set the number of nodes.' + <> + Resized Cluster:{' '} + Please select a plan or set the number of nodes. + ) : ( 'Please select a plan.' )} @@ -223,11 +256,17 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { const price = selectedPlanType.engines[engine].find( (cluster: DatabaseClusterSizeObject) => cluster.quantity === clusterSize )?.price as DatabasePriceObject; + const resizeBasePrice = selectedPlanType.engines[engine][0] + .price as DatabasePriceObject; + const currentPlanPrice = `$${resizeBasePrice?.monthly}/month`; setSummaryText({ numberOfNodes: clusterSize, plan: formatStorageUnits(selectedPlanType.label), - price: `$${price?.monthly}/month or $${price?.hourly}/hour`, + price: isDatabasesGAEnabled + ? `$${price?.monthly}/month` + : `$${price?.monthly}/month or $${price?.hourly}/hour`, + basePrice: currentPlanPrice, }); setShouldSubmitBeDisabled(false); @@ -246,16 +285,16 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { setSummaryText(undefined); return; } + const engineType = database.engine.split('/')[0] as Engine; // When only a higher node selection is made and plan has not been changed if (isDatabasesGAEnabled && nodeSelected && isSamePlanSelected) { - setSummaryAndPrices(database.type, database.engine, dbTypes); + setSummaryAndPrices(database.type, engineType, dbTypes); } // No plan selection or plan selection is unchanged if (!planSelected || isSamePlanSelected) { return; } // When a new plan is selected - const engineType = database.engine.split('/')[0] as Engine; setSummaryAndPrices(planSelected, engineType, dbTypes); }, [ dbTypes, @@ -304,6 +343,31 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { ? type.disk < currentPlanDisk : type.disk <= currentPlanDisk ); + const currentEngine = database.engine.split('/')[0] as Engine; + const currentPrice = currentPlan?.engines[currentEngine].find( + (cluster: DatabaseClusterSizeObject) => + cluster.quantity === database.cluster_size + )?.price as DatabasePriceObject; + const currentBasePrice = currentPlan?.engines[currentEngine][0] + .price as DatabasePriceObject; + const currentNodePrice = `$${currentPrice?.monthly}/month`; + const currentPlanPrice = `$${currentBasePrice?.monthly}/month`; + const currentSummary = ( + + + Current Cluster: {currentPlan?.heading} + {' '} + + {currentPlanPrice} + + + {' '} + {database.cluster_size} Node + {database.cluster_size > 1 ? 's - HA ' : ' - HA '} + + {currentNodePrice} + + ); const isDisabledSharedTab = database.cluster_size === 2; @@ -384,7 +448,7 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { label: ( 1 Node {` `} {database.cluster_size === 1 && currentChip} @@ -405,7 +469,7 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { label: ( 2 Nodes - High Availability {database.cluster_size === 2 && currentChip} @@ -425,7 +489,7 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { label: ( 3 Nodes - High Availability (recommended) {database.cluster_size === 3 && currentChip} @@ -506,7 +570,18 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { )} - {summaryPanel} + + ({ + marginBottom: isDatabasesGAEnabled ? theme.spacing(2) : 0, + })} + variant="h2" + > + Summary {isDatabasesGAEnabled ? database.label : ''} + + {isDatabasesGAEnabled && currentPlan ? currentSummary : null} + {resizeSummary} + { From ebdf47cb73c1b081850e498b3f87d14e28a1cbc7 Mon Sep 17 00:00:00 2001 From: Sam Mans Date: Tue, 8 Oct 2024 14:32:46 -0400 Subject: [PATCH 6/6] Applying feedback and updating unit and e2e tests --- .../core/databases/resize-database.spec.ts | 2 +- .../DatabaseResize/DatabaseResize.test.tsx | 230 +++++++++++++----- .../DatabaseResize/DatabaseResize.tsx | 10 +- 3 files changed, 179 insertions(+), 63 deletions(-) diff --git a/packages/manager/cypress/e2e/core/databases/resize-database.spec.ts b/packages/manager/cypress/e2e/core/databases/resize-database.spec.ts index 1dab1c0e0ad..fb2d9123c87 100644 --- a/packages/manager/cypress/e2e/core/databases/resize-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/resize-database.spec.ts @@ -149,7 +149,7 @@ describe('Resizing existing clusters', () => { if (!desiredPlanPrice) { throw new Error('Unable to find mock plan type'); } - cy.get('[data-testid="summary"]').within(() => { + cy.get('[data-testid="resizeSummary"]').within(() => { cy.contains(`${nodeType.label}`).should('be.visible'); cy.contains(`$${desiredPlanPrice.monthly}/month`).should( 'be.visible' diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.test.tsx index 2141221dadb..3f3b2d46e57 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.test.tsx @@ -1,5 +1,4 @@ import { - fireEvent, queryByAttribute, waitForElementToBeRemoved, } from '@testing-library/react'; @@ -7,12 +6,17 @@ import { createMemoryHistory } from 'history'; import * as React from 'react'; import { Router } from 'react-router-dom'; -import { databaseFactory, databaseTypeFactory } from 'src/factories'; +import { + accountFactory, + databaseFactory, + databaseTypeFactory, +} from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; import { DatabaseResize } from './DatabaseResize'; +import userEvent from '@testing-library/user-event'; const loadingTestId = 'circle-progress'; @@ -25,6 +29,29 @@ describe('database resize', () => { }); it('should render a loading state', async () => { + // Mock database types + const standardTypes = [ + databaseTypeFactory.build({ + class: 'nanode', + id: 'g6-nanode-1', + label: `Nanode 1 GB`, + memory: 1024, + }), + ...databaseTypeFactory.buildList(7, { class: 'standard' }), + ]; + + server.use( + http.get('*/databases/types', () => { + return HttpResponse.json( + makeResourcePage([...standardTypes, ...dedicatedTypes]) + ); + }), + http.get('*/account', () => { + const account = accountFactory.build(); + return HttpResponse.json(account); + }) + ); + const { getByTestId } = renderWithTheme( ); @@ -49,6 +76,10 @@ describe('database resize', () => { return HttpResponse.json( makeResourcePage([...standardTypes, ...dedicatedTypes]) ); + }), + http.get('*/account', () => { + const account = accountFactory.build(); + return HttpResponse.json(account); }) ); @@ -86,6 +117,10 @@ describe('database resize', () => { return HttpResponse.json( makeResourcePage([...standardTypes, ...dedicatedTypes]) ); + }), + http.get('*/account', () => { + const account = accountFactory.build(); + return HttpResponse.json(account); }) ); }); @@ -111,13 +146,13 @@ describe('database resize', () => { ); await waitForElementToBeRemoved(getByTestId(loadingTestId)); const getById = queryByAttribute.bind(null, 'id'); - fireEvent.click(getById(container, examplePlanType)); + await userEvent.click(getById(container, examplePlanType)); const resizeButton = getByText(/Resize Database Cluster/i); expect(resizeButton.closest('button')).toHaveAttribute( 'aria-disabled', 'false' ); - fireEvent.click(resizeButton); + await userEvent.click(resizeButton); getByText(`Resize Database Cluster ${database.label}?`); }); @@ -134,6 +169,42 @@ describe('database resize', () => { }); describe('on rendering of page and isDatabasesGAEnabled is true and the Shared CPU tab is preselected ', () => { + beforeEach(() => { + // Mock database types + const standardTypes = [ + databaseTypeFactory.build({ + class: 'nanode', + id: 'g6-nanode-1', + label: `New DBaaS - Nanode 1 GB`, + memory: 1024, + }), + ...databaseTypeFactory.buildList(7, { class: 'standard' }), + ]; + const mockDedicatedTypes = [ + databaseTypeFactory.build({ + class: 'dedicated', + disk: 81920, + id: 'g6-dedicated-2', + label: 'Dedicated 4 GB', + memory: 4096, + }), + ]; + + server.use( + http.get('*/databases/types', () => { + return HttpResponse.json( + makeResourcePage([...mockDedicatedTypes, ...standardTypes]) + ); + }), + http.get('*/account', () => { + const account = accountFactory.build({ + capabilities: ['Managed Databases', 'Managed Databases Beta'], + }); + return HttpResponse.json(account); + }) + ); + }); + it('should render set node section', async () => { const flags = { dbaasV2: { @@ -144,6 +215,7 @@ describe('database resize', () => { const mockDatabase = databaseFactory.build({ cluster_size: 3, type: 'g6-nanode-1', + engine: 'mysql', }); const { getByTestId, getByText } = renderWithTheme( , @@ -181,7 +253,7 @@ describe('database resize', () => { expect(nodeRadioBtns).toHaveTextContent('$140/month $0.21/hr'); const expectedCurrentSummary = - 'Current Cluster: Nanode 1 GB $60/month 3 Nodes - HA $140/month'; + 'Current Cluster: New DBaaS - Nanode 1 GB $60/month 3 Nodes - HA $140/month'; const currentSummary = getByTestId('currentSummary'); expect(currentSummary).toHaveTextContent(expectedCurrentSummary); @@ -253,14 +325,14 @@ describe('database resize', () => { // Mock clicking 3 Nodes option const selectedNodeRadioButton = getByTestId('database-node-3').children[0] .children[0] as HTMLInputElement; - fireEvent.click(selectedNodeRadioButton); + await userEvent.click(selectedNodeRadioButton); const resizeButton = getByText(/Resize Database Cluster/i).closest( 'button' ) as HTMLButtonElement; expect(resizeButton.disabled).toBeFalsy(); const expectedSummaryText = - 'Resized Cluster: Nanode 1 GB $60/month 3 Nodes - HA $140/month'; + 'Resized Cluster: New DBaaS - Nanode 1 GB $60/month 3 Nodes - HA $140/month'; const summary = getByTestId('resizeSummary'); expect(summary).toHaveTextContent(expectedSummaryText); }); @@ -284,7 +356,7 @@ describe('database resize', () => { // Mock clicking 3 Nodes option const threeNodesRadioButton = getByTestId('database-node-3').children[0] .children[0] as HTMLInputElement; - fireEvent.click(threeNodesRadioButton); + await userEvent.click(threeNodesRadioButton); const resizeButton = getByText(/Resize Database Cluster/i).closest( 'button' ); @@ -292,59 +364,79 @@ describe('database resize', () => { // Mock clicking 1 Node option const oneNodeRadioButton = getByTestId('database-node-1').children[0] .children[0] as HTMLInputElement; - fireEvent.click(oneNodeRadioButton); + await userEvent.click(oneNodeRadioButton); expect(resizeButton).toBeDisabled(); }); + }); - describe('on rendering of page and isDatabasesGAEnabled is true and the Dedicated CPU tab is preselected', () => { - beforeEach(() => { - // Mock database types - const mockDedicatedTypes = [ - databaseTypeFactory.build({ - class: 'dedicated', - disk: 81920, - id: 'g6-dedicated-2', - label: 'Dedicated 4 GB', - memory: 4096, - }), - databaseTypeFactory.build({ - class: 'dedicated', - disk: 163840, - id: 'g6-dedicated-4', - label: 'Dedicated 8 GB', - memory: 8192, - }), - ]; - server.use( - http.get('*/databases/types', () => { - return HttpResponse.json(makeResourcePage([...mockDedicatedTypes])); - }) - ); - }); - it('should render node selection for dedicated tab with default summary', async () => { - const mockDatabase = databaseFactory.build({ - type: 'g6-dedicated-2', - cluster_size: 3, - }); - - const flags = { - dbaasV2: { - beta: false, - enabled: true, - }, - }; - - const { getByTestId } = renderWithTheme( - , - { flags } - ); - expect(getByTestId(loadingTestId)).toBeInTheDocument(); - await waitForElementToBeRemoved(getByTestId(loadingTestId)); - expect(getByTestId('database-nodes')).toBeDefined(); - expect(getByTestId('database-node-1')).toBeDefined(); - expect(getByTestId('database-node-2')).toBeDefined(); - expect(getByTestId('database-node-3')).toBeDefined(); + describe('on rendering of page and isDatabasesGAEnabled is true and the Dedicated CPU tab is preselected', () => { + beforeEach(() => { + // Mock database types + const mockDedicatedTypes = [ + databaseTypeFactory.build({ + class: 'dedicated', + disk: 81920, + id: 'g6-dedicated-2', + label: 'Dedicated 4 GB', + memory: 4096, + }), + databaseTypeFactory.build({ + class: 'dedicated', + disk: 163840, + id: 'g6-dedicated-4', + label: 'Dedicated 8 GB', + memory: 8192, + }), + ]; + + // Mock database types + const standardTypes = [ + databaseTypeFactory.build({ + class: 'nanode', + id: 'g6-nanode-1', + label: `New DBaaS - Nanode 1 GB`, + memory: 1024, + }), + ]; + + server.use( + http.get('*/databases/types', () => { + return HttpResponse.json( + makeResourcePage([...mockDedicatedTypes, ...standardTypes]) + ); + }), + http.get('*/account', () => { + const account = accountFactory.build({ + capabilities: ['Managed Databases', 'Managed Databases Beta'], + }); + return HttpResponse.json(account); + }) + ); + }); + + it('should render node selection for dedicated tab with default summary', async () => { + const mockDatabase = databaseFactory.build({ + type: 'g6-dedicated-2', + cluster_size: 3, }); + + const flags = { + dbaasV2: { + beta: false, + enabled: true, + }, + }; + + const { getByTestId } = renderWithTheme( + , + { flags } + ); + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + expect(getByTestId('database-nodes')).toBeDefined(); + expect(getByTestId('database-node-1')).toBeDefined(); + expect(getByTestId('database-node-2')).toBeDefined(); + expect(getByTestId('database-node-3')).toBeDefined(); }); it('should disable lower node selections', async () => { @@ -410,6 +502,10 @@ describe('database resize', () => { server.use( http.get('*/databases/types', () => { return HttpResponse.json(makeResourcePage([...dedicatedTypes])); + }), + http.get('*/account', () => { + const account = accountFactory.build(); + return HttpResponse.json(account); }) ); const { getByTestId } = renderWithTheme( @@ -429,6 +525,26 @@ describe('database resize', () => { type: 'g6-dedicated-8', }); it('should disable Shared Plans Tab', async () => { + const standardTypes = [ + databaseTypeFactory.build({ + class: 'nanode', + id: 'g6-nanode-1', + label: `Nanode 1 GB`, + memory: 1024, + }), + ]; + server.use( + http.get('*/databases/types', () => { + return HttpResponse.json( + makeResourcePage([...dedicatedTypes, ...standardTypes]) + ); + }), + http.get('*/account', () => { + const account = accountFactory.build(); + return HttpResponse.json(account); + }) + ); + const { getByTestId, getByText } = renderWithTheme( ); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx index 01f12089124..ffe15698e5a 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.tsx @@ -54,12 +54,12 @@ const useStyles = makeStyles()((theme: Theme) => ({ summarySpanBorder: { borderRight: `1px solid ${theme.borderColors.borderTypography}`, color: theme.textColors.tableStatic, - paddingRight: '10px', - marginRight: '10px;', - marginLeft: '10px;', + paddingRight: theme.spacing(1), + marginRight: theme.spacing(1), + marginLeft: theme.spacing(1), }, nodeSpanSpacing: { - marginRight: '10px', + marginRight: theme.spacing(1), }, })); @@ -363,7 +363,7 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => { {' '} {database.cluster_size} Node - {database.cluster_size > 1 ? 's - HA ' : ' - HA '} + {database.cluster_size > 1 ? 's - HA ' : ' '} {currentNodePrice}