Skip to content

Commit

Permalink
feat: [UIE-6937] - Add the ability to scale up DBaaS (#9869)
Browse files Browse the repository at this point in the history
* feat: [UIE-6937] - Add the ability to scale DBaaS

* Added changeset: Add the ability to scale up DBaaS

* Addressed review comments

* Show confirmation popup message conditionally based on node size of cluster

* Addressed review comments

* Fixed issue with unit test case and addressed review comments

* Update logic to disable smaller plans in plan selection table

* UIE-7092: Added feature flag, disable tabs other than active one and addressed review comments

* Addressed review comments

* Addressed review comments

* Fix unit test

---------

Co-authored-by: Banks Nussman <banks@nussman.us>
  • Loading branch information
grevanak-akamai and bnussman authored Jan 11, 2024
1 parent f6dff19 commit 14e6d00
Show file tree
Hide file tree
Showing 21 changed files with 862 additions and 9 deletions.
5 changes: 5 additions & 0 deletions packages/api-v4/.changeset/pr-9869-added-1703002185530.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Added
---

Ability to scale up Database instances ([#9869](https://github.com/linode/manager/pull/9869))
1 change: 1 addition & 0 deletions packages/api-v4/src/account/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ export type EventAction =
| 'community_question_reply'
| 'credit_card_updated'
| 'database_low_disk_space'
| 'database_scale'
| 'database_backup_restore'
| 'database_create'
| 'database_credentials_reset'
Expand Down
2 changes: 1 addition & 1 deletion packages/api-v4/src/databases/databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export const getEngineDatabase = (engine: Engine, databaseID: number) =>
/**
* updateDatabase
*
* Update the label or allowed IPs of an
* Update the label or allowed IPs or plan of an
* existing database
*
*/
Expand Down
1 change: 1 addition & 0 deletions packages/api-v4/src/databases/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export interface UpdateDatabasePayload {
label?: string;
allow_list?: string[];
updates?: UpdatesSchedule;
type?: string;
}

export interface UpdateDatabaseResponse {
Expand Down
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-9869-added-1699013625860.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Added
---

Ability to scale up Database instances ([#9869](https://github.com/linode/manager/pull/9869))
23 changes: 22 additions & 1 deletion packages/manager/src/components/TabbedPanel/TabbedPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import HelpOutline from '@mui/icons-material/HelpOutline';
import { styled } from '@mui/material/styles';
import { SxProps } from '@mui/system';
import React, { useEffect, useState } from 'react';
Expand All @@ -9,11 +10,13 @@ import { TabList } from 'src/components/Tabs/TabList';
import { TabPanel } from 'src/components/Tabs/TabPanel';
import { TabPanels } from 'src/components/Tabs/TabPanels';
import { Tabs } from 'src/components/Tabs/Tabs';
import { Tooltip } from 'src/components/Tooltip';
import { Typography } from 'src/components/Typography';

import { Box } from '../Box';

export interface Tab {
disabled?: boolean;
render: (props: any) => JSX.Element | null;
title: string;
}
Expand All @@ -31,6 +34,7 @@ interface TabbedPanelProps {
noPadding?: boolean;
rootClass?: string;
sx?: SxProps;
tabDisabledMessage?: string;
tabs: Tab[];
value?: number;
}
Expand All @@ -52,6 +56,13 @@ const TabbedPanel = React.memo((props: TabbedPanelProps) => {

const [tabIndex, setTabIndex] = useState<number | undefined>(initTab);

const sxHelpIcon = {
height: 20,
m: 0.5,
verticalAlign: 'sub',
width: 20,
};

const tabChangeHandler = (index: number) => {
setTabIndex(index);
if (handleTabChange) {
Expand Down Expand Up @@ -89,8 +100,18 @@ const TabbedPanel = React.memo((props: TabbedPanelProps) => {
<StyledTabs index={tabIndex} onChange={tabChangeHandler}>
<StyledTabList>
{tabs.map((tab, idx) => (
<StyledTab key={`tabs-${tab.title}-${idx}`}>
<StyledTab
disabled={tab.disabled}
key={`tabs-${tab.title}-${idx}`}
>
{tab.title}
{tab.disabled && props.tabDisabledMessage && (
<Tooltip title={props.tabDisabledMessage}>
<span>
<HelpOutline fontSize="small" sx={sxHelpIcon} />
</span>
</Tooltip>
)}
</StyledTab>
))}
</StyledTabList>
Expand Down
1 change: 1 addition & 0 deletions packages/manager/src/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface Flags {
aglbFullCreateFlow: boolean;
apiMaintenance: APIMaintenance;
databaseBeta: boolean;
databaseScaleUp: boolean;
databases: boolean;
dcGetWell: boolean;
firewallNodebalancer: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Grid from '@mui/material/Unstable_Grid2';
import { styled } from '@mui/material/styles';

import { Button } from 'src/components/Button/Button';
import { PlansPanel } from 'src/features/components/PlansPanel/PlansPanel';

export const StyledGrid = styled(Grid, { label: 'StyledGrid' })(
({ theme }) => ({
alignItems: 'center',
display: 'flex',
justifyContent: 'flex-end',
marginTop: theme.spacing(2),
[theme.breakpoints.down('sm')]: {
alignItems: 'flex-end',
flexDirection: 'column',
marginTop: theme.spacing(),
},
})
);

export const StyledScaleUpButton = styled(Button, {
label: 'StyledScaleUpButton',
})(({ theme }) => ({
[theme.breakpoints.down('md')]: {
marginRight: theme.spacing(),
},
whiteSpace: 'nowrap',
}));

export const StyledPlansPanel = styled(PlansPanel, {
label: 'StyledPlansPanel',
})(() => ({
margin: 0,
padding: 0,
}));

export const StyledPlanSummarySpan = styled('span', {
label: 'StyledPlanSummarySpan',
})(({ theme }) => ({
fontFamily: theme.font.bold,
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import {
fireEvent,
queryByAttribute,
waitForElementToBeRemoved,
} from '@testing-library/react';
import { createMemoryHistory } from 'history';
import * as React from 'react';
import { QueryClient } from 'react-query';
import { Router } from 'react-router-dom';

import { databaseFactory, databaseTypeFactory } from 'src/factories';
import { makeResourcePage } from 'src/mocks/serverHandlers';
import { rest, server } from 'src/mocks/testServer';
import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers';

import { DatabaseScaleUp } from './DatabaseScaleUp';

const queryClient = new QueryClient();
const loadingTestId = 'circle-progress';

beforeAll(() => mockMatchMedia());
afterEach(() => {
queryClient.clear();
});

describe('database scale up', () => {
const database = databaseFactory.build();
const dedicatedTypes = databaseTypeFactory.buildList(7, {
class: 'dedicated',
});

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

// 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-standard-0',
label: `Nanode 1 GB`,
memory: 1024,
}),
...databaseTypeFactory.buildList(7, { class: 'standard' }),
];
server.use(
rest.get('*/databases/types', (req, res, ctx) => {
return res(
ctx.json(makeResourcePage([...standardTypes, ...dedicatedTypes]))
);
})
);

const { getByTestId, getByText } = renderWithTheme(
<DatabaseScaleUp database={database} />,
{
queryClient,
}
);
expect(getByTestId(loadingTestId)).toBeInTheDocument();

await waitForElementToBeRemoved(getByTestId(loadingTestId));

getByText('Current Configuration');
getByText('Choose a Plan');
getByText('Summary');
});

describe('On rendering of page', () => {
const examplePlanType = 'g6-standard-60';
const dedicatedTypes = databaseTypeFactory.buildList(7, {
class: 'dedicated',
});
const database = databaseFactory.build();
beforeEach(() => {
// Mock database types
const standardTypes = [
databaseTypeFactory.build({
class: 'nanode',
id: 'g6-standard-0',
label: `Nanode 1 GB`,
memory: 1024,
}),
...databaseTypeFactory.buildList(7, { class: 'standard' }),
];
server.use(
rest.get('*/databases/types', (req, res, ctx) => {
return res(
ctx.json(makeResourcePage([...standardTypes, ...dedicatedTypes]))
);
})
);
});

it('scale up button should be disabled when no input is provided in the form', async () => {
const { getByTestId, getByText } = renderWithTheme(
<DatabaseScaleUp database={database} />,
{
queryClient,
}
);
await waitForElementToBeRemoved(getByTestId(loadingTestId));
expect(
getByText(/Scale Up Database Cluster/i).closest('button')
).toHaveAttribute('aria-disabled', 'true');
});

it('when a plan is selected, scale up button should be enabled and on click of it, it should show a confirmation dialog', async () => {
// Mock route history so the Plan Selection table displays prices without requiring a region in the DB scale up flow.
const history = createMemoryHistory();
history.push(`databases/${database.engine}/${database.id}/scale-up`);
const { container, getByTestId, getByText } = renderWithTheme(
<Router history={history}>
<DatabaseScaleUp database={database} />
</Router>,
{
queryClient,
}
);
await waitForElementToBeRemoved(getByTestId(loadingTestId));
const getById = queryByAttribute.bind(null, 'id');
fireEvent.click(getById(container, examplePlanType));
const scaleUpButton = getByText(/Scale Up Database Cluster/i);
expect(scaleUpButton.closest('button')).toHaveAttribute(
'aria-disabled',
'false'
);
fireEvent.click(scaleUpButton);
getByText(`Scale up ${database.label}?`);
});
});
});
Loading

0 comments on commit 14e6d00

Please sign in to comment.