From 272b677fba16c51fabaa60d8c0c48760636da136 Mon Sep 17 00:00:00 2001 From: Conal Ryan Date: Tue, 8 Oct 2024 11:12:07 -0400 Subject: [PATCH] feat: [UIE-8074] - DBaaS GA Summary tab --- ...r-11091-upcoming-features-1728668394698.md | 5 + .../DatabaseBackups/DatabaseBackups.tsx | 8 +- .../DatabaseSummary/DatabaseSummary.test.tsx | 215 ++++++++++ .../DatabaseSummary/DatabaseSummary.tsx | 50 ++- .../DatabaseSummaryAccessControls.tsx | 18 - ...tabaseSummaryClusterConfiguration.style.ts | 51 +++ ...tabaseSummaryClusterConfiguration.test.tsx | 201 ++++++++++ .../DatabaseSummaryClusterConfiguration.tsx | 131 +++---- .../DatabaseSummaryConnectionDetails.style.ts | 99 +++++ .../DatabaseSummaryConnectionDetails.test.tsx | 129 ++++++ .../DatabaseSummaryConnectionDetails.tsx | 329 ++++------------ ...abaseSummaryClusterConfigurationLegacy.tsx | 146 +++++++ ...DatabaseSummaryConnectionDetailsLegacy.tsx | 370 ++++++++++++++++++ .../src/features/Databases/utilities.ts | 39 +- 14 files changed, 1422 insertions(+), 369 deletions(-) create mode 100644 packages/manager/.changeset/pr-11091-upcoming-features-1728668394698.md create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.test.tsx delete mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryAccessControls.tsx create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.style.ts create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.test.tsx create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.style.ts create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.test.tsx create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryClusterConfigurationLegacy.tsx create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryConnectionDetailsLegacy.tsx diff --git a/packages/manager/.changeset/pr-11091-upcoming-features-1728668394698.md b/packages/manager/.changeset/pr-11091-upcoming-features-1728668394698.md new file mode 100644 index 00000000000..142cb841d0b --- /dev/null +++ b/packages/manager/.changeset/pr-11091-upcoming-features-1728668394698.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +DBaaS GA summary tab enhancements ([#11091](https://github.com/linode/manager/pull/11091)) diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackups.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackups.tsx index c150abd0bfa..a29b00fa088 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackups.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackups.tsx @@ -136,7 +136,7 @@ export const DatabaseBackups = (props: Props) => { Summary Databases are automatically backed-up with full daily backups for the - past 10 days, and binary logs recorded continuously. Full backups are + past 14 days, and binary logs recorded continuously. Full backups are version-specific binary backups, which when combined with binary logs allow for consistent recovery to a specific point in time (PITR). @@ -146,13 +146,13 @@ export const DatabaseBackups = (props: Props) => { {isDatabasesV2GA ? ( The newest full backup plus incremental is selected by default. Or, - select any date and time within the last 10 days you want to create + select any date and time within the last 14 days you want to create a fork from. ) : ( - Select a date and time within the last 10 days you want to create a - forkfrom. + Select a date and time within the last 14 days you want to create a + fork from. )} diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.test.tsx new file mode 100644 index 00000000000..b8c05182ff1 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.test.tsx @@ -0,0 +1,215 @@ +import { waitFor } from '@testing-library/react'; +import * as React from 'react'; +import { vi } from 'vitest'; + +import { databaseFactory } from 'src/factories'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import * as utils from '../../utilities'; +import { DatabaseSummary } from './DatabaseSummary'; + +import type { Database } from '@linode/api-v4'; + +const CLUSTER_CONFIGURATION = 'Cluster Configuration'; +const THREE_NODE = 'Primary +2 replicas'; +const TWO_NODE = 'Primary +1 replicas'; +const VERSION = 'Version'; + +const CONNECTION_DETAILS = 'Connection Details'; +const PRIVATE_NETWORK_HOST = 'Private Network Host'; +const PRIVATE_NETWORK_HOST_LABEL = 'private network host'; +const READONLY_HOST_LABEL = 'read-only host'; +const GA_READONLY_HOST_LABEL = 'Read-only Host'; + +const ACCESS_CONTROLS = 'Access Controls'; + +const DEFAULT_PLATFORM = 'rdbms-default'; +const DEFAULT_PRIMARY = 'db-mysql-default-primary.net'; +const DEFAULT_STANDBY = 'db-mysql-default-standby.net'; + +const LEGACY_PLATFORM = 'rdbms-legacy'; +const LEGACY_PRIMARY = 'db-mysql-legacy-primary.net'; +const LEGACY_SECONDARY = 'db-mysql-legacy-secondary.net'; + +const spy = vi.spyOn(utils, 'useIsDatabasesEnabled'); +spy.mockReturnValue({ + isDatabasesEnabled: true, + isDatabasesV1Enabled: true, + isDatabasesV2Beta: false, + isDatabasesV2Enabled: true, + isDatabasesV2GA: true, + isUserExistingBeta: false, + isUserNewBeta: false, +}); + +describe('Database Summary', () => { + it('should render V2GA view default db', async () => { + const database = databaseFactory.build({ + cluster_size: 2, + hosts: { + primary: DEFAULT_PRIMARY, + standby: DEFAULT_STANDBY, + }, + platform: DEFAULT_PLATFORM, + }) as Database; + + const { queryAllByText } = renderWithTheme( + + ); + + await waitFor(() => { + expect(queryAllByText(CLUSTER_CONFIGURATION)).toHaveLength(1); + expect(queryAllByText(TWO_NODE)).toHaveLength(1); + expect(queryAllByText(VERSION)).toHaveLength(0); + + expect(queryAllByText(CONNECTION_DETAILS)).toHaveLength(1); + expect(queryAllByText(PRIVATE_NETWORK_HOST)).toHaveLength(0); + expect(queryAllByText(GA_READONLY_HOST_LABEL)).toHaveLength(1); + expect(queryAllByText(DEFAULT_STANDBY)).toHaveLength(1); + + expect(queryAllByText(ACCESS_CONTROLS)).toHaveLength(0); + }); + }); + + it('should render V2GA view legacy db', async () => { + const database = databaseFactory.build({ + cluster_size: 3, + hosts: { + primary: LEGACY_PRIMARY, + secondary: LEGACY_SECONDARY, + }, + platform: LEGACY_PLATFORM, + }) as Database; + + const { queryAllByText } = renderWithTheme( + + ); + + await waitFor(() => { + expect(queryAllByText(CLUSTER_CONFIGURATION)).toHaveLength(1); + expect(queryAllByText(THREE_NODE)).toHaveLength(1); + expect(queryAllByText(VERSION)).toHaveLength(0); + + expect(queryAllByText(CONNECTION_DETAILS)).toHaveLength(1); + expect(queryAllByText(PRIVATE_NETWORK_HOST)).toHaveLength(1); + expect(queryAllByText(GA_READONLY_HOST_LABEL)).toHaveLength(0); + expect(queryAllByText(LEGACY_SECONDARY)).toHaveLength(1); + + expect(queryAllByText(ACCESS_CONTROLS)).toHaveLength(0); + }); + }); + + it('should render Beta view default db', async () => { + spy.mockReturnValue({ + isDatabasesEnabled: true, + isDatabasesV1Enabled: true, + isDatabasesV2Beta: true, + isDatabasesV2Enabled: true, + isDatabasesV2GA: false, + isUserExistingBeta: true, + isUserNewBeta: false, + }); + const database = databaseFactory.build({ + cluster_size: 2, + hosts: { + primary: DEFAULT_PRIMARY, + secondary: undefined, + standby: DEFAULT_STANDBY, + }, + platform: DEFAULT_PLATFORM, + }) as Database; + + const { queryAllByText } = renderWithTheme( + + ); + + await waitFor(() => { + expect(queryAllByText(CLUSTER_CONFIGURATION)).toHaveLength(1); + expect(queryAllByText(TWO_NODE)).toHaveLength(1); + expect(queryAllByText(VERSION)).toHaveLength(1); + + expect(queryAllByText(CONNECTION_DETAILS)).toHaveLength(1); + expect(queryAllByText(PRIVATE_NETWORK_HOST_LABEL)).toHaveLength(0); + expect(queryAllByText(READONLY_HOST_LABEL)).toHaveLength(1); + expect(queryAllByText(/db-mysql-default-standby.net/)).toHaveLength(1); + + expect(queryAllByText(ACCESS_CONTROLS)).toHaveLength(1); + }); + }); + + it('should render Beta view legacy db', async () => { + spy.mockReturnValue({ + isDatabasesEnabled: true, + isDatabasesV1Enabled: true, + isDatabasesV2Beta: true, + isDatabasesV2Enabled: true, + isDatabasesV2GA: false, + isUserExistingBeta: true, + isUserNewBeta: false, + }); + const database = databaseFactory.build({ + cluster_size: 3, + hosts: { + primary: LEGACY_PRIMARY, + secondary: LEGACY_SECONDARY, + standby: undefined, + }, + platform: LEGACY_PLATFORM, + }) as Database; + + const { queryAllByText } = renderWithTheme( + + ); + + await waitFor(() => { + expect(queryAllByText(CLUSTER_CONFIGURATION)).toHaveLength(1); + expect(queryAllByText(THREE_NODE)).toHaveLength(1); + expect(queryAllByText(VERSION)).toHaveLength(1); + + expect(queryAllByText(CONNECTION_DETAILS)).toHaveLength(1); + expect(queryAllByText(PRIVATE_NETWORK_HOST_LABEL)).toHaveLength(1); + expect(queryAllByText(READONLY_HOST_LABEL)).toHaveLength(0); + expect(queryAllByText(/db-mysql-legacy-secondary.net/)).toHaveLength(1); + + expect(queryAllByText(ACCESS_CONTROLS)).toHaveLength(1); + }); + }); + + it('should render V1 view legacy db', async () => { + spy.mockReturnValue({ + isDatabasesEnabled: true, + isDatabasesV1Enabled: true, + isDatabasesV2Beta: false, + isDatabasesV2Enabled: false, + isDatabasesV2GA: false, + isUserExistingBeta: false, + isUserNewBeta: false, + }); + const database = databaseFactory.build({ + cluster_size: 3, + hosts: { + primary: LEGACY_PRIMARY, + secondary: LEGACY_SECONDARY, + standby: undefined, + }, + platform: LEGACY_PLATFORM, + }) as Database; + + const { queryAllByText } = renderWithTheme( + + ); + + await waitFor(() => { + expect(queryAllByText(CLUSTER_CONFIGURATION)).toHaveLength(1); + expect(queryAllByText(THREE_NODE)).toHaveLength(1); + expect(queryAllByText(VERSION)).toHaveLength(1); + + expect(queryAllByText(CONNECTION_DETAILS)).toHaveLength(1); + expect(queryAllByText(PRIVATE_NETWORK_HOST_LABEL)).toHaveLength(1); + expect(queryAllByText(READONLY_HOST_LABEL)).toHaveLength(0); + expect(queryAllByText(/db-mysql-legacy-secondary.net/)).toHaveLength(1); + + expect(queryAllByText(ACCESS_CONTROLS)).toHaveLength(1); + }); + }); +}); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.tsx index 37e50ebbdc2..f93c7f8fc0d 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.tsx @@ -5,10 +5,12 @@ import { Divider } from 'src/components/Divider'; import { Link } from 'src/components/Link'; import { Paper } from 'src/components/Paper'; import { Typography } from 'src/components/Typography'; - -import AccessControls from '../AccessControls'; -import ClusterConfiguration from './DatabaseSummaryClusterConfiguration'; -import ConnectionDetails from './DatabaseSummaryConnectionDetails'; +import AccessControls from 'src/features/Databases/DatabaseDetail/AccessControls'; +import ClusterConfiguration from 'src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration'; +import ConnectionDetails from 'src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails'; +import ClusterConfigurationLegacy from 'src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryClusterConfigurationLegacy'; +import ConnectionDetailsLegacy from 'src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryConnectionDetailsLegacy'; +import { useIsDatabasesEnabled } from 'src/features/Databases/utilities'; import type { Database } from '@linode/api-v4/lib/databases/types'; @@ -19,6 +21,7 @@ interface Props { export const DatabaseSummary: React.FC = (props) => { const { database, disabled } = props; + const { isDatabasesV2GA } = useIsDatabasesEnabled(); const description = ( <> @@ -40,19 +43,38 @@ export const DatabaseSummary: React.FC = (props) => { return ( - - + + {isDatabasesV2GA ? ( + + ) : ( + // Deprecated @since DBaaS V2 GA. Will be removed remove post GA release ~ Dec 2024 + // TODO (UIE-8214) remove POST GA + + )} - - + + {isDatabasesV2GA ? ( + + ) : ( + // Deprecated @since DBaaS V2 GA. Will be removed remove post GA release ~ Dec 2024 + // TODO (UIE-8214) remove POST GA + + )} - - + {!isDatabasesV2GA && ( + // Deprecated @since DBaaS V2 GA. Will be removed remove post GA release ~ Dec 2024 + // AccessControls accessible through dropdown menu on landing page table and on settings tab + // TODO (UIE-8214) remove POST GA + <> + + + + )} ); }; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryAccessControls.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryAccessControls.tsx deleted file mode 100644 index c4eb0c625b6..00000000000 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryAccessControls.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -// import useDatabases from 'src/hooks/useDatabases'; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface Props { - // databaseID: number; -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const DatabaseSummaryAccessControls: React.FC = (props) => { - // const databases = useDatabases(); - // const { databaseID } = props; - // const thisDatabase = databases.databases.itemsById[databaseID]; - - return <> Access Controls ; -}; - -export default DatabaseSummaryAccessControls; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.style.ts b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.style.ts new file mode 100644 index 00000000000..a187dac8e51 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.style.ts @@ -0,0 +1,51 @@ +import { styled } from '@mui/material/styles'; +import Grid2 from '@mui/material/Unstable_Grid2/Grid2'; + +import { Typography } from 'src/components/Typography'; + +export const StyledGridContainer = styled(Grid2, { + label: 'StyledGridContainer', +})(({ theme }) => ({ + '&>*:nth-of-type(even)': { + boxShadow: `inset 0px -1px 0px 0 ${ + theme.palette.mode === 'dark' + ? theme.color.white + : theme.palette.grey[200] + }`, + }, + '&>*:nth-of-type(odd)': { + boxShadow: `inset 0px -1px 0px 0 ${theme.color.white}`, + marginBottom: '1px', + }, + boxShadow: `inset 0 -1px 0 0 ${ + theme.palette.mode === 'dark' ? theme.color.white : theme.palette.grey[200] + }, inset 0 1px 0 0 ${ + theme.palette.mode === 'dark' ? theme.color.white : theme.palette.grey[200] + }, inset -1px 0 0 ${ + theme.palette.mode === 'dark' ? theme.color.white : theme.palette.grey[200] + }`, +})); + +export const StyledLabelTypography = styled(Typography, { + label: 'StyledLabelTypography', +})(({ theme }) => ({ + background: + theme.palette.mode === 'dark' + ? theme.bg.tableHeader + : theme.palette.grey[200], + color: theme.palette.mode === 'dark' ? theme.color.grey6 : 'inherit', + fontFamily: theme.font.bold, + height: '100%', + padding: `${theme.spacing(0.5)} 15px`, +})); + +export const StyledValueGrid = styled(Grid2, { + label: 'StyledValueGrid', +})(({ theme }) => ({ + alignItems: 'center', + color: theme.palette.mode === 'dark' ? theme.color.grey8 : theme.color.black, + display: 'flex', + padding: `0 ${theme.spacing()}`, +})); + +// theme.spacing() 8 diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.test.tsx new file mode 100644 index 00000000000..94231d3d897 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.test.tsx @@ -0,0 +1,201 @@ +import { waitFor } from '@testing-library/react'; +import React from 'react'; + +import { databaseFactory, databaseTypeFactory } from 'src/factories/databases'; +import { regionFactory } from 'src/factories/regions'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { DatabaseSummaryClusterConfiguration } from './DatabaseSummaryClusterConfiguration'; + +import type { Database, DatabaseStatus } from '@linode/api-v4/lib/databases'; + +const STATUS_VALUE = 'Active'; +const PLAN_VALUE = 'New DBaaS - Dedicated 8 GB'; +const NODES_VALUE = 'Primary +1 replicas'; +const REGION_ID = 'us-east'; +const REGION_LABEL = 'Newark, NJ'; + +const DEFAULT_PLATFORM = 'rdbms-default'; +const TYPE = 'g6-dedicated-4'; + +const queryMocks = vi.hoisted(() => ({ + useDatabaseTypesQuery: vi.fn().mockReturnValue({}), + useRegionsQuery: vi.fn().mockReturnValue({}), +})); + +vi.mock('src/queries/regions/regions', () => ({ + useRegionsQuery: queryMocks.useRegionsQuery, +})); + +vi.mock('src/queries/databases/databases', () => ({ + useDatabaseTypesQuery: queryMocks.useDatabaseTypesQuery, +})); + +describe('DatabaseSummaryClusterConfiguration', () => { + it('should display correctly for default db', async () => { + queryMocks.useRegionsQuery.mockReturnValue({ + data: regionFactory.buildList(1, { + country: 'us', + id: REGION_ID, + label: REGION_LABEL, + status: 'ok', + }), + }); + + queryMocks.useDatabaseTypesQuery.mockReturnValue({ + data: databaseTypeFactory.buildList(1, { + class: 'dedicated', + disk: 163840, + id: TYPE, + label: PLAN_VALUE, + memory: 8192, + vcpus: 4, + }), + }); + + const database = databaseFactory.build({ + cluster_size: 2, + engine: 'postgresql', + platform: DEFAULT_PLATFORM, + region: REGION_ID, + status: STATUS_VALUE.toLowerCase() as DatabaseStatus, + total_disk_size_gb: 130, + type: TYPE, + used_disk_size_gb: 6, + version: '16.4', + }) as Database; + + const { queryAllByText } = renderWithTheme( + + ); + + expect(queryMocks.useDatabaseTypesQuery).toHaveBeenCalledWith({ + platform: DEFAULT_PLATFORM, + }); + + await waitFor(() => { + expect(queryAllByText('Status')).toHaveLength(1); + expect(queryAllByText(STATUS_VALUE)).toHaveLength(1); + + expect(queryAllByText('Plan')).toHaveLength(1); + expect(queryAllByText(PLAN_VALUE)).toHaveLength(1); + + expect(queryAllByText('Nodes')).toHaveLength(1); + expect(queryAllByText(NODES_VALUE)).toHaveLength(1); + + expect(queryAllByText('CPUs')).toHaveLength(1); + expect(queryAllByText(4)).toHaveLength(1); + + expect(queryAllByText('Engine')).toHaveLength(1); + expect(queryAllByText('PostgreSQL v16.4')).toHaveLength(1); + + expect(queryAllByText('Region')).toHaveLength(1); + expect(queryAllByText(REGION_LABEL)).toHaveLength(1); + + expect(queryAllByText('RAM')).toHaveLength(1); + expect(queryAllByText('8 GB')).toHaveLength(1); + + expect(queryAllByText('Total Disk Size')).toHaveLength(1); + expect(queryAllByText('130 GB')).toHaveLength(1); + }); + }); + + it('should display correctly for legacy db', async () => { + queryMocks.useRegionsQuery.mockReturnValue({ + data: regionFactory.buildList(1, { + country: 'us', + id: 'us-southeast', + label: 'Atlanta, GA, USA', + status: 'ok', + }), + }); + + queryMocks.useDatabaseTypesQuery.mockReturnValue({ + data: databaseTypeFactory.buildList(1, { + class: 'nanode', + disk: 25600, + id: 'g6-nanode-1', + label: 'DBaaS - Nanode 1GB', + memory: 1024, + vcpus: 1, + }), + }); + + const database = databaseFactory.build({ + cluster_size: 1, + engine: 'mysql', + platform: 'rdbms-legacy', + region: 'us-southeast', + replication_type: 'none', + status: 'provisioning', + total_disk_size_gb: 15, + type: 'g6-nanode-1', + used_disk_size_gb: 2, + version: '8.0.30', + }) as Database; + + const { queryAllByText } = renderWithTheme( + + ); + + expect(queryMocks.useDatabaseTypesQuery).toHaveBeenCalledWith({ + platform: 'rdbms-legacy', + }); + + await waitFor(() => { + expect(queryAllByText('Status')).toHaveLength(1); + expect(queryAllByText('Provisioning')).toHaveLength(1); + + expect(queryAllByText('Plan')).toHaveLength(1); + expect(queryAllByText('Nanode 1 GB')).toHaveLength(1); + + expect(queryAllByText('Nodes')).toHaveLength(1); + expect(queryAllByText('Primary')).toHaveLength(1); + + expect(queryAllByText('CPUs')).toHaveLength(1); + expect(queryAllByText(1)).toHaveLength(1); + + expect(queryAllByText('Engine')).toHaveLength(1); + expect(queryAllByText('MySQL v8.0.30')).toHaveLength(1); + + expect(queryAllByText('Region')).toHaveLength(1); + expect(queryAllByText('Atlanta, GA, USA')).toHaveLength(1); + + expect(queryAllByText('RAM')).toHaveLength(1); + expect(queryAllByText('1 GB')).toHaveLength(1); + + expect(queryAllByText('Total Disk Size')).toHaveLength(1); + expect(queryAllByText('15 GB')).toHaveLength(1); + }); + }); + + it('should return null when there is no matching type', async () => { + queryMocks.useDatabaseTypesQuery.mockReturnValue({ + data: databaseTypeFactory.buildList(1, { + class: 'standard', + disk: 81920, + id: 'g6-standard-2', + label: 'DBaaS - Standard 4GB', + memory: 4096, + vcpus: 2, + }), + }); + + const database = databaseFactory.build({ + platform: 'rdbms-legacy', + type: 'g6-nanode-1', + }) as Database; + + const { queryAllByText } = renderWithTheme( + + ); + + expect(queryMocks.useDatabaseTypesQuery).toHaveBeenCalledWith({ + platform: 'rdbms-legacy', + }); + + await waitFor(() => { + expect(queryAllByText('Cluster Configuration')).toHaveLength(0); + }); + }); +}); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.tsx index 721633578d9..f619078acab 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.tsx @@ -1,57 +1,41 @@ +import Grid from '@mui/material/Unstable_Grid2/Grid2'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; -import { Box } from 'src/components/Box'; import { TooltipIcon } from 'src/components/TooltipIcon'; import { Typography } from 'src/components/Typography'; +import { DatabaseStatusDisplay } from 'src/features/Databases/DatabaseDetail/DatabaseStatusDisplay'; +import { + StyledGridContainer, + StyledLabelTypography, + StyledValueGrid, +} from 'src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.style'; +import { databaseEngineMap } from 'src/features/Databases/DatabaseLanding/DatabaseRow'; import { useDatabaseTypesQuery } from 'src/queries/databases/databases'; import { useInProgressEvents } from 'src/queries/events/events'; import { useRegionsQuery } from 'src/queries/regions/regions'; import { formatStorageUnits } from 'src/utilities/formatStorageUnits'; import { convertMegabytesTo } from 'src/utilities/unitConversions'; -import { databaseEngineMap } from '../../DatabaseLanding/DatabaseRow'; -import { DatabaseStatusDisplay } from '../DatabaseStatusDisplay'; - import type { Region } from '@linode/api-v4'; import type { Database, - DatabaseInstance, DatabaseType, } from '@linode/api-v4/lib/databases/types'; import type { Theme } from '@mui/material/styles'; const useStyles = makeStyles()((theme: Theme) => ({ - configs: { - fontSize: '0.875rem', - lineHeight: '22px', - }, header: { marginBottom: theme.spacing(2), }, - label: { - fontFamily: theme.font.bold, - lineHeight: '22px', - width: theme.spacing(13), - }, - status: { - alignItems: 'center', - display: 'flex', - textTransform: 'capitalize', - }, })); interface Props { database: Database; } -export const getDatabaseVersionNumber = ( - version: DatabaseInstance['version'] -) => version.split('/')[1]; - export const DatabaseSummaryClusterConfiguration = (props: Props) => { const { classes } = useStyles(); - const { database } = props; const { data: types } = useDatabaseTypesQuery({ @@ -88,60 +72,69 @@ export const DatabaseSummaryClusterConfiguration = (props: Props) => { Cluster Configuration -
- - Status -
- -
-
- - Version - {databaseEngineMap[database.engine]} v{database.version} - - - Nodes + + + Status + + + + + + Plan + + + {formatStorageUnits(type.label)} + + + Nodes + + {configuration} - - - Region + + + CPUs + + + {type.vcpus} + + + Engine + + + {databaseEngineMap[database.engine]} v{database.version} + + + Region + + {region?.label ?? database.region} - - - Plan - {formatStorageUnits(type.label)} - - - RAM + + + RAM + + {type.memory / 1024} GB - - - CPUs - {type.vcpus} - - {database.total_disk_size_gb ? ( - <> - - Total Disk Size + + + + {database.total_disk_size_gb ? 'Total Disk Size' : 'Storage'} + + + + {database.total_disk_size_gb ? ( + <> {database.total_disk_size_gb} GB - - - Used - {database.used_disk_size_gb} GB - - - ) : ( - - Storage - {convertMegabytesTo(type.disk, true)} - - )} -
+ + ) : ( + convertMegabytesTo(type.disk, true) + )} + + ); }; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.style.ts b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.style.ts new file mode 100644 index 00000000000..325501935ca --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.style.ts @@ -0,0 +1,99 @@ +import { makeStyles } from 'tss-react/mui'; + +import type { Theme } from '@mui/material/styles'; + +export const useStyles = makeStyles()((theme: Theme) => ({ + actionBtnsCtn: { + display: 'flex', + justifyContent: 'flex-end', + marginTop: '10px', + padding: `${theme.spacing(1)} 0`, + }, + caCertBtn: { + '& svg': { + marginRight: theme.spacing(), + }, + '&:hover': { + backgroundColor: 'transparent', + opacity: 0.7, + }, + '&[disabled]': { + '& g': { + stroke: '#cdd0d5', + }, + '&:hover': { + backgroundColor: 'inherit', + textDecoration: 'none', + }, + // Override disabled background color defined for dark mode + backgroundColor: 'transparent', + color: '#cdd0d5', + cursor: 'default', + }, + color: theme.palette.primary.main, + fontFamily: theme.font.bold, + fontSize: '0.875rem', + lineHeight: '1.125rem', + marginLeft: theme.spacing(), + minHeight: 'auto', + minWidth: 'auto', + padding: 0, + }, + connectionDetailsCtn: { + '& p': { + lineHeight: '1.5rem', + }, + '& span': { + fontFamily: theme.font.bold, + }, + background: theme.interactionTokens.Background.Secondary, + border: `1px solid ${theme.name === 'light' ? '#ccc' : '#222'}`, + padding: `${theme.spacing(1)} 15px`, + }, + copyToolTip: { + '& svg': { + color: theme.palette.primary.main, + height: `${theme.spacing(2)} !important`, + width: `${theme.spacing(2)} !important`, + }, + marginRight: 12, + }, + error: { + color: theme.color.red, + marginLeft: theme.spacing(2), + }, + header: { + marginBottom: theme.spacing(2), + }, + inlineCopyToolTip: { + '& svg': { + height: theme.spacing(2), + width: theme.spacing(2), + }, + '&:hover': { + backgroundColor: 'transparent', + }, + display: 'inline-flex', + marginLeft: theme.spacing(0.5), + }, + progressCtn: { + '& circle': { + stroke: theme.palette.primary.main, + }, + alignSelf: 'flex-end', + marginBottom: 2, + marginLeft: 22, + }, + provisioningText: { + fontFamily: theme.font.normal, + fontStyle: 'italic', + }, + showBtn: { + color: theme.palette.primary.main, + fontSize: '0.875rem', + marginLeft: theme.spacing(), + minHeight: 'auto', + minWidth: 'auto', + padding: 0, + }, +})); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.test.tsx new file mode 100644 index 00000000000..7b0e11c7009 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.test.tsx @@ -0,0 +1,129 @@ +import { waitFor } from '@testing-library/react'; +import React from 'react'; + +import { databaseFactory } from 'src/factories/databases'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import DatabaseSummaryConnectionDetails from './DatabaseSummaryConnectionDetails'; + +import type { Database } from '@linode/api-v4/lib/databases'; + +const AKMADMIN = 'akmadmin'; +const POSTGRESQL = 'postgresql'; +const DEFAULT_PRIMARY = 'db-mysql-default-primary.net'; +const DEFAULT_STANDBY = 'db-mysql-default-standby.net'; + +const MYSQL = 'mysql'; +const LINROOT = 'linroot'; +const LEGACY_PRIMARY = 'db-mysql-legacy-primary.net'; +const LEGACY_SECONDARY = 'db-mysql-legacy-secondary.net'; + +const queryMocks = vi.hoisted(() => ({ + useDatabaseCredentialsQuery: vi.fn().mockReturnValue({}), +})); + +vi.mock('src/queries/databases/databases', () => ({ + useDatabaseCredentialsQuery: queryMocks.useDatabaseCredentialsQuery, +})); + +describe('DatabaseSummaryConnectionDetails', () => { + it('should display correctly for default db', async () => { + queryMocks.useDatabaseCredentialsQuery.mockReturnValue({ + data: { + password: 'abc123', + username: AKMADMIN, + }, + }); + + const database = databaseFactory.build({ + engine: POSTGRESQL, + hosts: { + primary: DEFAULT_PRIMARY, + secondary: undefined, + standby: DEFAULT_STANDBY, + }, + id: 99, + platform: 'rdbms-default', + port: 22496, + ssl_connection: true, + }) as Database; + + const { queryAllByText } = renderWithTheme( + + ); + + expect(queryMocks.useDatabaseCredentialsQuery).toHaveBeenCalledWith( + POSTGRESQL, + 99 + ); + + await waitFor(() => { + expect(queryAllByText('Username')).toHaveLength(1); + expect(queryAllByText(AKMADMIN)).toHaveLength(1); + + expect(queryAllByText('Password')).toHaveLength(1); + + expect(queryAllByText('Host')).toHaveLength(1); + expect(queryAllByText(DEFAULT_PRIMARY)).toHaveLength(1); + + expect(queryAllByText('Read-only Host')).toHaveLength(1); + expect(queryAllByText(DEFAULT_STANDBY)).toHaveLength(1); + + expect(queryAllByText('Port')).toHaveLength(1); + expect(queryAllByText('22496')).toHaveLength(1); + + expect(queryAllByText('SSL')).toHaveLength(1); + expect(queryAllByText('ENABLED')).toHaveLength(1); + }); + }); + + it('should display correctly for legacy db', async () => { + queryMocks.useDatabaseCredentialsQuery.mockReturnValue({ + data: { + password: 'abc123', + username: LINROOT, + }, + }); + + const database = databaseFactory.build({ + engine: MYSQL, + hosts: { + primary: LEGACY_PRIMARY, + secondary: LEGACY_SECONDARY, + standby: undefined, + }, + id: 22, + platform: 'rdbms-legacy', + port: 3306, + ssl_connection: true, + }) as Database; + + const { queryAllByText } = renderWithTheme( + + ); + + expect(queryMocks.useDatabaseCredentialsQuery).toHaveBeenCalledWith( + MYSQL, + 22 + ); + + await waitFor(() => { + expect(queryAllByText('Username')).toHaveLength(1); + expect(queryAllByText(LINROOT)).toHaveLength(1); + + expect(queryAllByText('Password')).toHaveLength(1); + + expect(queryAllByText('Host')).toHaveLength(1); + expect(queryAllByText(LEGACY_PRIMARY)).toHaveLength(1); + + expect(queryAllByText('Private Network Host')).toHaveLength(1); + expect(queryAllByText(LEGACY_SECONDARY)).toHaveLength(1); + + expect(queryAllByText('Port')).toHaveLength(1); + expect(queryAllByText('3306')).toHaveLength(1); + + expect(queryAllByText('SSL')).toHaveLength(1); + expect(queryAllByText('ENABLED')).toHaveLength(1); + }); + }); +}); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx index 17b54795979..48b122b70ef 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx @@ -1,13 +1,9 @@ import { getSSLFields } from '@linode/api-v4/lib/databases/databases'; -import { Database, SSLFields } from '@linode/api-v4/lib/databases/types'; -import { useTheme } from '@mui/material'; -import { Theme } from '@mui/material/styles'; +import Grid from '@mui/material/Unstable_Grid2/Grid2'; import { useSnackbar } from 'notistack'; import * as React from 'react'; -import { makeStyles } from 'tss-react/mui'; import DownloadIcon from 'src/assets/icons/lke-download.svg'; -import { Box } from 'src/components/Box'; import { Button } from 'src/components/Button/Button'; import { CircleProgress } from 'src/components/CircleProgress'; import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; @@ -18,100 +14,14 @@ import { useDatabaseCredentialsQuery } from 'src/queries/databases/databases'; import { downloadFile } from 'src/utilities/downloadFile'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; -const useStyles = makeStyles()((theme: Theme) => ({ - actionBtnsCtn: { - display: 'flex', - justifyContent: 'flex-end', - padding: `${theme.spacing(1)} 0`, - }, - caCertBtn: { - '& svg': { - marginRight: theme.spacing(), - }, - '&:hover': { - backgroundColor: 'transparent', - opacity: 0.7, - }, - '&[disabled]': { - '& g': { - stroke: '#cdd0d5', - }, - '&:hover': { - backgroundColor: 'inherit', - textDecoration: 'none', - }, - // Override disabled background color defined for dark mode - backgroundColor: 'transparent', - color: '#cdd0d5', - cursor: 'default', - }, - color: theme.palette.primary.main, - fontFamily: theme.font.bold, - fontSize: '0.875rem', - lineHeight: '1.125rem', - marginLeft: theme.spacing(), - minHeight: 'auto', - minWidth: 'auto', - padding: 0, - }, - connectionDetailsCtn: { - '& p': { - lineHeight: '1.5rem', - }, - '& span': { - fontFamily: theme.font.bold, - }, - background: theme.interactionTokens.Background.Secondary, - border: `1px solid ${theme.name === 'light' ? '#ccc' : '#222'}`, - padding: '8px 15px', - }, - copyToolTip: { - '& svg': { - color: theme.palette.primary.main, - height: `16px !important`, - width: `16px !important`, - }, - marginRight: 12, - }, - error: { - color: theme.color.red, - marginLeft: theme.spacing(2), - }, - header: { - marginBottom: theme.spacing(2), - }, - inlineCopyToolTip: { - '& svg': { - height: `16px`, - width: `16px`, - }, - '&:hover': { - backgroundColor: 'transparent', - }, - display: 'inline-flex', - marginLeft: 4, - }, - progressCtn: { - '& circle': { - stroke: theme.palette.primary.main, - }, - alignSelf: 'flex-end', - marginBottom: 2, - marginLeft: 22, - }, - provisioningText: { - fontFamily: theme.font.normal, - fontStyle: 'italic', - }, - showBtn: { - color: theme.palette.primary.main, - fontSize: '0.875rem', - marginLeft: theme.spacing(), - minHeight: 'auto', - minWidth: 'auto', - padding: 0, - }, -})); +import { + StyledGridContainer, + StyledLabelTypography, + StyledValueGrid, +} from './DatabaseSummaryClusterConfiguration.style'; +import { useStyles } from './DatabaseSummaryConnectionDetails.style'; + +import type { Database, SSLFields } from '@linode/api-v4/lib/databases/types'; interface Props { database: Database; @@ -125,14 +35,11 @@ const sxTooltipIcon = { const privateHostCopy = 'A private network host and a private IP can only be used to access a Database Cluster from Linodes in the same data center and will not incur transfer costs.'; -const mongoHostHelperCopy = - 'This is a public hostname. Coming soon: connect to your MongoDB clusters using private IPs'; - export const DatabaseSummaryConnectionDetails = (props: Props) => { const { database } = props; const { classes } = useStyles(); - const theme = useTheme(); const { enqueueSnackbar } = useSnackbar(); + const isLegacy = database.platform !== 'rdbms-default'; const [showCredentials, setShowPassword] = React.useState(false); const [isCACertDownloading, setIsCACertDownloading] = React.useState( @@ -160,9 +67,6 @@ export const DatabaseSummaryConnectionDetails = (props: Props) => { setShowPassword((showCredentials) => !showCredentials); }; - const isMongoReplicaSet = - database.engine === 'mongodb' && database.cluster_size > 1; - React.useEffect(() => { if (showCredentials && !credentials) { getDatabaseCredentials(); @@ -198,7 +102,31 @@ export const DatabaseSummaryConnectionDetails = (props: Props) => { const disableShowBtn = ['failed', 'provisioning'].includes(database.status); const disableDownloadCACertificateBtn = database.status === 'provisioning'; - const readOnlyHost = database?.hosts?.standby || database?.hosts?.secondary; + const readOnlyHostValue = + database?.hosts?.standby ?? database?.hosts?.secondary ?? ''; + + const readOnlyHost = () => { + const defaultValue = isLegacy ? '-' : 'not available'; + const value = readOnlyHostValue ?? defaultValue; + return ( + <> + {value} + {value && ( + + )} + {isLegacy && ( + + )} + + ); + }; const credentialsBtn = (handleClick: () => void, btnText: string) => { return ( @@ -223,7 +151,7 @@ export const DatabaseSummaryConnectionDetails = (props: Props) => { Download CA Certificate - {disableDownloadCACertificateBtn ? ( + {disableDownloadCACertificateBtn && ( { text="Your Database Cluster is currently provisioning." /> - ) : null} + )} ); @@ -240,14 +168,18 @@ export const DatabaseSummaryConnectionDetails = (props: Props) => { Connection Details - - - username = {username} - - - - password = {password} - + + + Username + + + {username} + + + Password + + + {password} {showCredentials && credentialsLoading ? (
@@ -265,7 +197,7 @@ export const DatabaseSummaryConnectionDetails = (props: Props) => { showCredentials && credentials ? 'Hide' : 'Show' ) )} - {disableShowBtn ? ( + {disableShowBtn && ( { status="help" sxTooltipIcon={sxTooltipIcon} /> - ) : null} - {showCredentials && credentials ? ( + )} + {showCredentials && credentials && ( - ) : null} - - - {!isMongoReplicaSet ? ( - - {database.hosts?.primary ? ( - <> - - host ={' '} - - {database.hosts?.primary} - {' '} - - - {database.engine === 'mongodb' ? ( - - ) : null} - - ) : ( - - host ={' '} - - Your hostname will appear here once it is available. - - - )} - - ) : ( - <> - - hosts ={' '} - {!database.peers || database.peers.length === 0 ? ( - - Your hostnames will appear here once they are available. - - ) : null} - - {database.peers && database.peers.length > 0 - ? database.peers.map((hostname, i) => ( - - - - {hostname} - - - - {/* Display the helper text on the first hostname */} - {i === 0 ? ( - - ) : null} - - )) - : null} - )} - - {readOnlyHost ? ( - - - {database.platform === 'rdbms-default' ? ( - read-only host - ) : ( - private network host - )} - = {readOnlyHost} - - - - - ) : null} - - port = {database.port} - - {isMongoReplicaSet ? ( - database.replica_set ? ( - - - replica set ={' '} - - {database.replica_set} - - + + + Host + + + {database.hosts?.primary ? ( + <> + {database.hosts?.primary} - + ) : ( - replica set ={' '} - Your replica set will appear here once it is available. + Your hostname will appear here once it is available. - ) - ) : null} - - ssl = {database.ssl_connection ? 'ENABLED' : 'DISABLED'} - - + )} + + + + {isLegacy ? 'Private Network Host' : 'Read-only Host'} + + + + {readOnlyHost()} + + + Port + + + {database.port} + + + SSL + + + {database.ssl_connection ? 'ENABLED' : 'DISABLED'} + +
{database.ssl_connection ? caCertificateJSX : null}
diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryClusterConfigurationLegacy.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryClusterConfigurationLegacy.tsx new file mode 100644 index 00000000000..5bc03953c51 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryClusterConfigurationLegacy.tsx @@ -0,0 +1,146 @@ +import * as React from 'react'; +import { makeStyles } from 'tss-react/mui'; + +import { Box } from 'src/components/Box'; +import { TooltipIcon } from 'src/components/TooltipIcon'; +import { Typography } from 'src/components/Typography'; +import { DatabaseStatusDisplay } from 'src/features/Databases/DatabaseDetail/DatabaseStatusDisplay'; +import { databaseEngineMap } from 'src/features/Databases/DatabaseLanding/DatabaseRow'; +import { useDatabaseTypesQuery } from 'src/queries/databases/databases'; +import { useInProgressEvents } from 'src/queries/events/events'; +import { useRegionsQuery } from 'src/queries/regions/regions'; +import { formatStorageUnits } from 'src/utilities/formatStorageUnits'; +import { convertMegabytesTo } from 'src/utilities/unitConversions'; + +import type { Region } from '@linode/api-v4'; +import type { + Database, + DatabaseType, +} from '@linode/api-v4/lib/databases/types'; +import type { Theme } from '@mui/material/styles'; + +const useStyles = makeStyles()((theme: Theme) => ({ + configs: { + fontSize: '0.875rem', + lineHeight: '22px', + }, + header: { + marginBottom: theme.spacing(2), + }, + label: { + fontFamily: theme.font.bold, + lineHeight: '22px', + width: theme.spacing(13), + }, + status: { + alignItems: 'center', + display: 'flex', + textTransform: 'capitalize', + }, +})); + +interface Props { + database: Database; +} + +/** + * Deprecated @since DBaaS V2 GA. Will be removed remove post GA release ~ Dec 2024 + * TODO (UIE-8214) remove POST GA + */ +export const DatabaseSummaryClusterConfigurationLegacy = (props: Props) => { + const { classes } = useStyles(); + const { database } = props; + + const { data: types } = useDatabaseTypesQuery({ + platform: database.platform, + }); + + const type = types?.find((type: DatabaseType) => type.id === database?.type); + + const { data: regions } = useRegionsQuery(); + + const region = regions?.find((r: Region) => r.id === database.region); + + const { data: events } = useInProgressEvents(); + + if (!database || !type) { + return null; + } + + const configuration = + database.cluster_size === 1 + ? 'Primary' + : `Primary +${database.cluster_size - 1} replicas`; + + const sxTooltipIcon = { + marginLeft: '4px', + padding: '0px', + }; + + const STORAGE_COPY = + 'The total disk size is smaller than the selected plan capacity due to overhead from the OS.'; + + return ( + <> + + Cluster Configuration + +
+ + Status +
+ +
+
+ + Version + {databaseEngineMap[database.engine]} v{database.version} + + + Nodes + {configuration} + + + Region + {region?.label ?? database.region} + + + Plan + {formatStorageUnits(type.label)} + + + RAM + {type.memory / 1024} GB + + + CPUs + {type.vcpus} + + {database.total_disk_size_gb ? ( + <> + + Total Disk Size + {database.total_disk_size_gb} GB + + + + Used + {database.used_disk_size_gb} GB + + + ) : ( + + Storage + {convertMegabytesTo(type.disk, true)} + + )} +
+ + ); +}; + +export default DatabaseSummaryClusterConfigurationLegacy; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryConnectionDetailsLegacy.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryConnectionDetailsLegacy.tsx new file mode 100644 index 00000000000..8fbdb195452 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryConnectionDetailsLegacy.tsx @@ -0,0 +1,370 @@ +import { getSSLFields } from '@linode/api-v4/lib/databases/databases'; +import { useTheme } from '@mui/material'; +import { useSnackbar } from 'notistack'; +import * as React from 'react'; +import { makeStyles } from 'tss-react/mui'; + +import DownloadIcon from 'src/assets/icons/lke-download.svg'; +import { Box } from 'src/components/Box'; +import { Button } from 'src/components/Button/Button'; +import { CircleProgress } from 'src/components/CircleProgress'; +import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; +import { TooltipIcon } from 'src/components/TooltipIcon'; +import { Typography } from 'src/components/Typography'; +import { DB_ROOT_USERNAME } from 'src/constants'; +import { useDatabaseCredentialsQuery } from 'src/queries/databases/databases'; +import { downloadFile } from 'src/utilities/downloadFile'; +import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; + +import type { Database, SSLFields } from '@linode/api-v4/lib/databases/types'; +import type { Theme } from '@mui/material/styles'; + +const useStyles = makeStyles()((theme: Theme) => ({ + actionBtnsCtn: { + display: 'flex', + justifyContent: 'flex-end', + marginTop: '10px', + padding: `${theme.spacing(1)} 0`, + }, + caCertBtn: { + '& svg': { + marginRight: theme.spacing(), + }, + '&:hover': { + backgroundColor: 'transparent', + opacity: 0.7, + }, + '&[disabled]': { + '& g': { + stroke: '#cdd0d5', + }, + '&:hover': { + backgroundColor: 'inherit', + textDecoration: 'none', + }, + // Override disabled background color defined for dark mode + backgroundColor: 'transparent', + color: '#cdd0d5', + cursor: 'default', + }, + color: theme.palette.primary.main, + fontFamily: theme.font.bold, + fontSize: '0.875rem', + lineHeight: '1.125rem', + marginLeft: theme.spacing(), + minHeight: 'auto', + minWidth: 'auto', + padding: 0, + }, + connectionDetailsCtn: { + '& p': { + lineHeight: '1.5rem', + }, + '& span': { + fontFamily: theme.font.bold, + }, + background: theme.bg.bgAccessRowTransparentGradient, + border: `1px solid ${theme.name === 'light' ? '#ccc' : '#222'}`, + padding: '8px 15px', + }, + copyToolTip: { + '& svg': { + color: theme.palette.primary.main, + height: `16px !important`, + width: `16px !important`, + }, + marginRight: 12, + }, + error: { + color: theme.color.red, + marginLeft: theme.spacing(2), + }, + header: { + marginBottom: theme.spacing(2), + }, + inlineCopyToolTip: { + '& svg': { + height: `16px`, + width: `16px`, + }, + '&:hover': { + backgroundColor: 'transparent', + }, + display: 'inline-flex', + marginLeft: 4, + }, + progressCtn: { + '& circle': { + stroke: theme.palette.primary.main, + }, + alignSelf: 'flex-end', + marginBottom: 2, + marginLeft: 22, + }, + provisioningText: { + fontFamily: theme.font.normal, + fontStyle: 'italic', + }, + showBtn: { + color: theme.palette.primary.main, + fontSize: '0.875rem', + marginLeft: theme.spacing(), + minHeight: 'auto', + minWidth: 'auto', + padding: 0, + }, +})); + +interface Props { + database: Database; +} + +const sxTooltipIcon = { + marginLeft: '4px', + padding: '0px', +}; + +const privateHostCopy = + 'A private network host and a private IP can only be used to access a Database Cluster from Linodes in the same data center and will not incur transfer costs.'; + +const mongoHostHelperCopy = + 'This is a public hostname. Coming soon: connect to your MongoDB clusters using private IPs'; + +/** + * Deprecated @since DBaaS V2 GA. Will be removed remove post GA release ~ Dec 2024 + * TODO (UIE-8214) remove POST GA + */ +export const DatabaseSummaryConnectionDetailsLegacy = (props: Props) => { + const { database } = props; + const { classes } = useStyles(); + const theme = useTheme(); + const { enqueueSnackbar } = useSnackbar(); + + const [showCredentials, setShowPassword] = React.useState(false); + const [isCACertDownloading, setIsCACertDownloading] = React.useState( + false + ); + + const { + data: credentials, + error: credentialsError, + isLoading: credentialsLoading, + refetch: getDatabaseCredentials, + } = useDatabaseCredentialsQuery(database.engine, database.id); + + const username = + database.platform === 'rdbms-default' + ? 'akmadmin' + : database.engine === 'postgresql' + ? 'linpostgres' + : DB_ROOT_USERNAME; + + const password = + showCredentials && credentials ? credentials?.password : '••••••••••'; + + const handleShowPasswordClick = () => { + setShowPassword((showCredentials) => !showCredentials); + }; + + React.useEffect(() => { + if (showCredentials && !credentials) { + getDatabaseCredentials(); + } + }, [credentials, getDatabaseCredentials, showCredentials]); + + const handleDownloadCACertificate = () => { + setIsCACertDownloading(true); + getSSLFields(database.engine, database.id) + .then((response: SSLFields) => { + // Convert to utf-8 from base64 + try { + const decodedFile = window.atob(response.ca_certificate); + downloadFile(`${database.label}-ca-certificate.crt`, decodedFile); + setIsCACertDownloading(false); + } catch (e) { + enqueueSnackbar('Error parsing your CA Certificate file', { + variant: 'error', + }); + setIsCACertDownloading(false); + return; + } + }) + .catch((errorResponse: any) => { + const error = getErrorStringOrDefault( + errorResponse, + 'Unable to download your CA Certificate' + ); + setIsCACertDownloading(false); + enqueueSnackbar(error, { variant: 'error' }); + }); + }; + + const disableShowBtn = ['failed', 'provisioning'].includes(database.status); + const disableDownloadCACertificateBtn = database.status === 'provisioning'; + const readOnlyHost = database?.hosts?.standby || database?.hosts?.secondary; + + const credentialsBtn = (handleClick: () => void, btnText: string) => { + return ( + + ); + }; + + const caCertificateJSX = ( + <> + + {disableDownloadCACertificateBtn && ( + + + + )} + + ); + + return ( + <> + + Connection Details + + + + username = {username} + + + + password = {password} + + {showCredentials && credentialsLoading ? ( +
+ +
+ ) : credentialsError ? ( + <> + + Error retrieving credentials. + + {credentialsBtn(() => getDatabaseCredentials(), 'Retry')} + + ) : ( + credentialsBtn( + handleShowPasswordClick, + showCredentials && credentials ? 'Hide' : 'Show' + ) + )} + {disableShowBtn && ( + + )} + {showCredentials && credentials && ( + + )} +
+ + + hosts ={' '} + {(!database.peers || database.peers.length === 0) && ( + + Your hostnames will appear here once they are available. + + )} + + {database.peers && + database.peers.length > 0 && + database.peers.map((hostname, i) => ( + + + + {hostname} + + + + {/* Display the helper text on the first hostname */} + {i === 0 && ( + + )} + + ))} + + {readOnlyHost && ( + + + {database.platform === 'rdbms-default' ? ( + read-only host + ) : ( + private network host + )} + = {readOnlyHost} + + + {database.platform === 'rdbms-legacy' && ( + + )} + + )} + + port = {database.port} + + + ssl = {database.ssl_connection ? 'ENABLED' : 'DISABLED'} + +
+
+ {database.ssl_connection ? caCertificateJSX : null} +
+ + ); +}; + +export default DatabaseSummaryConnectionDetailsLegacy; diff --git a/packages/manager/src/features/Databases/utilities.ts b/packages/manager/src/features/Databases/utilities.ts index ce4fe9bc149..2d3179964c0 100644 --- a/packages/manager/src/features/Databases/utilities.ts +++ b/packages/manager/src/features/Databases/utilities.ts @@ -1,17 +1,22 @@ -import type { DatabaseInstance } from '@linode/api-v4'; -import { DatabaseFork } from '@linode/api-v4'; import { DateTime } from 'luxon'; + import { useFlags } from 'src/hooks/useFlags'; 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 { DatabaseFork } from '@linode/api-v4'; + export interface IsDatabasesEnabled { isDatabasesEnabled: boolean; + isDatabasesMonitorBeta?: boolean; + isDatabasesMonitorEnabled?: boolean; isDatabasesV1Enabled: boolean; - isDatabasesV2Enabled: boolean; isDatabasesV2Beta: boolean; + isDatabasesV2Enabled: boolean; isDatabasesV2GA: boolean; /** * Temporary variable to be removed post GA release @@ -21,8 +26,6 @@ export interface IsDatabasesEnabled { * Temporary variable to be removed post GA release */ isUserNewBeta: boolean; - isDatabasesMonitorEnabled: boolean; - isDatabasesMonitorBeta: boolean; } /** @@ -75,18 +78,16 @@ export const useIsDatabasesEnabled = (): IsDatabasesEnabled => { return { isDatabasesEnabled: isDatabasesV1Enabled || isDatabasesV2Enabled, + isDatabasesMonitorBeta: !!flags.dbaasV2MonitorMetrics?.beta, + isDatabasesMonitorEnabled: !!flags.dbaasV2MonitorMetrics?.enabled, isDatabasesV1Enabled, - isDatabasesV2Enabled, - isDatabasesV2Beta, - isUserExistingBeta: isDatabasesV2Beta && isDatabasesV1Enabled, - isUserNewBeta: isDatabasesV2Beta && !isDatabasesV1Enabled, - + isDatabasesV2Enabled, isDatabasesV2GA: (isDatabasesV1Enabled || isDatabasesV2Enabled) && hasV2GAFlag, - isDatabasesMonitorEnabled: !!flags.dbaasV2MonitorMetrics?.enabled, - isDatabasesMonitorBeta: !!flags.dbaasV2MonitorMetrics?.beta, + isUserExistingBeta: isDatabasesV2Beta && isDatabasesV1Enabled, + isUserNewBeta: isDatabasesV2Beta && !isDatabasesV1Enabled, }; } @@ -95,17 +96,14 @@ export const useIsDatabasesEnabled = (): IsDatabasesEnabled => { return { isDatabasesEnabled: hasLegacyTypes || hasDefaultTypes, + isDatabasesMonitorBeta: !!flags.dbaasV2MonitorMetrics?.beta, + isDatabasesMonitorEnabled: !!flags.dbaasV2MonitorMetrics?.enabled, isDatabasesV1Enabled: hasLegacyTypes, - isDatabasesV2Enabled: hasDefaultTypes, - isDatabasesV2Beta: hasDefaultTypes && hasV2BetaFlag, + isDatabasesV2Enabled: hasDefaultTypes, + isDatabasesV2GA: (hasLegacyTypes || hasDefaultTypes) && hasV2GAFlag, isUserExistingBeta: hasLegacyTypes && hasDefaultTypes && hasV2BetaFlag, isUserNewBeta: !hasLegacyTypes && hasDefaultTypes && hasV2BetaFlag, - - isDatabasesV2GA: (hasLegacyTypes || hasDefaultTypes) && hasV2GAFlag, - - isDatabasesMonitorEnabled: !!flags.dbaasV2MonitorMetrics?.enabled, - isDatabasesMonitorBeta: !!flags.dbaasV2MonitorMetrics?.beta, }; }; @@ -169,8 +167,7 @@ export const toSelectedDateTime = ( const isoTime = DateTime.now() .set({ hour: time, minute: 0 }) ?.toISOTime({ includeOffset: false }); - const selectedDateTime = DateTime.fromISO(`${isoDate}T${isoTime}`); - return selectedDateTime; + return DateTime.fromISO(`${isoDate}T${isoTime}`); }; /**