diff --git a/packages/api-v4/.changeset/pr-13413-changed-1771602214397.md b/packages/api-v4/.changeset/pr-13413-changed-1771602214397.md new file mode 100644 index 00000000000..69bdb6b4dff --- /dev/null +++ b/packages/api-v4/.changeset/pr-13413-changed-1771602214397.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Changed +--- + +Export `HostEndpoint` and rename `private_access` to `public_access` ([#13413](https://github.com/linode/manager/pull/13413)) diff --git a/packages/api-v4/src/databases/types.ts b/packages/api-v4/src/databases/types.ts index 59df60aa1c0..971a59a6869 100644 --- a/packages/api-v4/src/databases/types.ts +++ b/packages/api-v4/src/databases/types.ts @@ -96,10 +96,10 @@ export type HostEndpointRole = | 'standby' | 'standby-connection-pool'; -interface HostEndpoint { +export interface HostEndpoint { address: string; port: number; - private_access: boolean; + public_access: boolean; role: HostEndpointRole; } diff --git a/packages/manager/.changeset/pr-13413-upcoming-features-1771602250906.md b/packages/manager/.changeset/pr-13413-upcoming-features-1771602250906.md new file mode 100644 index 00000000000..ae8b475f78a --- /dev/null +++ b/packages/manager/.changeset/pr-13413-upcoming-features-1771602250906.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Use new hostname endpoint in Database Summary and Network tab ([#13413](https://github.com/linode/manager/pull/13413)) diff --git a/packages/manager/src/dev-tools/FeatureFlagTool.tsx b/packages/manager/src/dev-tools/FeatureFlagTool.tsx index b800fb52cf9..5b4a8a16484 100644 --- a/packages/manager/src/dev-tools/FeatureFlagTool.tsx +++ b/packages/manager/src/dev-tools/FeatureFlagTool.tsx @@ -36,6 +36,7 @@ const options: { flag: keyof Flags; label: string }[] = [ }, { flag: 'gecko2', label: 'Gecko' }, { flag: 'generationalPlansv2', label: 'Generational compute plans' }, + { flag: 'hostnameEndpoints', label: 'Hostname Endpoints' }, { flag: 'limitsEvolution', label: 'Limits Evolution' }, { flag: 'linodeDiskEncryption', label: 'Linode Disk Encryption (LDE)' }, { flag: 'linodeInterfaces', label: 'Linode Interfaces' }, diff --git a/packages/manager/src/factories/databases.ts b/packages/manager/src/factories/databases.ts index 0c1218b2288..7e57944aede 100644 --- a/packages/manager/src/factories/databases.ts +++ b/packages/manager/src/factories/databases.ts @@ -179,7 +179,7 @@ export const databaseInstanceFactory = { address: 'public-db-mysql-primary-0.b.linodeb.net', role: 'primary', - private_access: false, + public_access: true, port: 3306, }, ], @@ -191,7 +191,7 @@ export const databaseInstanceFactory = { address: 'public-db-mysql-primary-0.b.linodeb.net', role: 'primary', - private_access: false, + public_access: true, port: 3306, }, ], @@ -253,7 +253,7 @@ export const databaseFactory = Factory.Sync.makeFactory({ { address: 'public-db-mysql-primary-0.b.linodeb.net', role: 'primary', - private_access: false, + public_access: true, port: 3306, }, ], @@ -265,7 +265,7 @@ export const databaseFactory = Factory.Sync.makeFactory({ { address: 'public-db-mysql-primary-0.b.linodeb.net', role: 'primary', - private_access: false, + public_access: true, port: 3306, }, ], diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index 3862ee35e25..2f5cb5a5c46 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -242,6 +242,7 @@ export interface Flags { gecko2: GeckoFeatureFlag; generationalPlansv2: GenerationalPlansFlag; gpuv2: GpuV2; + hostnameEndpoints: boolean; iam: BaseFeatureFlag; iamDelegation: BaseFeatureFlag; iamLimitedAvailabilityBadges: boolean; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostDisplay.tsx b/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostDisplay.tsx new file mode 100644 index 00000000000..533bb3cc6c3 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostDisplay.tsx @@ -0,0 +1,62 @@ +import { TooltipIcon } from '@linode/ui'; +import { styled } from '@mui/material/styles'; +import * as React from 'react'; + +import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; + +import { + SUMMARY_HOST_TOOLTIP_COPY, + SUMMARY_PRIVATE_HOST_COPY, +} from '../constants'; + +import type { HostEndpoint } from '@linode/api-v4/lib/databases/types'; + +interface ConnectionDetailsHostDisplayProps { + host: HostEndpoint; +} + +export const ConnectionDetailsHostDisplay = ( + props: ConnectionDetailsHostDisplayProps +) => { + const { host } = props; + + return ( + <> + {host?.address} + + + + ); +}; + +export const StyledCopyTooltip = styled(CopyTooltip, { + label: 'StyledCopyTooltip', +})(({ theme }) => ({ + '& svg': { + height: theme.spacingFunction(16), + width: theme.spacingFunction(16), + }, + '&:hover': { + backgroundColor: 'transparent', + }, + display: 'inline-flex', + marginLeft: theme.spacingFunction(4), +})); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.test.tsx index 043831e70c9..c46c0fed9fc 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.test.tsx @@ -17,6 +17,10 @@ const PRIVATE_STANDBY = `private-${DEFAULT_STANDBY}`; const LEGACY_PRIMARY = 'db-mysql-legacy-primary.net'; const LEGACY_SECONDARY = 'db-mysql-legacy-secondary.net'; +/** + * @TODO - delete this file after API releases hostname endpoint changes + */ + describe('ConnectionDetailsHostRows', () => { it('should display Host and Read-only Host fields for a default database with no VPC configured', () => { const database = databaseFactory.build({ diff --git a/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.tsx b/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.tsx index 0cc591d1eac..87ec404e099 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.tsx @@ -26,6 +26,8 @@ interface ConnectionDetailsHostRowsProps { type HostContentMode = 'default' | 'private' | 'public'; /** + * @deprecated Delete this file in favor of ConnectionDetailsHostRows2 after the API releases hostname endpoint changes. + * * This component is responsible for conditionally rendering the Private Host, Public Host, and Read-only Host rows that get displayed in * the Connection Details tables that appear in the Database Summary and Networking tabs */ export const ConnectionDetailsHostRows = ( diff --git a/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.test.tsx new file mode 100644 index 00000000000..b8439543189 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.test.tsx @@ -0,0 +1,182 @@ +import { screen } from '@testing-library/react'; +import React from 'react'; + +import { databaseFactory } from 'src/factories/databases'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { ConnectionDetailsHostRows2 } from './ConnectionDetailsHostRows2'; + +import type { Database } from '@linode/api-v4/lib/databases'; + +const DEFAULT_PRIMARY = 'db-mysql-default-primary.net'; +const DEFAULT_STANDBY = 'db-mysql-default-standby.net'; + +const PRIVATE_PRIMARY = `private-${DEFAULT_PRIMARY}`; +const PRIVATE_STANDBY = `private-${DEFAULT_STANDBY}`; + +describe('ConnectionDetailsHostRows2', () => { + it('should display Host and Read-only Host fields for a default database with no VPC configured', () => { + const database = databaseFactory.build({ + hosts: { + primary: DEFAULT_PRIMARY, + standby: DEFAULT_STANDBY, + endpoints: [ + { + role: 'primary', + address: DEFAULT_PRIMARY, + port: 15847, + public_access: true, + }, + { + role: 'standby', + address: DEFAULT_STANDBY, + port: 15847, + public_access: true, + }, + ], + }, + platform: 'rdbms-default', + private_network: null, // No VPC configured, so Host and Read-only Host fields render + }) as Database; + + renderWithTheme(); + + expect(screen.getByText('Host')).toBeVisible(); + expect(screen.getByText(DEFAULT_PRIMARY)).toBeVisible(); + + expect(screen.getByText('Read-only Host')).toBeVisible(); + expect(screen.getByText(DEFAULT_STANDBY)).toBeVisible(); + }); + + it('should display N/A for default DB with blank read-only Host field', () => { + const database = databaseFactory.build({ + hosts: { + primary: DEFAULT_PRIMARY, + standby: undefined, + endpoints: [ + { + role: 'primary', + address: DEFAULT_PRIMARY, + port: 15847, + public_access: true, + }, + ], + }, + platform: 'rdbms-default', + }); + + renderWithTheme(); + + expect(screen.getByText('N/A')).toBeVisible(); + }); + + it('should display provisioning text when hosts are not available', () => { + const database = databaseFactory.build({ + hosts: undefined, + platform: 'rdbms-default', + }) as Database; + + const { getByText } = renderWithTheme( + + ); + + const hostNameProvisioningText = getByText( + 'Your hostname will appear here once it is available.' + ); + + expect(hostNameProvisioningText).toBeInTheDocument(); + }); + + it('should display Private variations of Host and Read-only fields when a VPC is configured with public access set to false', () => { + const database = databaseFactory.build({ + hosts: { + primary: PRIVATE_PRIMARY, + standby: PRIVATE_STANDBY, + endpoints: [ + { + role: 'primary', + address: PRIVATE_PRIMARY, + port: 15847, + public_access: false, + }, + { + role: 'standby', + address: PRIVATE_STANDBY, + port: 15847, + public_access: false, + }, + ], + }, + platform: 'rdbms-default', + private_network: { + public_access: false, + subnet_id: 1, + vpc_id: 123, + }, // VPC configuration with public access set to false + }) as Database; + + renderWithTheme(); + + expect(screen.getByText('Private Host')).toBeVisible(); + expect(screen.getByText(PRIVATE_PRIMARY)).toBeVisible(); + expect(screen.getByText('Private Read-only Host')).toBeVisible(); + expect(screen.getByText(PRIVATE_STANDBY)).toBeVisible(); + }); + + it('should display Private and Public variations of Host and Read-only Host fields when a VPC is configured with public access set to true', () => { + const database = databaseFactory.build({ + hosts: { + primary: PRIVATE_PRIMARY, + standby: PRIVATE_STANDBY, + endpoints: [ + { + role: 'primary', + address: `public-${DEFAULT_PRIMARY}`, + port: 15847, + public_access: true, + }, + { + role: 'standby', + address: `public-${DEFAULT_STANDBY}`, + port: 15847, + public_access: true, + }, + { + role: 'primary', + address: PRIVATE_PRIMARY, + port: 15847, + public_access: false, + }, + { + role: 'standby', + address: PRIVATE_STANDBY, + port: 15847, + public_access: false, + }, + ], + }, + platform: 'rdbms-default', + private_network: { + public_access: true, + subnet_id: 1, + vpc_id: 123, + }, // VPC configuration with public access set to true + }) as Database; + + renderWithTheme(); + + // Verify that Private and Public Host and Readonly-host fields are rendered + expect(screen.getByText('Private Host')).toBeVisible(); + expect(screen.getByText('Public Host')).toBeVisible(); + expect(screen.getByText('Private Read-only Host')).toBeVisible(); + expect(screen.getByText('Public Read-only Host')).toBeVisible(); + + // Verify that the Private and Public hostname is rendered correctly + expect(screen.getByText(PRIVATE_PRIMARY)).toBeVisible(); + expect(screen.getByText(`public-${DEFAULT_PRIMARY}`)).toBeVisible(); + + // Verify that the Private and Public read-only hostname is rendered correctly + expect(screen.getByText(PRIVATE_STANDBY)).toBeVisible(); + expect(screen.getByText(`public-${DEFAULT_STANDBY}`)).toBeVisible(); + }); +}); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.tsx b/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.tsx new file mode 100644 index 00000000000..f1adca94a1b --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.tsx @@ -0,0 +1,90 @@ +import { Typography } from '@linode/ui'; +import * as React from 'react'; + +import { ConnectionDetailsHostDisplay } from './ConnectionDetailsHostDisplay'; +import { ConnectionDetailsRow } from './ConnectionDetailsRow'; +import { useStyles } from './DatabaseSummary/DatabaseSummaryConnectionDetails.style'; + +import type { Database } from '@linode/api-v4/lib/databases/types'; + +interface ConnectionDetailsHostRowsProps { + database: Database; + isSummaryTab?: boolean; +} + +/** + * This component is responsible for conditionally rendering the Private Host, Public Host, and Read-only Host rows that get displayed in + * the Connection Details tables that appear in the Database Summary and Networking tabs */ +export const ConnectionDetailsHostRows2 = ( + props: ConnectionDetailsHostRowsProps +) => { + const { database, isSummaryTab } = props; + const { classes } = useStyles(); + + const hasVPC = Boolean(database?.private_network?.vpc_id); + const hasPublicVPC = hasVPC && database?.private_network?.public_access; + + const getPrimaryHostContent = (mode?: 'private' | 'public') => { + const isPublic = mode === 'private' ? false : true; + const primaryHost = database.hosts?.endpoints.find( + (endpoint) => + endpoint.role === 'primary' && endpoint.public_access === isPublic + ); + + if (!primaryHost) { + return ( + + + Your hostname will appear here once it is available. + + + ); + } + + return ; + }; + + const getReadOnlyHostContent = (mode?: 'private' | 'public') => { + const isPublic = mode === 'private' ? false : true; + const readOnlyHost = database.hosts?.endpoints.find( + (endpoint) => + endpoint.role === 'standby' && endpoint.public_access === isPublic + ); + + if (!readOnlyHost) { + return 'N/A'; + } + + return ; + }; + + return ( + <> + + {getPrimaryHostContent(hasVPC ? 'private' : 'public')} + + {hasPublicVPC && ( + + {getPrimaryHostContent('public')} + + )} + + {getReadOnlyHostContent(hasVPC ? 'private' : 'public')} + + {hasPublicVPC && ( + + {getReadOnlyHostContent('public')} + + )} + + ); +}; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseManageNetworking.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseManageNetworking.tsx index a441e72e98c..27b0993432b 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseManageNetworking.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseManageNetworking.tsx @@ -15,6 +15,7 @@ import { useFlags } from 'src/hooks/useFlags'; import { MANAGE_NETWORKING_LEARN_MORE_LINK } from '../../constants'; import { makeSettingsItemStyles } from '../../shared.styles'; import { ConnectionDetailsHostRows } from '../ConnectionDetailsHostRows'; +import { ConnectionDetailsHostRows2 } from '../ConnectionDetailsHostRows2'; import { ConnectionDetailsRow } from '../ConnectionDetailsRow'; import { StyledGridContainer } from '../DatabaseSummary/DatabaseSummaryClusterConfiguration.style'; import DatabaseManageNetworkingDrawer from './DatabaseManageNetworkingDrawer'; @@ -122,8 +123,11 @@ export const DatabaseManageNetworking = ({ database }: Props) => { )} - - + {flags.hostnameEndpoints ? ( + + ) : ( + + )} {hasVPCConfigured && ( {database?.private_network?.public_access ? 'Yes' : 'No'} diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx index 5329032f66c..378605b6578 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx @@ -10,6 +10,7 @@ import { useFlags } from 'src/hooks/useFlags'; import { isDefaultDatabase } from '../../utilities'; import { ConnectionDetailsHostRows } from '../ConnectionDetailsHostRows'; +import { ConnectionDetailsHostRows2 } from '../ConnectionDetailsHostRows2'; import { ConnectionDetailsRow } from '../ConnectionDetailsRow'; import { ServiceURI } from '../ServiceURI'; import { StyledGridContainer } from './DatabaseSummaryClusterConfiguration.style'; @@ -136,7 +137,11 @@ export const DatabaseSummaryConnectionDetails = (props: Props) => { {isLegacy ? database.engine : 'defaultdb'} - + {flags.hostnameEndpoints ? ( + + ) : ( + + )} {database.port} diff --git a/packages/manager/src/features/Databases/utilities.test.ts b/packages/manager/src/features/Databases/utilities.test.ts index 1f3648fbdd4..dbc95ab1275 100644 --- a/packages/manager/src/features/Databases/utilities.test.ts +++ b/packages/manager/src/features/Databases/utilities.test.ts @@ -545,7 +545,7 @@ describe('getReadOnlyHost', () => { { address: 'public-primary.example.com', role: 'primary' as HostEndpointRole, - private_access: false, + public_access: true, port: 12345, }, ], @@ -565,7 +565,7 @@ describe('getReadOnlyHost', () => { { address: 'public-primary.example.com', role: 'primary' as HostEndpointRole, - private_access: false, + public_access: true, port: 12345, }, ], diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 30a2a54d24c..2ab6f042881 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -220,26 +220,72 @@ const makeMockDatabase = (params: PathParams): Database => { const database = databaseFactory.build(db); - if (database.platform !== 'rdbms-default') { - delete database.private_network; - } + // Mock a database cluster with a public VPC Configuration + database.private_network = { + public_access: true, + subnet_id: 123, + vpc_id: 10, + }; - if (database.platform === 'rdbms-default' && !!database.private_network) { + if (database.private_network) { // When a database is configured with a VPC, the primary and standby hostnames are prepended with 'private-' in the backend database.hosts = { primary: 'private-db-mysql-primary-0.b.linodeb.net', standby: 'private-db-mysql-standby-0.b.linodeb.net', + /** + * The contents of the hosts.endpoints vary based off whether the VPC has public access or not. + * If private_network public_access is true, the endpoints should return both public and private addresses. + * If private_network public_access is false, the endpoints should only return private addresses. + */ endpoints: [ { - address: 'private-db-mysql-primary-0.b.linodeb.net', role: 'primary', - private_access: true, - port: 12345, + address: 'public-db-mysql-primary-0.b.linodeb.net', + port: 15847, + public_access: true, + }, + { + role: 'primary', + address: 'private-db-mysql-primary-0.b.linodeb.net', + port: 15847, + public_access: false, + }, + { + role: 'standby', + address: 'public-replica-db-mysql-standby-0.b.linodeb.net', + port: 15847, + public_access: true, + }, + { + role: 'standby', + address: 'private-replica-db-mysql-standby-0.b.linodeb.net', + port: 15847, + public_access: false, + }, + { + role: 'primary-connection-pool', + address: 'private-db-mysql-primary-0.b.linodeb.net', + port: 15848, + public_access: false, }, ], }; } + // Uncomment the lines below to mock a database cluster without a VPC configuration + // database.private_network = null; + // database.hosts = { + // primary: 'db-mysql-primary-0.b.linodeb.net', + // endpoints: [ + // { + // role: 'primary', + // address: 'db-mysql-primary-0.b.linodeb.net', + // port: 15847, + // public_access: true, + // }, + // ], + // }; + return database; };