Skip to content

Commit

Permalink
change: [UIE-8228] - Database Resize: disable same size plan (#11481)
Browse files Browse the repository at this point in the history
* change: [UIE-8228] - Database Resize: disable same size plan

* Added changeset: Database Resize: Disable plans when the usable storage equals the used storage of the database cluster
  • Loading branch information
mpolotsk-akamai authored Jan 9, 2025
1 parent e97ff2f commit 5a20623
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 143 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-11481-changed-1736250933443.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Changed
---

Database Resize: Disable plans when the usable storage equals the used storage of the database cluster ([#11481](https://github.com/linode/manager/pull/11481))
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
queryByAttribute,
waitForElementToBeRemoved,
} from '@testing-library/react';
import { waitForElementToBeRemoved } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createMemoryHistory } from 'history';
import * as React from 'react';
Expand All @@ -17,29 +14,36 @@ import { HttpResponse, http, server } from 'src/mocks/testServer';
import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers';

import { DatabaseResize } from './DatabaseResize';
import { isSmallerOrEqualCurrentPlan } from './DatabaseResize.utils';

import type { PlanSelectionWithDatabaseType } from 'src/features/components/PlansPanel/types';

const loadingTestId = 'circle-progress';

beforeAll(() => mockMatchMedia());

describe('database resize', () => {
const database = databaseFactory.build();
const mockDatabase = databaseFactory.build({
cluster_size: 3,
engine: 'mysql',
platform: 'rdbms-default',
type: 'g6-nanode-1',
used_disk_size_gb: 0,
});
const dedicatedTypes = databaseTypeFactory.buildList(7, {
class: 'dedicated',
});

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' }),
];

const standardTypes = [
databaseTypeFactory.build({
class: 'nanode',
id: 'g6-nanode-1',
label: `Nanode 1 GB`,
memory: 1024,
}),
...databaseTypeFactory.buildList(7, { class: 'standard' }),
];
beforeEach(() => {
server.use(
http.get('*/databases/types', () => {
return HttpResponse.json(
Expand All @@ -51,38 +55,17 @@ describe('database resize', () => {
return HttpResponse.json(account);
})
);
});

it('should render a loading state', async () => {
const { getByTestId } = renderWithTheme(
<DatabaseResize database={database} />
);

// Should render a loading state
expect(getByTestId(loadingTestId)).toBeInTheDocument();
});

it('should render configuration, summary sections and input field to choose a plan', 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, getByText } = renderWithTheme(
<DatabaseResize database={database} />
);
Expand All @@ -96,34 +79,12 @@ describe('database resize', () => {
});

describe('On rendering of page', () => {
const examplePlanType = 'g6-dedicated-50';
const dedicatedTypes = databaseTypeFactory.buildList(7, {
class: 'dedicated',
});
const database = databaseFactory.build();
beforeEach(() => {
// 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 flags = {
dbaasV2: {
beta: false,
enabled: true,
},
};

it('resize button should be disabled when no input is provided in the form', async () => {
const { getByTestId, getByText } = renderWithTheme(
Expand All @@ -139,26 +100,35 @@ describe('database resize', () => {
// Mock route history so the Plan Selection table displays prices without requiring a region in the DB resize flow.
const history = createMemoryHistory();
history.push(`databases/${database.engine}/${database.id}/resize`);
const { container, getByTestId, getByText } = renderWithTheme(
const { getByRole, getByTestId, getByText } = renderWithTheme(
<Router history={history}>
<DatabaseResize database={database} />
</Router>
<DatabaseResize database={mockDatabase} />
</Router>,
{ flags }
);
await waitForElementToBeRemoved(getByTestId(loadingTestId));
const getById = queryByAttribute.bind(null, 'id');
await userEvent.click(getById(container, examplePlanType));

const planRadioButton = document.getElementById('g6-standard-6');
await userEvent.click(planRadioButton as HTMLInputElement);

const resizeButton = getByText(/Resize Database Cluster/i);
expect(resizeButton.closest('button')).toHaveAttribute(
'aria-disabled',
'false'
);

await userEvent.click(resizeButton);
getByText(`Resize Database Cluster ${database.label}?`);

const dialogElement = getByRole('dialog');
expect(dialogElement).toBeInTheDocument();
expect(dialogElement).toHaveTextContent(
`Resize Database Cluster ${mockDatabase.label}?`
);
});

it('Should disable the "Resize Database Cluster" button when disabled = true', async () => {
const { getByTestId, getByText } = renderWithTheme(
<DatabaseResize database={database} disabled={true} />
<DatabaseResize database={mockDatabase} disabled={true} />
);
await waitForElementToBeRemoved(getByTestId(loadingTestId));
const resizeDatabaseBtn = getByText('Resize Database Cluster').closest(
Expand All @@ -169,13 +139,6 @@ describe('database resize', () => {
});

describe('on rendering of page and isDatabasesV2GA is true and the Shared CPU tab is preselected ', () => {
const mockDatabase = databaseFactory.build({
cluster_size: 3,
engine: 'mysql',
platform: 'rdbms-default',
type: 'g6-nanode-1',
});

const flags = {
dbaasV2: {
beta: false,
Expand Down Expand Up @@ -403,51 +366,41 @@ describe('database resize', () => {
});

describe('should be disabled smaller plans', () => {
const database = databaseFactory.build({
type: 'g6-dedicated-8',
});
// Mock database types
const dedicatedTypes = [
databaseTypeFactory.build({
class: 'dedicated',
disk: 1,
id: 'g6-dedicated-2',
label: 'Dedicated 4 GB',
}) as PlanSelectionWithDatabaseType,
databaseTypeFactory.build({
class: 'dedicated',
disk: 2,
id: 'g6-dedicated-4',
label: 'Dedicated 8 GB',
}) as PlanSelectionWithDatabaseType,
databaseTypeFactory.build({
class: 'dedicated',
disk: 3,
id: 'g6-dedicated-8',
label: `Linode 16 GB`,
}) as PlanSelectionWithDatabaseType,
];

const disabledTypes = isSmallerOrEqualCurrentPlan(
'g6-dedicated-8',
3,
dedicatedTypes,
true
);

it('disabled smaller plans', async () => {
// Mock database types
const dedicatedTypes = [
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,
}),
databaseTypeFactory.build({
class: 'dedicated',
disk: 327680,
id: 'g6-dedicated-8',
label: `Linode 16 GB`,
memory: 16384,
}),
];
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(
<DatabaseResize database={database} />
);
expect(getByTestId(loadingTestId)).toBeInTheDocument();
await waitForElementToBeRemoved(getByTestId(loadingTestId));
expect(
document.getElementById('g6-dedicated-4')?.hasAttribute('disabled')
);
expect(disabledTypes.includes(dedicatedTypes[1])).toBe(true);
});

it('disable plan if it is the same size as used storage', async () => {
expect(disabledTypes.includes(dedicatedTypes[2])).toBe(true);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ import { DatabaseSummarySection } from 'src/features/Databases/DatabaseCreate/Da
import { DatabaseResizeCurrentConfiguration } from 'src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResizeCurrentConfiguration';
import { useIsDatabasesEnabled } from 'src/features/Databases/utilities';
import { typeLabelDetails } from 'src/features/Linodes/presentation';
import { useDatabaseTypesQuery } from 'src/queries/databases/databases';
import { useDatabaseMutation } from 'src/queries/databases/databases';
import { useDatabaseTypesQuery } from 'src/queries/databases/databases';
import { formatStorageUnits } from 'src/utilities/formatStorageUnits';
import { convertMegabytesTo } from 'src/utilities/unitConversions';

import {
StyledGrid,
StyledPlansPanel,
StyledResizeButton,
} from './DatabaseResize.style';
import { isSmallerOrEqualCurrentPlan } from './DatabaseResize.utils';

import type {
ClusterSize,
Expand Down Expand Up @@ -216,21 +216,12 @@ export const DatabaseResize = ({ database, disabled = false }: Props) => {
setSelectedTab(initialTab);
}, []);

const currentPlanDisk = currentPlan ? currentPlan.disk : 0;
const disabledPlans = !isNewDatabaseGA
? displayTypes?.filter((type) =>
type.class === 'dedicated'
? type.disk < currentPlanDisk
: type.disk <= currentPlanDisk
)
: displayTypes?.filter(
(type) =>
database?.used_disk_size_gb &&
database.used_disk_size_gb >
+convertMegabytesTo(type.disk, true)
.split(/(GB|MB|KB)/i)[0]
.trim()
);
const disabledPlans = isSmallerOrEqualCurrentPlan(
currentPlan?.id,
database?.used_disk_size_gb,
displayTypes,
isNewDatabaseGA
);
const isDisabledSharedTab = database.cluster_size === 2;

const shouldSubmitBeDisabled = React.useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { convertMegabytesTo } from 'src/utilities/unitConversions';

import type { PlanSelectionWithDatabaseType } from 'src/features/components/PlansPanel/types';

/**
* Filters a list of plans based on the current plan's disk size or the current used disk size.
*
* @param {string | undefined} currentPlanID - The ID of the current plan.
* @param {null | number} currentUsedDiskSize - The current used disk size.
* @param {PlanSelectionWithDatabaseType[]} types - The list of available plans to filter.
* @param {boolean} [isNewDatabase] - Optional flag indicating whether the database is new. If true, the filtering logic based on disk usage is applied.
*
* @returns {PlanSelectionWithDatabaseType[]} A filtered list of plans based on the conditions:
* - If `isNewDatabase` is false and `currentPlanID` is provided, plans with disk sizes smaller or equal to the current plan are included.
* - If `isNewDatabase` is true, plans are filtered based on their disk size compared to the current used disk size.
*/
export const isSmallerOrEqualCurrentPlan = (
currentPlanID: string | undefined,
currentUsedDiskSize: null | number,
types: PlanSelectionWithDatabaseType[],
isNewDatabase?: boolean
) => {
const currentType = types.find((thisType) => thisType.id === currentPlanID);

return !isNewDatabase && currentType
? types?.filter((type) =>
type.class === 'dedicated'
? type.disk < currentType?.disk
: type.disk <= currentType?.disk
)
: types?.filter(
(type) =>
currentUsedDiskSize &&
currentUsedDiskSize >=
+convertMegabytesTo(type.disk, true)
.split(/(GB|MB|KB)/i)[0]
.trim()
);
};

0 comments on commit 5a20623

Please sign in to comment.