From 06577f7dc3ca60fdd7928029f0ea7b02a833a900 Mon Sep 17 00:00:00 2001 From: Conal Ryan Date: Thu, 31 Oct 2024 14:15:44 -0400 Subject: [PATCH 1/2] feat: [UIE-8194] - DBaaS Upgrades and Maintenance 2 --- .../Databases/DatabaseEngineVersion.test.tsx | 225 ++++++++++++++++++ .../Databases/DatabaseEngineVersion.tsx | 65 +++++ .../src/features/Databases/utilities.test.ts | 128 +++++++++- .../src/features/Databases/utilities.ts | 40 +++- 4 files changed, 447 insertions(+), 11 deletions(-) create mode 100644 packages/manager/src/features/Databases/DatabaseEngineVersion.test.tsx create mode 100644 packages/manager/src/features/Databases/DatabaseEngineVersion.tsx diff --git a/packages/manager/src/features/Databases/DatabaseEngineVersion.test.tsx b/packages/manager/src/features/Databases/DatabaseEngineVersion.test.tsx new file mode 100644 index 00000000000..4af82ffefa6 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseEngineVersion.test.tsx @@ -0,0 +1,225 @@ +import { waitFor } from '@testing-library/react'; +import React from 'react'; + +import { databaseFactory } from 'src/factories/databases'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { DatabaseEngineVersion } from './DatabaseEngineVersion'; +import * as utils from './utilities'; + +const v1 = () => { + return { + isDatabasesEnabled: true, + isDatabasesV1Enabled: true, + isDatabasesV2Beta: false, + isDatabasesV2Enabled: false, + isDatabasesV2GA: false, + isUserExistingBeta: false, + isUserNewBeta: false, + }; +}; + +const v2Beta = () => { + return { + isDatabasesEnabled: true, + isDatabasesV1Enabled: true, + isDatabasesV2Beta: true, + isDatabasesV2Enabled: true, + isDatabasesV2GA: false, + isUserExistingBeta: false, + isUserNewBeta: true, + }; +}; + +const v2GA = () => ({ + isDatabasesEnabled: true, + isDatabasesV1Enabled: true, + isDatabasesV2Beta: false, + isDatabasesV2Enabled: true, + isDatabasesV2GA: true, + isUserExistingBeta: false, + isUserNewBeta: false, +}); + +const spy = vi.spyOn(utils, 'useIsDatabasesEnabled'); +spy.mockReturnValue(v2GA()); + +describe('Database Engine Version', () => { + it('should render V1 view legacy db', async () => { + spy.mockReturnValue(v1()); + + const database = databaseFactory.build({ + engine: 'postgresql', + platform: 'rdbms-legacy', + version: '14.6', + }); + + const { queryAllByText, queryByTestId } = renderWithTheme( + + ); + + await waitFor(async () => { + expect(queryAllByText('PostgreSQL v14.6')).toHaveLength(1); + expect(queryByTestId('maintenance-link')).toBeNull(); + }); + }); + + it('should render V2 beta view legacy db', async () => { + spy.mockReturnValue(v2Beta()); + + const database = databaseFactory.build({ + engine: 'postgresql', + platform: 'rdbms-legacy', + version: '14.6', + }); + + const { queryAllByText, queryByTestId } = renderWithTheme( + + ); + + await waitFor(async () => { + expect(queryAllByText('PostgreSQL v14.6')).toHaveLength(1); + expect(queryByTestId('maintenance-link')).toBeNull(); + }); + }); + + it('should render V2 beta view default db without pendingUpdates', async () => { + spy.mockReturnValue(v2Beta()); + + const database = databaseFactory.build({ + engine: 'postgresql', + platform: 'rdbms-default', + updates: { + pending: [], + }, + version: '14.6', + }); + + const { queryAllByText, queryByTestId } = renderWithTheme( + + ); + + await waitFor(async () => { + expect(queryAllByText('PostgreSQL v14.6')).toHaveLength(1); + expect(queryByTestId('maintenance-link')).toBeNull(); + }); + }); + + it('should render V2 beta view default db with pendingUpdates', async () => { + spy.mockReturnValue(v2Beta()); + + const database = databaseFactory.build({ + engine: 'postgresql', + platform: 'rdbms-default', + version: '14.6', + }); + + const { queryAllByText, queryByTestId } = renderWithTheme( + + ); + + await waitFor(async () => { + expect(queryAllByText('PostgreSQL v14.6')).toHaveLength(1); + expect(queryByTestId('maintenance-link')).toBeNull(); + }); + }); + + it('should render V2 GA view legacy db', async () => { + spy.mockReturnValue(v2GA()); + + const database = databaseFactory.build({ + engine: 'postgresql', + platform: 'rdbms-legacy', + version: '14.6', + }); + + const { queryAllByText, queryByTestId } = renderWithTheme( + + ); + + await waitFor(async () => { + expect(queryAllByText('PostgreSQL v14.6')).toHaveLength(1); + expect(queryByTestId('maintenance-link')).toBeNull(); + }); + }); + + it('should render V2 GA view default db without pendingUpdates', async () => { + spy.mockReturnValue(v2GA()); + + const database = databaseFactory.build({ + engine: 'mysql', + platform: 'rdbms-default', + updates: { + pending: [], + }, + version: '8.0.30', + }); + + const { queryAllByText, queryByTestId } = renderWithTheme( + + ); + + await waitFor(async () => { + expect(queryAllByText('MySQL v8.0.30')).toHaveLength(1); + expect(queryByTestId('maintenance-link')).toBeNull(); + }); + }); + + it('should render V2 GA view default db with pendingUpdates', async () => { + spy.mockReturnValue(v2GA()); + + const database = databaseFactory.build({ + engine: 'postgresql', + platform: 'rdbms-default', + version: '14.6', + }); + + const { queryAllByText, queryByTestId } = renderWithTheme( + + ); + + await waitFor(async () => { + expect(queryAllByText('PostgreSQL v14.6')).toHaveLength(1); + expect(queryByTestId('maintenance-link')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/src/features/Databases/DatabaseEngineVersion.tsx b/packages/manager/src/features/Databases/DatabaseEngineVersion.tsx new file mode 100644 index 00000000000..ded04420263 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseEngineVersion.tsx @@ -0,0 +1,65 @@ +import { styled } from '@mui/material'; +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; + +import { + getDatabasesDescription, + hasPendingUpdates, + isDefaultDatabase, + useIsDatabasesEnabled, +} from './utilities'; + +import type { + Engine, + PendingUpdates, + Platform, +} from '@linode/api-v4/lib/databases'; + +interface Props { + databaseEngine: Engine; + databaseID: number; + databasePendingUpdates?: PendingUpdates[]; + databasePlatform?: Platform; + databaseVersion: string; +} + +export const DatabaseEngineVersion = (props: Props) => { + const { + databaseEngine: engine, + databaseID, + databasePendingUpdates, + databasePlatform: platform, + databaseVersion: version, + } = props; + const engineVersion = getDatabasesDescription({ engine, version }); + + const { isDatabasesV2GA } = useIsDatabasesEnabled(); + const isDefaultGA = isDatabasesV2GA && isDefaultDatabase({ platform }); + const hasUpdates = hasPendingUpdates(databasePendingUpdates); + + return ( + <> + {engineVersion} + {isDefaultGA && hasUpdates && ( + + + + )} + + ); +}; + +export const StyledLink = styled(Link)(({ theme }) => ({ + alignItems: 'center', + display: 'inline-flex', + marginLeft: theme.spacing(0.5), +})); diff --git a/packages/manager/src/features/Databases/utilities.test.ts b/packages/manager/src/features/Databases/utilities.test.ts index 6960014fda6..3d2e93a6c07 100644 --- a/packages/manager/src/features/Databases/utilities.test.ts +++ b/packages/manager/src/features/Databases/utilities.test.ts @@ -1,18 +1,27 @@ import { renderHook, waitFor } from '@testing-library/react'; import { DateTime } from 'luxon'; -import { AccountCapability } from '@linode/api-v4'; -import { accountFactory, databaseTypeFactory } from 'src/factories'; -import { TimeOption } from 'src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackups'; import { + accountFactory, + databaseFactory, + databaseTypeFactory, +} from 'src/factories'; +import { + getDatabasesDescription, isDateOutsideBackup, + isDefaultDatabase, + isLegacyDatabase, isTimeOutsideBackup, toISOString, + upgradableVersions, useIsDatabasesEnabled, } from 'src/features/Databases/utilities'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { wrapWithTheme } from 'src/utilities/testHelpers'; +import type { AccountCapability, Database, Engine } from '@linode/api-v4'; +import type { TimeOption } from 'src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackups'; + const setup = (capabilities: AccountCapability[], flags: any) => { const account = accountFactory.build({ capabilities }); @@ -376,7 +385,7 @@ describe('isTimeOutsideBackup', () => { describe('toISOString', () => { it('should convert a date and time to ISO string format', () => { - const selectedDate = DateTime.fromObject({ year: 2023, month: 5, day: 15 }); + const selectedDate = DateTime.fromObject({ day: 15, month: 5, year: 2023 }); const selectedTime: TimeOption = { label: '02:00', value: 14 }; const result = toISOString(selectedDate, selectedTime.value); expect(result).toContain('2023-05-15T14:00'); @@ -384,9 +393,9 @@ describe('toISOString', () => { it('should handle midnight correctly', () => { const selectedDate = DateTime.fromObject({ - year: 2023, - month: 12, day: 31, + month: 12, + year: 2023, }); const selectedTime: TimeOption = { label: '12:00 AM', value: 0 }; const result = toISOString(selectedDate, selectedTime.value); @@ -394,9 +403,114 @@ describe('toISOString', () => { }); it('should handle noon correctly', () => { - const selectedDate = DateTime.fromObject({ year: 2024, month: 1, day: 1 }); + const selectedDate = DateTime.fromObject({ day: 1, month: 1, year: 2024 }); const selectedTime: TimeOption = { label: '12:00 PM', value: 12 }; const result = toISOString(selectedDate, selectedTime.value); expect(result).toContain('2024-01-01T12:00'); }); }); + +describe('getDatabasesDescription', () => { + it('should return MySQL', () => { + const result = getDatabasesDescription({ + engine: 'mysql', + version: '8.0.30', + }); + expect(result).toEqual('MySQL v8.0.30'); + }); + + it('should return PostgreSQL', () => { + const db: Database = databaseFactory.build({ + engine: 'postgresql', + version: '14.13', + }); + const result = getDatabasesDescription(db); + expect(result).toEqual('PostgreSQL v14.13'); + }); +}); + +describe('isDefaultDatabase', () => { + it('should return true for default platform database', () => { + const db: Database = databaseFactory.build({ + platform: 'rdbms-default', + }); + const result = isDefaultDatabase(db); + expect(result).toBe(true); + }); + + it('should return false for legacy platform database', () => { + const db: Database = databaseFactory.build({ + platform: 'rdbms-legacy', + }); + const result = isDefaultDatabase(db); + expect(result).toBe(false); + expect(isDefaultDatabase({ platform: undefined })).toBe(false); + }); +}); + +describe('isLegacyDatabase', () => { + it('should return true for legacy databases', () => { + expect(isLegacyDatabase({ platform: 'rdbms-legacy' })).toBe(true); + }); + + it('should return true fro undefined platform', () => { + expect(isLegacyDatabase({ platform: undefined })).toBe(true); + }); + + it('should return false for non-legacy databases', () => { + expect(isLegacyDatabase({ platform: 'rdbms-default' })).toBe(false); + }); +}); + +describe('upgradableVersions', () => { + const mockEngines = [ + { + engine: 'mysql' as Engine, + id: 'mysql/8', + version: '8', + }, + { + engine: 'postgresql' as Engine, + id: 'postgresql/13', + version: '13', + }, + { + engine: 'postgresql' as Engine, + id: 'postgresql/14', + version: '14', + }, + { + engine: 'postgresql' as Engine, + id: 'postgresql/15', + version: '15', + }, + { + engine: 'postgresql' as Engine, + id: 'postgresql/16', + version: '16', + }, + ]; + + it('should return engines with higher versions for the same engine type', () => { + const result = upgradableVersions('postgresql', '14.0.26', mockEngines); + expect(result).toHaveLength(2); + expect(result![0].version).toBe('15'); + }); + + it('should return empty array when no upgrades are available', () => { + const result = upgradableVersions('mysql', '8.0.30', mockEngines); + expect(result).toHaveLength(0); + }); + + it('should only return engines of the same type', () => { + const result = upgradableVersions('postgresql', '14.13.0', mockEngines); + expect(result?.every((engine) => engine.engine === 'postgresql')).toBe( + true + ); + }); + + it('should return undefined when no engines are provided', () => { + const result = upgradableVersions('mysql', '8.0.26', undefined); + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/manager/src/features/Databases/utilities.ts b/packages/manager/src/features/Databases/utilities.ts index 2d3179964c0..062f065f3e8 100644 --- a/packages/manager/src/features/Databases/utilities.ts +++ b/packages/manager/src/features/Databases/utilities.ts @@ -5,9 +5,12 @@ import { useAccount } from 'src/queries/account/account'; import { useDatabaseTypesQuery } from 'src/queries/databases/databases'; import { isFeatureEnabledV2 } from 'src/utilities/accountCapabilities'; -import { databaseEngineMap } from './DatabaseLanding/DatabaseRow'; - -import type { DatabaseInstance } from '@linode/api-v4'; +import type { + DatabaseEngine, + DatabaseInstance, + Engine, + PendingUpdates, +} from '@linode/api-v4'; import type { DatabaseFork } from '@linode/api-v4'; export interface IsDatabasesEnabled { @@ -213,6 +216,35 @@ export const toDatabaseFork = ( return fork; }; -export const getDatabasesDescription = (database: DatabaseInstance) => { +export const databaseEngineMap: Record = { + mongodb: 'MongoDB', + mysql: 'MySQL', + postgresql: 'PostgreSQL', + redis: 'Redis', +}; + +export const getDatabasesDescription = ( + database: Pick +) => { return `${databaseEngineMap[database.engine]} v${database.version}`; }; + +export const hasPendingUpdates = (pendingUpdates?: PendingUpdates[]) => + pendingUpdates?.some((update) => update.deadline || update.planned_for); + +export const isDefaultDatabase = ( + database: Pick +) => database.platform === 'rdbms-default'; + +export const isLegacyDatabase = ( + database: Pick +) => !database.platform || database.platform === 'rdbms-legacy'; + +export const upgradableVersions = ( + engine: Engine, + version: string, + engines?: Pick[] +) => + engines + ?.filter((e) => e.engine === engine) + ?.filter((e) => e.version > version); From 9ba2e1fd0e2bafb3fd59ca95b3aa60cd316169fd Mon Sep 17 00:00:00 2001 From: Conal Ryan Date: Thu, 31 Oct 2024 14:30:18 -0400 Subject: [PATCH 2/2] feat: [UIE-8194] - DBaaS major and minor upgrades 3 --- ...r-11198-upcoming-features-1730465952580.md | 5 + .../DatabaseSettingsMaintenance.test.tsx | 145 ++++++++++++++++ .../DatabaseSettingsMaintenance.tsx | 84 ++++++++++ ...tabaseSettingsReviewUpdatesDialog.test.tsx | 66 ++++++++ .../DatabaseSettingsReviewUpdatesDialog.tsx | 100 +++++++++++ ...abaseSettingsUpgradeVersionDialog.test.tsx | 96 +++++++++++ .../DatabaseSettingsUpgradeVersionDialog.tsx | 155 ++++++++++++++++++ .../Databases/DatabaseEngineVersion.tsx | 2 +- .../src/features/Databases/utilities.ts | 15 +- 9 files changed, 659 insertions(+), 9 deletions(-) create mode 100644 packages/manager/.changeset/pr-11198-upcoming-features-1730465952580.md create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMaintenance.test.tsx create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMaintenance.tsx create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsReviewUpdatesDialog.test.tsx create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsReviewUpdatesDialog.tsx create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsUpgradeVersionDialog.test.tsx create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsUpgradeVersionDialog.tsx diff --git a/packages/manager/.changeset/pr-11198-upcoming-features-1730465952580.md b/packages/manager/.changeset/pr-11198-upcoming-features-1730465952580.md new file mode 100644 index 00000000000..cd7c2c8e48f --- /dev/null +++ b/packages/manager/.changeset/pr-11198-upcoming-features-1730465952580.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +DBaaS add new Maintenance component, Upgrade version dialog, Review udpates dialog ([#11198](https://github.com/linode/manager/pull/11198)) diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMaintenance.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMaintenance.test.tsx new file mode 100644 index 00000000000..2672c02b16d --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMaintenance.test.tsx @@ -0,0 +1,145 @@ +import React from 'react'; + +import { databaseFactory } from 'src/factories'; +import { DatabaseSettingsMaintenance } from 'src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMaintenance'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import type { Engine } from '@linode/api-v4'; + +const UPGRADE_VERSION = 'Upgrade Version'; + +const queryMocks = vi.hoisted(() => ({ + useDatabaseEnginesQuery: vi.fn().mockReturnValue({ + data: [ + { + engine: 'mysql' as Engine, + id: 'mysql/8', + version: '8', + }, + { + engine: 'postgresql' as Engine, + id: 'postgresql/13', + version: '13', + }, + { + engine: 'postgresql' as Engine, + id: 'postgresql/14', + version: '14', + }, + { + engine: 'postgresql' as Engine, + id: 'postgresql/15', + version: '15', + }, + { + engine: 'postgresql' as Engine, + id: 'postgresql/16', + version: '16', + }, + ], + }), +})); + +vi.mock('src/queries/databases/databases', async () => { + const actual = await vi.importActual('src/queries/databases/databases'); + return { + ...actual, + useDatabaseEnginesQuery: queryMocks.useDatabaseEnginesQuery, + }; +}); + +describe('Database Settings Maintenance', () => { + it('should disable upgrade version modal button when there are no upgrades available', async () => { + const database = databaseFactory.build({ + engine: 'mysql', + version: '8.0.30', + }); + + const onReviewUpdates = vi.fn(); + const onUpgradeVersion = vi.fn(); + + const { findByRole } = renderWithTheme( + + ); + + const button = await findByRole('button', { name: UPGRADE_VERSION }); + + expect(button).toBeDisabled(); + }); + + it('should enable upgrade version modal button when there are upgrades available', async () => { + const database = databaseFactory.build({ + engine: 'postgresql', + version: '13', + }); + + const onReviewUpdates = vi.fn(); + const onUpgradeVersion = vi.fn(); + + const { findByRole } = renderWithTheme( + + ); + + const button = await findByRole('button', { name: UPGRADE_VERSION }); + + expect(button).toBeEnabled(); + }); + + it('should show review text and modal button when there are updates', async () => { + const database = databaseFactory.build(); + + const onReviewUpdates = vi.fn(); + const onUpgradeVersion = vi.fn(); + + const { findByRole } = renderWithTheme( + + ); + + const button = await findByRole('button', { name: UPGRADE_VERSION }); + + expect(button).toBeEnabled(); + }); + + it('should not show review text and modal button when there are no updates', async () => { + const database = databaseFactory.build({ + updates: { + pending: [], + }, + }); + + const onReviewUpdates = vi.fn(); + const onUpgradeVersion = vi.fn(); + + const { queryByRole } = renderWithTheme( + + ); + + const button = queryByRole('button', { name: 'Click to review' }); + + expect(button).not.toBeInTheDocument(); + }); +}); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMaintenance.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMaintenance.tsx new file mode 100644 index 00000000000..a128e14dacf --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMaintenance.tsx @@ -0,0 +1,84 @@ +import { Grid, styled } from '@mui/material'; +import * as React from 'react'; + +import { StyledLinkButton } from 'src/components/Button/StyledLinkButton'; +import { Typography } from 'src/components/Typography'; +import { + getDatabasesDescription, + hasPendingUpdates, + upgradableVersions, +} from 'src/features/Databases/utilities'; +import { useDatabaseEnginesQuery } from 'src/queries/databases/databases'; + +import type { Engine, PendingUpdates } from '@linode/api-v4'; + +interface Props { + databaseEngine: Engine; + databasePendingUpdates?: PendingUpdates[]; + databaseVersion: string; + onReviewUpdates: () => void; + onUpgradeVersion: () => void; +} + +export const DatabaseSettingsMaintenance = (props: Props) => { + const { + databaseEngine: engine, + databasePendingUpdates, + databaseVersion: version, + onReviewUpdates, + onUpgradeVersion, + } = props; + const engineVersion = getDatabasesDescription({ engine, version }); + const { data: engines } = useDatabaseEnginesQuery(true); + const versions = upgradableVersions(engine, version, engines); + const hasUpdates = hasPendingUpdates(databasePendingUpdates); + + return ( + + + Maintenance + Version + {engineVersion} + + Upgrade Version + + + + {/* + TODO Uncomment and provide value when the EOL is returned by the API. + Currently, it is not supported, however they are working on returning it since it has value to the end user + End of life + */} + + + Maintenance updates + {hasUpdates ? ( + + One or more minor version upgrades or patches will be applied during + the next maintenance window.{' '} + + Click to review + + + ) : ( + + There are no minor version upgrades or patches planned for the next + maintenance window.{' '} + + )} + + + ); +}; + +const StyledTypography = styled(Typography)(({ theme }) => ({ + marginBottom: theme.spacing(0.25), +})); + +const BoldTypography = styled(StyledTypography)(({ theme }) => ({ + fontFamily: theme.font.bold, +})); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsReviewUpdatesDialog.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsReviewUpdatesDialog.test.tsx new file mode 100644 index 00000000000..7911e3feb0d --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsReviewUpdatesDialog.test.tsx @@ -0,0 +1,66 @@ +import React from 'react'; + +import { databaseFactory } from 'src/factories/databases'; +import { DatabaseSettingsReviewUpdatesDialog } from 'src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsReviewUpdatesDialog'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +describe('Database Settings Review Updates Dialog', () => { + it('should list updates', async () => { + const database = databaseFactory.build({ + updates: { + pending: [ + { + deadline: null, + description: 'Update a', + planned_for: '2044-09-15T17:15:12', + }, + { + deadline: null, + description: 'Update b', + planned_for: '2044-09-15T17:15:12', + }, + ], + }, + }); + + const onClose = vi.fn(); + + const { findByText } = renderWithTheme( + + ); + + const a = await findByText('Update a'); + const b = await findByText('Update b'); + + expect(a).toBeDefined(); + expect(b).toBeDefined(); + }); + + it('should enable buttons', async () => { + const database = databaseFactory.build(); + + const onClose = vi.fn(); + + const { findByTestId } = renderWithTheme( + + ); + + const start = await findByTestId('start'); + const close = await findByTestId('close'); + + expect(start).toBeEnabled(); + expect(close).toBeEnabled(); + }); +}); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsReviewUpdatesDialog.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsReviewUpdatesDialog.tsx new file mode 100644 index 00000000000..e531dc963c7 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsReviewUpdatesDialog.tsx @@ -0,0 +1,100 @@ +import { useTheme } from '@mui/material'; +import { useSnackbar } from 'notistack'; +import * as React from 'react'; + +import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; +import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; +import { Notice } from 'src/components/Notice/Notice'; +import { Typography } from 'src/components/Typography'; +import { usePatchDatabaseMutation } from 'src/queries/databases/databases'; +import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; + +import type { Engine, PendingUpdates } from '@linode/api-v4/lib/databases'; + +interface Props { + databaseEngine: Engine; + databaseID: number; + databasePendingUpdates?: PendingUpdates[]; + onClose: () => void; + open: boolean; +} + +export const DatabaseSettingsReviewUpdatesDialog = (props: Props) => { + const { + databaseEngine, + databaseID, + databasePendingUpdates, + onClose, + open, + } = props; + const theme = useTheme(); + const { enqueueSnackbar } = useSnackbar(); + const { mutateAsync: patchDatabase } = usePatchDatabaseMutation( + databaseEngine, + databaseID + ); + + const [error, setError] = React.useState(''); + const [loading, setIsLoading] = React.useState(false); + + const onStartMaintenance = () => { + setIsLoading(true); + patchDatabase() + .then(() => { + setIsLoading(false); + enqueueSnackbar('Database maintenance started successfully.', { + variant: 'success', + }); + onClose(); + }) + .catch((e) => { + setIsLoading(false); + setError( + getAPIErrorOrDefault(e, 'There was an error starting maintenance.')[0] + .reason + ); + }); + }; + + const renderActions = ( + + ); + + return ( + + {error && } + + During the maintenance there is a brief service interruption. + + {databasePendingUpdates?.length && ( +
    + {databasePendingUpdates.map((update) => ( +
  • {update.description}
  • + ))} +
+ )} +
+ ); +}; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsUpgradeVersionDialog.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsUpgradeVersionDialog.test.tsx new file mode 100644 index 00000000000..f537685046b --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsUpgradeVersionDialog.test.tsx @@ -0,0 +1,96 @@ +import React from 'react'; + +import { databaseFactory } from 'src/factories/databases'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { DatabaseSettingsUpgradeVersionDialog } from './DatabaseSettingsUpgradeVersionDialog'; + +import type { Engine } from '@linode/api-v4'; + +const queryMocks = vi.hoisted(() => ({ + useDatabaseEnginesQuery: vi.fn().mockReturnValue({ + data: [ + { + engine: 'mysql' as Engine, + id: 'mysql/8', + version: '8', + }, + { + engine: 'postgresql' as Engine, + id: 'postgresql/13', + version: '13', + }, + { + engine: 'postgresql' as Engine, + id: 'postgresql/14', + version: '14', + }, + { + engine: 'postgresql' as Engine, + id: 'postgresql/15', + version: '15', + }, + { + engine: 'postgresql' as Engine, + id: 'postgresql/16', + version: '16', + }, + ], + }), +})); + +vi.mock('src/queries/databases/databases', async () => { + const actual = await vi.importActual('src/queries/databases/databases'); + return { + ...actual, + useDatabaseEnginesQuery: queryMocks.useDatabaseEnginesQuery, + }; +}); + +describe('Database Settings Upgrade Version Dialog', () => { + it('should display warning', async () => { + const database = databaseFactory.build(); + + const onClose = vi.fn(); + + const { findByText } = renderWithTheme( + + ); + + const warning = await findByText( + 'Reverting back to the prior version is not possible once the upgrade has been started' + ); + + expect(warning).toBeInTheDocument(); + }); + + it('should disable upgrade button when no selectedVersion', async () => { + const database = databaseFactory.build(); + + const onClose = vi.fn(); + + const { findByTestId } = renderWithTheme( + + ); + + const upgrade = await findByTestId('upgrade'); + const cancel = await findByTestId('cancel'); + + expect(upgrade).toBeDisabled(); + expect(cancel).toBeEnabled(); + }); +}); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsUpgradeVersionDialog.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsUpgradeVersionDialog.tsx new file mode 100644 index 00000000000..f42d1777e40 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsUpgradeVersionDialog.tsx @@ -0,0 +1,155 @@ +import { FormControl } from '@linode/ui'; +import { useTheme } from '@mui/material'; +import { useSnackbar } from 'notistack'; +import * as React from 'react'; + +import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; +import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; +import { Notice } from 'src/components/Notice/Notice'; +import { Typography } from 'src/components/Typography'; +import { + DATABASE_ENGINE_MAP, + upgradableVersions, +} from 'src/features/Databases/utilities'; +import { + useDatabaseEnginesQuery, + useDatabaseMutation, +} from 'src/queries/databases/databases'; +import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; + +import type { Engine } from '@linode/api-v4/lib/databases'; + +interface Props { + databaseEngine: Engine; + databaseID: number; + databaseLabel: string; + databaseVersion: string; + onClose: () => void; + open: boolean; +} + +interface VersionOption { + label: string; + value: string; +} + +export const DatabaseSettingsUpgradeVersionDialog = (props: Props) => { + const { + databaseEngine, + databaseID, + databaseLabel, + databaseVersion, + onClose, + open, + } = props; + const theme = useTheme(); + const { enqueueSnackbar } = useSnackbar(); + const { mutateAsync: updateDatabase } = useDatabaseMutation( + databaseEngine, + databaseID + ); + const { data: engines } = useDatabaseEnginesQuery(true); + + const versions = upgradableVersions( + databaseEngine, + databaseVersion, + engines + )?.map((engine) => { + return { + label: `v${engine.version}`, + value: engine.version, + }; + }); + + const [ + selectedVersion, + setSelectedVersion, + ] = React.useState(null); + const [error, setError] = React.useState(''); + const [loading, setIsLoading] = React.useState(false); + + const dialogTitle = `${DATABASE_ENGINE_MAP[databaseEngine]} on ${databaseLabel}`; + const defaultError = 'There was an error upgrading this version.'; + + const onUpgradeVersion = () => { + if (!selectedVersion) { + return; + } + setIsLoading(true); + updateDatabase({ version: selectedVersion.value }) + .then(() => { + setIsLoading(false); + enqueueSnackbar('Database version upgraded successfully.', { + variant: 'success', + }); + onClose(); + }) + .catch((e) => { + setIsLoading(false); + setError(getAPIErrorOrDefault(e, defaultError)[0].reason); + }); + }; + + const renderActions = ( + + ); + + return ( + + {error && } + + Current Version: v{databaseVersion} + + + Please select the new MySQL version. Once you select the new version we + will check it for compatibility with your current version. If it is + compatible you can proceed with the upgrade. + + + + setSelectedVersion(v)} + options={versions ?? []} + placeholder="Select a version" + value={selectedVersion} + /> + + + {loading && ( + + + Checking version upgrade compatibility, then will start upgrade + + {/* Then the text changes to "Starting to upgrade." then closes after 1 second */} + + )} + + + Reverting back to the prior version is not possible once the upgrade + has been started + + + + ); +}; diff --git a/packages/manager/src/features/Databases/DatabaseEngineVersion.tsx b/packages/manager/src/features/Databases/DatabaseEngineVersion.tsx index ded04420263..5e613a7e591 100644 --- a/packages/manager/src/features/Databases/DatabaseEngineVersion.tsx +++ b/packages/manager/src/features/Databases/DatabaseEngineVersion.tsx @@ -58,7 +58,7 @@ export const DatabaseEngineVersion = (props: Props) => { ); }; -export const StyledLink = styled(Link)(({ theme }) => ({ +const StyledLink = styled(Link)(({ theme }) => ({ alignItems: 'center', display: 'inline-flex', marginLeft: theme.spacing(0.5), diff --git a/packages/manager/src/features/Databases/utilities.ts b/packages/manager/src/features/Databases/utilities.ts index 062f065f3e8..4736d94b4fa 100644 --- a/packages/manager/src/features/Databases/utilities.ts +++ b/packages/manager/src/features/Databases/utilities.ts @@ -216,21 +216,23 @@ export const toDatabaseFork = ( return fork; }; -export const databaseEngineMap: Record = { +export const DATABASE_ENGINE_MAP: Record = { mongodb: 'MongoDB', mysql: 'MySQL', postgresql: 'PostgreSQL', redis: 'Redis', -}; +} as const; export const getDatabasesDescription = ( database: Pick ) => { - return `${databaseEngineMap[database.engine]} v${database.version}`; + return `${DATABASE_ENGINE_MAP[database.engine]} v${database.version}`; }; export const hasPendingUpdates = (pendingUpdates?: PendingUpdates[]) => - pendingUpdates?.some((update) => update.deadline || update.planned_for); + Boolean( + pendingUpdates?.some((update) => update.deadline || update.planned_for) + ); export const isDefaultDatabase = ( database: Pick @@ -244,7 +246,4 @@ export const upgradableVersions = ( engine: Engine, version: string, engines?: Pick[] -) => - engines - ?.filter((e) => e.engine === engine) - ?.filter((e) => e.version > version); +) => engines?.filter((e) => e.engine === engine && e.version > version);