From c02de8c824ded26a6a44c7b0384061a06e1573d4 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Wed, 18 Feb 2026 17:35:46 -0500 Subject: [PATCH 01/11] add hostname endpoint feature flag --- packages/manager/src/dev-tools/FeatureFlagTool.tsx | 1 + packages/manager/src/featureFlags.ts | 1 + 2 files changed, 2 insertions(+) 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/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; From b591469609a0c89b9568d1fdbef782a2d231e4f5 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Wed, 18 Feb 2026 17:41:30 -0500 Subject: [PATCH 02/11] use hostname endpoints in database summary connection details --- packages/api-v4/src/databases/types.ts | 2 +- packages/manager/src/factories/databases.ts | 8 +- .../ConnectionDetailsHostRows.tsx | 2 + .../ConnectionDetailsHostRows2.tsx | 142 ++++++++++++++++++ .../DatabaseSummaryConnectionDetails.tsx | 7 +- .../src/features/Databases/utilities.test.ts | 4 +- packages/manager/src/mocks/serverHandlers.ts | 72 ++++++++- 7 files changed, 222 insertions(+), 15 deletions(-) create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.tsx diff --git a/packages/api-v4/src/databases/types.ts b/packages/api-v4/src/databases/types.ts index 59df60aa1c0..ab028572cfc 100644 --- a/packages/api-v4/src/databases/types.ts +++ b/packages/api-v4/src/databases/types.ts @@ -99,7 +99,7 @@ export type HostEndpointRole = interface HostEndpoint { address: string; port: number; - private_access: boolean; + public_access: boolean; role: HostEndpointRole; } 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/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.tsx b/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.tsx new file mode 100644 index 00000000000..96f82ae62de --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.tsx @@ -0,0 +1,142 @@ +import { TooltipIcon, Typography } from '@linode/ui'; +import * as React from 'react'; + +import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; + +import { + SUMMARY_HOST_TOOLTIP_COPY, + SUMMARY_PRIVATE_HOST_COPY, +} from '../constants'; +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 sxTooltipIcon = { + marginLeft: '4px', + padding: '0px', + }; + + const hostTooltipComponentProps = { + tooltip: { + style: { + minWidth: 285, + }, + }, + }; + + 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 primaryHostName = database.hosts?.endpoints.find( + (endpoint) => + endpoint.role === 'primary' && endpoint.public_access === isPublic + )?.address; + + if (!primaryHostName) { + return ( + + + Your hostname will appear here once it is available. + + + ); + } + + return ( + <> + {primaryHostName} + + + + ); + }; + + 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 ( + <> + {readOnlyHost?.address} + + + + ); + }; + + return ( + <> + + {getPrimaryHostContent(hasVPC ? 'private' : 'public')} + + {hasPublicVPC && ( + + {getPrimaryHostContent('public')} + + )} + + {getReadOnlyHostContent(hasVPC ? 'private' : 'public')} + + {hasPublicVPC && ( + + {getReadOnlyHostContent('public')} + + )} + + ); +}; 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..389661a07a6 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -220,22 +220,80 @@ const makeMockDatabase = (params: PathParams): Database => { const database = databaseFactory.build(db); - if (database.platform !== 'rdbms-default') { - delete database.private_network; - } + database.private_network = { + public_access: true, + subnet_id: 123, + vpc_id: 10, + }; - if (database.platform === 'rdbms-default' && !!database.private_network) { + // database.private_network = null; + + // database.hosts = { + // primary: 'public-db-mysql-primary-0.b.linodeb.net', + // endpoints: [ + // { + // role: 'primary', + // address: 'public-db-mysql-primary-0.b.linodeb.net', + // port: 15847, + // public_access: true, + // }, + // ], + // }; + + 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', 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, }, + // { + // role: 'standby-connection-pool', + // address: 'private-replica-db-mysql-standby-0.b.linodeb.net', + // port: 15848, + // public_access: false, + // }, + // { + // role: 'primary-connection-pool', + // address: 'public-replica-db-mysql-standby-0.b.linodeb.net', + // port: 15848, + // public_access: true, + // }, + // { + // role: 'standby-connection-pool', + // address: 'public-replica-db-mysql-standby-0.b.linodeb.net', + // port: 15848, + // public_access: true, + // }, ], }; } From dd94dbed90fc8573e86cb053fc7fbf3069826ae7 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Wed, 18 Feb 2026 17:42:03 -0500 Subject: [PATCH 03/11] use hostname endpoints in database networking tab --- .../DatabaseNetworking/DatabaseManageNetworking.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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'} From f0ee457daeab99ccaf518f7cddcfe187d5cc45f3 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Thu, 19 Feb 2026 16:27:35 -0500 Subject: [PATCH 04/11] clean up --- packages/api-v4/src/databases/types.ts | 2 +- .../ConnectionDetailsHostRows2.tsx | 93 +++++------ packages/manager/src/mocks/serverHandlers.ts | 158 +++++++++--------- 3 files changed, 120 insertions(+), 133 deletions(-) diff --git a/packages/api-v4/src/databases/types.ts b/packages/api-v4/src/databases/types.ts index ab028572cfc..971a59a6869 100644 --- a/packages/api-v4/src/databases/types.ts +++ b/packages/api-v4/src/databases/types.ts @@ -96,7 +96,7 @@ export type HostEndpointRole = | 'standby' | 'standby-connection-pool'; -interface HostEndpoint { +export interface HostEndpoint { address: string; port: number; public_access: boolean; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.tsx b/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.tsx index 96f82ae62de..b9f6a6b1275 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.tsx @@ -10,7 +10,10 @@ import { import { ConnectionDetailsRow } from './ConnectionDetailsRow'; import { useStyles } from './DatabaseSummary/DatabaseSummaryConnectionDetails.style'; -import type { Database } from '@linode/api-v4/lib/databases/types'; +import type { + Database, + HostEndpoint, +} from '@linode/api-v4/lib/databases/types'; interface ConnectionDetailsHostRowsProps { database: Database; @@ -26,30 +29,48 @@ export const ConnectionDetailsHostRows2 = ( const { database, isSummaryTab } = props; const { classes } = useStyles(); - const sxTooltipIcon = { - marginLeft: '4px', - padding: '0px', - }; - - const hostTooltipComponentProps = { - tooltip: { - style: { - minWidth: 285, - }, - }, - }; - const hasVPC = Boolean(database?.private_network?.vpc_id); const hasPublicVPC = hasVPC && database?.private_network?.public_access; + const getHostDisplay = (host: HostEndpoint) => { + return ( + <> + {host?.address} + + + + ); + }; + const getPrimaryHostContent = (mode?: 'private' | 'public') => { const isPublic = mode === 'private' ? false : true; - const primaryHostName = database.hosts?.endpoints.find( + const primaryHost = database.hosts?.endpoints.find( (endpoint) => endpoint.role === 'primary' && endpoint.public_access === isPublic - )?.address; + ); - if (!primaryHostName) { + if (!primaryHost) { return ( @@ -59,23 +80,7 @@ export const ConnectionDetailsHostRows2 = ( ); } - return ( - <> - {primaryHostName} - - - - ); + return getHostDisplay(primaryHost); }; const getReadOnlyHostContent = (mode?: 'private' | 'public') => { @@ -89,25 +94,7 @@ export const ConnectionDetailsHostRows2 = ( return 'N/A'; } - return ( - <> - {readOnlyHost?.address} - - - - ); + return getHostDisplay(readOnlyHost); }; return ( diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 389661a07a6..918fb6e2f28 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -220,26 +220,26 @@ const makeMockDatabase = (params: PathParams): Database => { const database = databaseFactory.build(db); - database.private_network = { - public_access: true, - subnet_id: 123, - vpc_id: 10, - }; - + // No VPC // database.private_network = null; - // database.hosts = { - // primary: 'public-db-mysql-primary-0.b.linodeb.net', + // primary: 'db-mysql-primary-0.b.linodeb.net', // endpoints: [ // { // role: 'primary', - // address: 'public-db-mysql-primary-0.b.linodeb.net', + // address: 'db-mysql-primary-0.b.linodeb.net', // port: 15847, // public_access: true, // }, // ], // }; + database.private_network = { + public_access: true, + subnet_id: 123, + vpc_id: 10, + }; + 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 = { @@ -1579,12 +1579,12 @@ export const handlers = [ const match = isPrefixListSpecial ? [] // Special PLs: API currently returns empty; @TODO: update with actual response once API supports them : [ - existingPrefixList ?? - firewallPrefixListFactory.build({ - name: filter.name, - description: `${filter.name} description`, - }), - ]; + existingPrefixList ?? + firewallPrefixListFactory.build({ + name: filter.name, + description: `${filter.name} description`, + }), + ]; return HttpResponse.json(makeResourcePage(match)); } } @@ -1638,46 +1638,46 @@ export const handlers = [ const firewall = params.firewallId === '1001' ? firewallFactory.build({ - id: 1001, - label: 'firewall with rule and ruleset reference', - rules: firewallRulesFactory.build({ - inbound: [ - { ruleset: 123 }, // Referenced Ruleset to the Firewall (ID 123) - { ruleset: 123456789 }, // Referenced Ruleset to the Firewall (ID 123456789) - ...firewallRuleFactory.buildList(1, { - addresses: { - ipv4: [ - 'pl::supports-both', - 'pl:system:supports-only-ipv4', - '192.168.1.213', - '192.168.1.214', - 'pl::vpcs:supports-both-1', - 'pl::supports-both-but-empty-both', - '172.31.255.255', - 'pl::marked-for-deletion', - 'pl::vpcs:', // special prefixlist - ], - ipv6: [ - 'pl::supports-both', - 'pl::supports-only-ipv6', - 'pl::supports-both-but-ipv6-empty', - 'pl::vpcs:supports-both-2', - '2001:db8:85a3::8a2e:370:7334/128', - '2001:db8:85a3::8a2e:371:7335/128', - // Duplicate PrefixList entries like the below one, may not appear, but if they do, - // our logic will treat them as a single entity within the ipv4 or ipv6 array. - 'pl::vpcs:supports-both-2', - '2001:db8:85a3::8a2e:372:7336/128', - 'pl::subnets:', // special prefixlist - ], - }, - ports: '22, 53, 80, 100, 443, 3306', - protocol: 'UDP', - action: 'ACCEPT', - }), - ], - }), - }) + id: 1001, + label: 'firewall with rule and ruleset reference', + rules: firewallRulesFactory.build({ + inbound: [ + { ruleset: 123 }, // Referenced Ruleset to the Firewall (ID 123) + { ruleset: 123456789 }, // Referenced Ruleset to the Firewall (ID 123456789) + ...firewallRuleFactory.buildList(1, { + addresses: { + ipv4: [ + 'pl::supports-both', + 'pl:system:supports-only-ipv4', + '192.168.1.213', + '192.168.1.214', + 'pl::vpcs:supports-both-1', + 'pl::supports-both-but-empty-both', + '172.31.255.255', + 'pl::marked-for-deletion', + 'pl::vpcs:', // special prefixlist + ], + ipv6: [ + 'pl::supports-both', + 'pl::supports-only-ipv6', + 'pl::supports-both-but-ipv6-empty', + 'pl::vpcs:supports-both-2', + '2001:db8:85a3::8a2e:370:7334/128', + '2001:db8:85a3::8a2e:371:7335/128', + // Duplicate PrefixList entries like the below one, may not appear, but if they do, + // our logic will treat them as a single entity within the ipv4 or ipv6 array. + 'pl::vpcs:supports-both-2', + '2001:db8:85a3::8a2e:372:7336/128', + 'pl::subnets:', // special prefixlist + ], + }, + ports: '22, 53, 80, 100, 443, 3306', + protocol: 'UDP', + action: 'ACCEPT', + }), + ], + }), + }) : firewallFactory.build(); return HttpResponse.json(firewall); }), @@ -1908,12 +1908,12 @@ export const handlers = [ const buckets = region !== 'ap-west' && region !== 'us-iad' ? objectStorageBucketFactoryGen2.buildList(1, { - cluster: `${region}-1`, - endpoint_type: randomEndpointType, - hostname: `obj-bucket-${randomBucketNumber}.${region}.linodeobjects.com`, - label: `obj-bucket-${randomBucketNumber}`, - region, - }) + cluster: `${region}-1`, + endpoint_type: randomEndpointType, + hostname: `obj-bucket-${randomBucketNumber}.${region}.linodeobjects.com`, + label: `obj-bucket-${randomBucketNumber}`, + region, + }) : []; if (region === 'ap-west') { buckets.push( @@ -2291,9 +2291,9 @@ export const handlers = [ headers.status === 'completed' ? accountMaintenanceFactory.buildList(30, { status: 'completed' }) : [ - ...accountMaintenanceFactory.buildList(90, { status: 'pending' }), - ...accountMaintenanceFactory.buildList(3, { status: 'started' }), - ]; + ...accountMaintenanceFactory.buildList(90, { status: 'pending' }), + ...accountMaintenanceFactory.buildList(3, { status: 'started' }), + ]; if (request.headers.get('x-filter')) { accountMaintenance.sort((a, b) => { @@ -2506,9 +2506,9 @@ export const handlers = [ const grantsResponse = grantsFactory.build({ global: parentAccountNonAdminUser.restricted ? { - cancel_account: false, - child_account_access: true, - } + cancel_account: false, + child_account_access: true, + } : undefined, }); return HttpResponse.json(grantsResponse); @@ -3124,19 +3124,19 @@ export const handlers = [ // The availability of MTC plans is fully handled by this endpoint, which determines the plan's availability status (true/false) for the selected region. ...(['no-osl-1', 'us-iad', 'us-iad-2'].includes(selectedRegion) ? [ - regionAvailabilityFactory.build({ - available: true, // In supported regions, this can be `true` (plan available) or `false` (plan sold-out). - plan: 'g8-premium-128-ht', - region: selectedRegion, - }), - ] + regionAvailabilityFactory.build({ + available: true, // In supported regions, this can be `true` (plan available) or `false` (plan sold-out). + plan: 'g8-premium-128-ht', + region: selectedRegion, + }), + ] : [ - regionAvailabilityFactory.build({ - available: false, // In unsupported region, this will always be `false` (Plan not offered/not available). - plan: 'g8-premium-128-ht', - region: selectedRegion, - }), - ]), + regionAvailabilityFactory.build({ + available: false, // In unsupported region, this will always be `false` (Plan not offered/not available). + plan: 'g8-premium-128-ht', + region: selectedRegion, + }), + ]), ]); }), From 8eeb1c62e22a19506e5596f60ce23eef259e530e Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Thu, 19 Feb 2026 17:33:03 -0500 Subject: [PATCH 05/11] clean up --- .../ConnectionDetailsHostDisplay.tsx | 62 +++++++++++++++++++ .../ConnectionDetailsHostRows2.tsx | 49 ++------------- 2 files changed, 67 insertions(+), 44 deletions(-) create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostDisplay.tsx 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/ConnectionDetailsHostRows2.tsx b/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.tsx index b9f6a6b1275..f1adca94a1b 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.tsx @@ -1,19 +1,11 @@ -import { TooltipIcon, Typography } from '@linode/ui'; +import { Typography } from '@linode/ui'; import * as React from 'react'; -import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; - -import { - SUMMARY_HOST_TOOLTIP_COPY, - SUMMARY_PRIVATE_HOST_COPY, -} from '../constants'; +import { ConnectionDetailsHostDisplay } from './ConnectionDetailsHostDisplay'; import { ConnectionDetailsRow } from './ConnectionDetailsRow'; import { useStyles } from './DatabaseSummary/DatabaseSummaryConnectionDetails.style'; -import type { - Database, - HostEndpoint, -} from '@linode/api-v4/lib/databases/types'; +import type { Database } from '@linode/api-v4/lib/databases/types'; interface ConnectionDetailsHostRowsProps { database: Database; @@ -32,37 +24,6 @@ export const ConnectionDetailsHostRows2 = ( const hasVPC = Boolean(database?.private_network?.vpc_id); const hasPublicVPC = hasVPC && database?.private_network?.public_access; - const getHostDisplay = (host: HostEndpoint) => { - return ( - <> - {host?.address} - - - - ); - }; - const getPrimaryHostContent = (mode?: 'private' | 'public') => { const isPublic = mode === 'private' ? false : true; const primaryHost = database.hosts?.endpoints.find( @@ -80,7 +41,7 @@ export const ConnectionDetailsHostRows2 = ( ); } - return getHostDisplay(primaryHost); + return ; }; const getReadOnlyHostContent = (mode?: 'private' | 'public') => { @@ -94,7 +55,7 @@ export const ConnectionDetailsHostRows2 = ( return 'N/A'; } - return getHostDisplay(readOnlyHost); + return ; }; return ( From ecc0a5674cf66db9c92a6a33a5fab92bdbd680fe Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Fri, 20 Feb 2026 09:30:51 -0500 Subject: [PATCH 06/11] update unit tests --- .../ConnectionDetailsHostRows.test.tsx | 4 + .../ConnectionDetailsHostRows2.test.tsx | 182 ++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows2.test.tsx 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/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(); + }); +}); From 8b886cecb6f0d5bf00be264c9ee44af3b95dd04a Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Fri, 20 Feb 2026 09:30:59 -0500 Subject: [PATCH 07/11] linting --- packages/manager/src/mocks/serverHandlers.ts | 140 +++++++++---------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 918fb6e2f28..a60b7cb9986 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -1579,12 +1579,12 @@ export const handlers = [ const match = isPrefixListSpecial ? [] // Special PLs: API currently returns empty; @TODO: update with actual response once API supports them : [ - existingPrefixList ?? - firewallPrefixListFactory.build({ - name: filter.name, - description: `${filter.name} description`, - }), - ]; + existingPrefixList ?? + firewallPrefixListFactory.build({ + name: filter.name, + description: `${filter.name} description`, + }), + ]; return HttpResponse.json(makeResourcePage(match)); } } @@ -1638,46 +1638,46 @@ export const handlers = [ const firewall = params.firewallId === '1001' ? firewallFactory.build({ - id: 1001, - label: 'firewall with rule and ruleset reference', - rules: firewallRulesFactory.build({ - inbound: [ - { ruleset: 123 }, // Referenced Ruleset to the Firewall (ID 123) - { ruleset: 123456789 }, // Referenced Ruleset to the Firewall (ID 123456789) - ...firewallRuleFactory.buildList(1, { - addresses: { - ipv4: [ - 'pl::supports-both', - 'pl:system:supports-only-ipv4', - '192.168.1.213', - '192.168.1.214', - 'pl::vpcs:supports-both-1', - 'pl::supports-both-but-empty-both', - '172.31.255.255', - 'pl::marked-for-deletion', - 'pl::vpcs:', // special prefixlist - ], - ipv6: [ - 'pl::supports-both', - 'pl::supports-only-ipv6', - 'pl::supports-both-but-ipv6-empty', - 'pl::vpcs:supports-both-2', - '2001:db8:85a3::8a2e:370:7334/128', - '2001:db8:85a3::8a2e:371:7335/128', - // Duplicate PrefixList entries like the below one, may not appear, but if they do, - // our logic will treat them as a single entity within the ipv4 or ipv6 array. - 'pl::vpcs:supports-both-2', - '2001:db8:85a3::8a2e:372:7336/128', - 'pl::subnets:', // special prefixlist - ], - }, - ports: '22, 53, 80, 100, 443, 3306', - protocol: 'UDP', - action: 'ACCEPT', - }), - ], - }), - }) + id: 1001, + label: 'firewall with rule and ruleset reference', + rules: firewallRulesFactory.build({ + inbound: [ + { ruleset: 123 }, // Referenced Ruleset to the Firewall (ID 123) + { ruleset: 123456789 }, // Referenced Ruleset to the Firewall (ID 123456789) + ...firewallRuleFactory.buildList(1, { + addresses: { + ipv4: [ + 'pl::supports-both', + 'pl:system:supports-only-ipv4', + '192.168.1.213', + '192.168.1.214', + 'pl::vpcs:supports-both-1', + 'pl::supports-both-but-empty-both', + '172.31.255.255', + 'pl::marked-for-deletion', + 'pl::vpcs:', // special prefixlist + ], + ipv6: [ + 'pl::supports-both', + 'pl::supports-only-ipv6', + 'pl::supports-both-but-ipv6-empty', + 'pl::vpcs:supports-both-2', + '2001:db8:85a3::8a2e:370:7334/128', + '2001:db8:85a3::8a2e:371:7335/128', + // Duplicate PrefixList entries like the below one, may not appear, but if they do, + // our logic will treat them as a single entity within the ipv4 or ipv6 array. + 'pl::vpcs:supports-both-2', + '2001:db8:85a3::8a2e:372:7336/128', + 'pl::subnets:', // special prefixlist + ], + }, + ports: '22, 53, 80, 100, 443, 3306', + protocol: 'UDP', + action: 'ACCEPT', + }), + ], + }), + }) : firewallFactory.build(); return HttpResponse.json(firewall); }), @@ -1908,12 +1908,12 @@ export const handlers = [ const buckets = region !== 'ap-west' && region !== 'us-iad' ? objectStorageBucketFactoryGen2.buildList(1, { - cluster: `${region}-1`, - endpoint_type: randomEndpointType, - hostname: `obj-bucket-${randomBucketNumber}.${region}.linodeobjects.com`, - label: `obj-bucket-${randomBucketNumber}`, - region, - }) + cluster: `${region}-1`, + endpoint_type: randomEndpointType, + hostname: `obj-bucket-${randomBucketNumber}.${region}.linodeobjects.com`, + label: `obj-bucket-${randomBucketNumber}`, + region, + }) : []; if (region === 'ap-west') { buckets.push( @@ -2291,9 +2291,9 @@ export const handlers = [ headers.status === 'completed' ? accountMaintenanceFactory.buildList(30, { status: 'completed' }) : [ - ...accountMaintenanceFactory.buildList(90, { status: 'pending' }), - ...accountMaintenanceFactory.buildList(3, { status: 'started' }), - ]; + ...accountMaintenanceFactory.buildList(90, { status: 'pending' }), + ...accountMaintenanceFactory.buildList(3, { status: 'started' }), + ]; if (request.headers.get('x-filter')) { accountMaintenance.sort((a, b) => { @@ -2506,9 +2506,9 @@ export const handlers = [ const grantsResponse = grantsFactory.build({ global: parentAccountNonAdminUser.restricted ? { - cancel_account: false, - child_account_access: true, - } + cancel_account: false, + child_account_access: true, + } : undefined, }); return HttpResponse.json(grantsResponse); @@ -3124,19 +3124,19 @@ export const handlers = [ // The availability of MTC plans is fully handled by this endpoint, which determines the plan's availability status (true/false) for the selected region. ...(['no-osl-1', 'us-iad', 'us-iad-2'].includes(selectedRegion) ? [ - regionAvailabilityFactory.build({ - available: true, // In supported regions, this can be `true` (plan available) or `false` (plan sold-out). - plan: 'g8-premium-128-ht', - region: selectedRegion, - }), - ] + regionAvailabilityFactory.build({ + available: true, // In supported regions, this can be `true` (plan available) or `false` (plan sold-out). + plan: 'g8-premium-128-ht', + region: selectedRegion, + }), + ] : [ - regionAvailabilityFactory.build({ - available: false, // In unsupported region, this will always be `false` (Plan not offered/not available). - plan: 'g8-premium-128-ht', - region: selectedRegion, - }), - ]), + regionAvailabilityFactory.build({ + available: false, // In unsupported region, this will always be `false` (Plan not offered/not available). + plan: 'g8-premium-128-ht', + region: selectedRegion, + }), + ]), ]); }), From 22933b5fd68f1dd48d2a2dbe9b794f1da86f0b93 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Fri, 20 Feb 2026 10:43:34 -0500 Subject: [PATCH 08/11] Added changeset: Export `HostEndpoint` and rename `private_access` to `public_access` --- packages/api-v4/.changeset/pr-13413-changed-1771602214397.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/api-v4/.changeset/pr-13413-changed-1771602214397.md 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)) From 9ad2d79ff3f425f87c48731a6e67e5ad33e46335 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Fri, 20 Feb 2026 10:44:11 -0500 Subject: [PATCH 09/11] Added changeset: Use new hostname endpoint in Database Summary and Network tab --- .../.changeset/pr-13413-upcoming-features-1771602250906.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-13413-upcoming-features-1771602250906.md 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)) From 4c8508b175769d1e2d30916280be3b5a725f927b Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Fri, 20 Feb 2026 16:06:28 -0500 Subject: [PATCH 10/11] address feedback --- packages/manager/src/mocks/serverHandlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index a60b7cb9986..bef9ad24b0c 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -220,7 +220,7 @@ const makeMockDatabase = (params: PathParams): Database => { const database = databaseFactory.build(db); - // No VPC + // 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', From 9770d1f19d18065be7268f7a36a69edad7243894 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Fri, 20 Feb 2026 16:08:12 -0500 Subject: [PATCH 11/11] address feedback --- packages/manager/src/mocks/serverHandlers.ts | 52 ++++++++------------ 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index bef9ad24b0c..2ab6f042881 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -220,20 +220,7 @@ const makeMockDatabase = (params: PathParams): Database => { const database = databaseFactory.build(db); - // 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, - // }, - // ], - // }; - + // Mock a database cluster with a public VPC Configuration database.private_network = { public_access: true, subnet_id: 123, @@ -245,6 +232,11 @@ const makeMockDatabase = (params: PathParams): Database => { 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: [ { role: 'primary', @@ -276,28 +268,24 @@ const makeMockDatabase = (params: PathParams): Database => { port: 15848, public_access: false, }, - // { - // role: 'standby-connection-pool', - // address: 'private-replica-db-mysql-standby-0.b.linodeb.net', - // port: 15848, - // public_access: false, - // }, - // { - // role: 'primary-connection-pool', - // address: 'public-replica-db-mysql-standby-0.b.linodeb.net', - // port: 15848, - // public_access: true, - // }, - // { - // role: 'standby-connection-pool', - // address: 'public-replica-db-mysql-standby-0.b.linodeb.net', - // port: 15848, - // public_access: true, - // }, ], }; } + // 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; };