Skip to content
5 changes: 5 additions & 0 deletions packages/api-v4/.changeset/pr-10262-added-1709742032135.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Added
---

Added event type for database resize create ([#10262](https://github.com/linode/manager/pull/10262))
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 @@ -288,6 +288,7 @@ export type EventAction =
| 'credit_card_updated'
| 'database_low_disk_space'
| 'database_resize'
| 'database_resize_create'
| 'database_backup_restore'
| 'database_create'
| 'database_credentials_reset'
Expand Down
3 changes: 2 additions & 1 deletion packages/api-v4/src/databases/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export type DatabaseStatus =
| 'resuming'
| 'restoring'
| 'failed'
| 'degraded';
| 'degraded'
| 'resizing';

export type DatabaseBackupType = 'snapshot' | 'auto';

Expand Down
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-10262-changed-1709742412767.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Changed
---

Notifications for database resize events ([#10262](https://github.com/linode/manager/pull/10262))
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import {
import { usePreferences } from 'src/queries/preferences';

interface EntityInfo {
action?: 'cancellation' | 'deletion' | 'detachment' | 'restoration';
action?:
| 'cancellation'
| 'deletion'
| 'detachment'
| 'restoration'
| 'resizing';
name?: string | undefined;
primaryBtnText: string;
subType?: 'CloseAccount' | 'Cluster' | 'ObjectStorage';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ describe('database resize', () => {
'false'
);
fireEvent.click(resizeButton);
getByText(`Resize ${database.label}?`);
getByText(`Resize Database Cluster ${database.label}?`);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
import { LinodeTypeClass } from '@linode/api-v4';
import {
Database,
DatabaseClusterSizeObject,
DatabasePriceObject,
DatabaseType,
Engine,
} from '@linode/api-v4/lib/databases/types';
import { useSnackbar } from 'notistack';
import * as React from 'react';
import { useHistory } from 'react-router-dom';

import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { Box } from 'src/components/Box';
import { CircleProgress } from 'src/components/CircleProgress';
import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog';
import { ErrorState } from 'src/components/ErrorState/ErrorState';
import { Notice } from 'src/components/Notice/Notice';
import { Paper } from 'src/components/Paper';
import { Typography } from 'src/components/Typography';
import { typeLabelDetails } from 'src/features/Linodes/presentation';
import { PlanSelectionType } from 'src/features/components/PlansPanel/types';
import { getPlanSelectionsByPlanType } from 'src/features/components/PlansPanel/utils';
import { useDatabaseTypesQuery } from 'src/queries/databases';
import { useDatabaseMutation } from 'src/queries/databases';
import { formatStorageUnits } from 'src/utilities/formatStorageUnits';
import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog';

import {
StyledGrid,
Expand Down Expand Up @@ -75,12 +72,9 @@ export const DatabaseResize = ({ database }: Props) => {
updateDatabase({
type: planSelected,
}).then(() => {
enqueueSnackbar(
`Your database cluster ${database.label} is being resized.`,
{
variant: 'info',
}
);
enqueueSnackbar(`Database cluster ${database.label} is being resized.`, {
variant: 'info',
});
history.push(`/databases/${database.engine}/${database.id}`);
});
};
Expand Down Expand Up @@ -117,22 +111,6 @@ export const DatabaseResize = ({ database }: Props) => {
</>
);

const confirmationDialogActions = (
<ActionsPanel
primaryButtonProps={{
'data-testid': 'button-confirm',
label: 'Resize',
loading: submitInProgress,
onClick: onResize,
}}
secondaryButtonProps={{
'data-testid': 'button-cancel',
label: 'Cancel',
onClick: () => setIsResizeConfirmationDialogOpen(false),
}}
/>
);

const costSummary = (
<Typography sx={{ marginBottom: '10px' }} variant="h3">
{`The cost of the resized database is ${summaryText?.price}.`}
Expand Down Expand Up @@ -160,7 +138,9 @@ export const DatabaseResize = ({ database }: Props) => {
return;
}

const selectedPlanType = dbTypes.find((type) => type.id === planSelected);
const selectedPlanType = dbTypes.find(
(type: DatabaseType) => type.id === planSelected
);
if (!selectedPlanType) {
setPlanSelected(undefined);
setSummaryText(undefined);
Expand Down Expand Up @@ -195,11 +175,12 @@ export const DatabaseResize = ({ database }: Props) => {
if (!dbTypes) {
return [];
}
return dbTypes.map((type) => {
return dbTypes.map((type: DatabaseType) => {
const { label } = type;
const formattedLabel = formatStorageUnits(label);
const nodePricing = type.engines[selectedEngine].find(
(cluster) => cluster.quantity === database.cluster_size
(cluster: DatabaseClusterSizeObject) =>
cluster.quantity === database.cluster_size
);
const price = nodePricing?.price ?? {
hourly: null,
Expand All @@ -220,18 +201,12 @@ export const DatabaseResize = ({ database }: Props) => {
}, [database.cluster_size, dbTypes, selectedEngine]);

const currentPlan = displayTypes?.find((type) => type.id === database.type);
// create an array of different class of types.
const typeClasses: LinodeTypeClass[] = Object.keys(
getPlanSelectionsByPlanType(displayTypes)
).map((plan) => (plan === 'shared' ? 'standard' : (plan as LinodeTypeClass)));
const currentPlanClass = currentPlan?.class ?? 'dedicated';
// We don't have a "Nanodes" tab anymore, so use `shared`
const selectedTypeClass =
currentPlanClass === 'nanode' ? 'standard' : currentPlanClass;
// User cannot switch to different plan type apart from current plan while resizing a DB cluster. So disable rest of the tabs.
const tabsToBeDisabled = typeClasses
.filter((typeClass) => typeClass !== selectedTypeClass)
.map((plan) => (plan === 'standard' ? 'shared' : plan));

const disabledPlans = displayTypes?.filter(
(type) =>
type.disk < (currentPlan ? currentPlan.disk : 0) ||
(currentPlan?.class === 'dedicated' && type.disk === currentPlan?.disk)
);
if (typesLoading) {
return <CircleProgress />;
}
Expand All @@ -249,15 +224,12 @@ export const DatabaseResize = ({ database }: Props) => {
</Paper>
<Paper sx={{ marginTop: 2 }}>
<StyledPlansPanel
tabDisabledMessage={
'You can resize your cluster only within already selected plan.'
}
currentPlanHeading={currentPlan?.heading}
data-qa-select-plan
disabledTabs={tabsToBeDisabled}
disabledPlanTypes={disabledPlans}
disabledPlanTypesToolTipText="Resizing to smaller plans is not supported."
header="Choose a Plan"
onSelect={(selected: string) => setPlanSelected(selected)}
selectedDiskSize={currentPlan?.disk}
selectedId={planSelected}
types={displayTypes}
/>
Expand All @@ -275,15 +247,26 @@ export const DatabaseResize = ({ database }: Props) => {
Resize Database Cluster
</StyledResizeButton>
</StyledGrid>
<ConfirmationDialog
actions={confirmationDialogActions}
error={resizeError?.[0].reason}
<TypeToConfirmDialog
entity={{
action: 'resizing',
name: database.label,
primaryBtnText: 'Resize Cluster',
subType: 'Cluster',
type: 'Database',
}}
label={'Cluster Name'}
loading={submitInProgress}
onClick={onResize}
onClose={() => setIsResizeConfirmationDialogOpen(false)}
open={isResizeConfirmationDialogOpen}
title={`Resize ${database.label}?`}
title={`Resize Database Cluster ${database.label}?`}
>
{resizeError ? (
<Notice text={resizeError[0].reason} variant="error" />
) : null}
{confirmationPopUpMessage}
</ConfirmationDialog>
</TypeToConfirmDialog>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@ export const StyledTitleTypography = styled(Typography, {
marginBottom: theme.spacing(2),
}));

export const StyledStatusSpan = styled('span', { label: 'StyledStatusSpan' })(
() => ({
textTransform: 'capitalize',
})
);
export const StyledStatusBox = styled(Box, {
label: 'StyledStatusBox',
})(() => ({
alignItems: 'center',
display: 'inline-flex',
textTransform: 'capitalize',
verticalAlign: 'sub',
}));
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import { Database, DatabaseInstance } from '@linode/api-v4/lib/databases/types';
import {
Database,
DatabaseInstance,
DatabaseType,
} from '@linode/api-v4/lib/databases/types';
import { useTheme } from '@mui/material/styles';
import * as React from 'react';

import { Box } from 'src/components/Box';
import { CircleProgress } from 'src/components/CircleProgress';
import { ErrorState } from 'src/components/ErrorState/ErrorState';
import { StatusIcon } from 'src/components/StatusIcon/StatusIcon';
import { TooltipIcon } from 'src/components/TooltipIcon';
import { useDatabaseTypesQuery } from 'src/queries/databases';
import { useRegionsQuery } from 'src/queries/regions';
import { useInProgressEvents } from 'src/queries/events/events';
import { formatStorageUnits } from 'src/utilities/formatStorageUnits';
import { convertMegabytesTo } from 'src/utilities/unitConversions';

import {
databaseEngineMap,
databaseStatusMap,
} from '../../DatabaseLanding/DatabaseRow';
import { databaseEngineMap } from '../../DatabaseLanding/DatabaseRow';
import {
StyledStatusSpan,
StyledSummaryBox,
StyledSummaryTextBox,
StyledSummaryTextTypography,
StyledTitleTypography,
StyledStatusBox,
} from './DatabaseResizeCurrentConfiguration.style';
import { Region } from '@linode/api-v4';
import { DatabaseStatusDisplay } from '../DatabaseStatusDisplay';

interface Props {
database: Database;
Expand All @@ -41,10 +42,11 @@ export const DatabaseResizeCurrentConfiguration = ({ database }: Props) => {
const theme = useTheme();
const { data: regions } = useRegionsQuery();

const region = regions?.find((r) => r.id === database.region);
const region = regions?.find((r: Region) => r.id === database.region);

const type = types?.find((type) => type.id === database?.type);
const type = types?.find((type: DatabaseType) => type.id === database?.type);

const { data: events } = useInProgressEvents();
if (typesLoading) {
return <CircleProgress />;
}
Expand All @@ -68,7 +70,7 @@ export const DatabaseResizeCurrentConfiguration = ({ database }: Props) => {
};

const STORAGE_COPY =
'The total disk size is smaller than the selected plan capacity due to the OS overhead.';
'The total disk size is smaller than the selected plan capacity due to overhead from the OS.';

return (
<>
Expand All @@ -83,13 +85,9 @@ export const DatabaseResizeCurrentConfiguration = ({ database }: Props) => {
<Box key={'status-version'} paddingRight={6}>
<StyledSummaryTextBox>
<span style={{ fontFamily: theme.font.bold }}>Status</span>{' '}
<StyledStatusSpan>
<StatusIcon
status={databaseStatusMap[database.status]}
sx={{ verticalAlign: 'sub' }}
/>
{database.status}
</StyledStatusSpan>
<StyledStatusBox>
<DatabaseStatusDisplay events={events} database={database} />
</StyledStatusBox>
</StyledSummaryTextBox>
<StyledSummaryTextTypography>
<span style={{ fontFamily: theme.font.bold }}>Version</span>{' '}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import { DatabaseStatusDisplay } from '../DatabaseDetail/DatabaseStatusDisplay';
import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers';
import { databaseFactory } from 'src/factories';
import { entityFactory, eventFactory } from 'src/factories/events';
beforeAll(() => mockMatchMedia());

describe('DatabaseStatusDisplay component', () => {
it(`renders status 'resizing' with percentage when recent event status is 'started' or 'scheduled'`, () => {
const mockEvent = eventFactory.build({
action: 'database_resize',
percent_complete: 50,
entity: entityFactory.build({ id: 1, type: 'database' }),
});
const database = databaseFactory.build({ id: 1, status: 'resizing' });

const { getByText } = renderWithTheme(
<DatabaseStatusDisplay events={[mockEvent]} database={database} />
);
expect(getByText('Resizing (50%)')).toBeInTheDocument();
});

it(`renders status 'active' when recent event status is 'finished' `, () => {
const mockEvent = eventFactory.build({
action: 'database_resize',
status: 'finished',
entity: entityFactory.build({ id: 1, type: 'database' }),
});
const database = databaseFactory.build({ id: 1, status: 'active' });

const { getByText } = renderWithTheme(
<DatabaseStatusDisplay events={[mockEvent]} database={database} />
);

expect(getByText('Active')).toBeInTheDocument();
});
});
Loading