Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-12939-added-1759348628476.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Added
---

ConnectionDetailsRow and ConnectionDetailsHostRows components to manage connection details table content ([#12939](https://github.com/linode/manager/pull/12939))
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-12939-changed-1759348579491.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Changed
---

DBaaS - Host field in connection details table renders based on VPC configuration and host fields are synced between Details and Networking tabs ([#12939](https://github.com/linode/manager/pull/12939))
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import React from 'react';

import { databaseFactory } from 'src/factories/databases';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { ConnectionDetailsHostRows } from './ConnectionDetailsHostRows';

import type { Database } from '@linode/api-v4/lib/databases';

const DEFAULT_PRIMARY = 'private-db-mysql-default-primary.net';
const DEFAULT_STANDBY = 'db-mysql-default-standby.net';

const LEGACY_PRIMARY = 'db-mysql-legacy-primary.net';
const LEGACY_SECONDARY = 'db-mysql-legacy-secondary.net';

describe('ConnectionDetailsHostRows', () => {
it('should display correctly for default database', () => {
const database = databaseFactory.build({
hosts: {
primary: DEFAULT_PRIMARY,
secondary: undefined,

Check warning on line 21 in packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Use null instead. Raw Output: {"ruleId":"sonarjs/no-undefined-assignment","severity":1,"message":"Use null instead.","line":21,"column":20,"nodeType":"Identifier","messageId":"useNull","endLine":21,"endColumn":29}
standby: DEFAULT_STANDBY,
},
platform: 'rdbms-default',

Check warning on line 24 in packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Define a constant instead of duplicating this literal 7 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 7 times.","line":24,"column":17,"nodeType":"Literal","endLine":24,"endColumn":32}
private_network: null, // Added to test that Host field renders
}) as Database;

const { queryAllByText } = renderWithTheme(
<ConnectionDetailsHostRows database={database} />
);

expect(queryAllByText('Host')).toHaveLength(1);
expect(queryAllByText(DEFAULT_PRIMARY)).toHaveLength(1);

expect(queryAllByText('Read-only Host')).toHaveLength(1);
});

it('should display N/A for default DB with blank read-only Host field', () => {
const database = databaseFactory.build({
hosts: {
primary: DEFAULT_PRIMARY,
secondary: undefined,

Check warning on line 42 in packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Use null instead. Raw Output: {"ruleId":"sonarjs/no-undefined-assignment","severity":1,"message":"Use null instead.","line":42,"column":20,"nodeType":"Identifier","messageId":"useNull","endLine":42,"endColumn":29}
standby: undefined,

Check warning on line 43 in packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Use null instead. Raw Output: {"ruleId":"sonarjs/no-undefined-assignment","severity":1,"message":"Use null instead.","line":43,"column":18,"nodeType":"Identifier","messageId":"useNull","endLine":43,"endColumn":27}
},
platform: 'rdbms-default',
});

const { queryAllByText } = renderWithTheme(
<ConnectionDetailsHostRows database={database} />
);

expect(queryAllByText('N/A')).toHaveLength(1);
});

it('should display Host rows correctly for legacy db', () => {
const database = databaseFactory.build({
hosts: {
primary: LEGACY_PRIMARY,
secondary: LEGACY_SECONDARY,
standby: undefined,

Check warning on line 60 in packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Use null instead. Raw Output: {"ruleId":"sonarjs/no-undefined-assignment","severity":1,"message":"Use null instead.","line":60,"column":18,"nodeType":"Identifier","messageId":"useNull","endLine":60,"endColumn":27}
},
id: 22,
platform: 'rdbms-legacy',
port: 3306,
ssl_connection: true,
}) as Database;

const { queryAllByText } = renderWithTheme(
<ConnectionDetailsHostRows database={database} />
);

expect(queryAllByText('Host')).toHaveLength(1);
expect(queryAllByText(LEGACY_PRIMARY)).toHaveLength(1);

expect(queryAllByText('Private Network Host')).toHaveLength(1);
expect(queryAllByText(LEGACY_SECONDARY)).toHaveLength(1);
});

it('should display provisioning text when hosts are not available', () => {
const database = databaseFactory.build({
hosts: undefined,

Check warning on line 81 in packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Use null instead. Raw Output: {"ruleId":"sonarjs/no-undefined-assignment","severity":1,"message":"Use null instead.","line":81,"column":14,"nodeType":"Identifier","messageId":"useNull","endLine":81,"endColumn":23}
platform: 'rdbms-default',
}) as Database;

const { getByText } = renderWithTheme(
<ConnectionDetailsHostRows database={database} />
);

const hostNameProvisioningText = getByText(
'Your hostname will appear here once it is available.'
);

expect(hostNameProvisioningText).toBeInTheDocument();
});

it('should display Host when VPC is not configured', () => {
const privateStrIndex = DEFAULT_PRIMARY.indexOf('-');
const baseHostName = DEFAULT_PRIMARY.slice(privateStrIndex + 1);

const database = databaseFactory.build({
hosts: {
primary: baseHostName,
},
platform: 'rdbms-default',
private_network: null, // VPC not configured
}) as Database;

const { queryAllByText } = renderWithTheme(
<ConnectionDetailsHostRows database={database} />
);

expect(queryAllByText('Host')).toHaveLength(1);
expect(queryAllByText(baseHostName)).toHaveLength(1);
});

it('should display Private Host field when VPC is configured with public access as false', () => {
const database = databaseFactory.build({
hosts: {
primary: DEFAULT_PRIMARY,
secondary: undefined,

Check warning on line 120 in packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Use null instead. Raw Output: {"ruleId":"sonarjs/no-undefined-assignment","severity":1,"message":"Use null instead.","line":120,"column":20,"nodeType":"Identifier","messageId":"useNull","endLine":120,"endColumn":29}
standby: undefined,

Check warning on line 121 in packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Use null instead. Raw Output: {"ruleId":"sonarjs/no-undefined-assignment","severity":1,"message":"Use null instead.","line":121,"column":18,"nodeType":"Identifier","messageId":"useNull","endLine":121,"endColumn":27}
},
platform: 'rdbms-default',
private_network: {
public_access: false,
subnet_id: 1,
vpc_id: 123,
},
}) as Database;

const { queryAllByText } = renderWithTheme(
<ConnectionDetailsHostRows database={database} />
);
expect(queryAllByText('Private Host')).toHaveLength(1);
expect(queryAllByText(DEFAULT_PRIMARY)).toHaveLength(1);
});

it('should display both Private Host and Public Host fields when VPC is configured with public access as true', () => {
const database = databaseFactory.build({
hosts: {
primary: DEFAULT_PRIMARY,
secondary: undefined,

Check warning on line 142 in packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Use null instead. Raw Output: {"ruleId":"sonarjs/no-undefined-assignment","severity":1,"message":"Use null instead.","line":142,"column":20,"nodeType":"Identifier","messageId":"useNull","endLine":142,"endColumn":29}
standby: undefined,

Check warning on line 143 in packages/manager/src/features/Databases/DatabaseDetail/ConnectionDetailsHostRows.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Use null instead. Raw Output: {"ruleId":"sonarjs/no-undefined-assignment","severity":1,"message":"Use null instead.","line":143,"column":18,"nodeType":"Identifier","messageId":"useNull","endLine":143,"endColumn":27}
},
platform: 'rdbms-default',
private_network: {
public_access: true,
subnet_id: 1,
vpc_id: 123,
},
}) as Database;

const { queryAllByText } = renderWithTheme(
<ConnectionDetailsHostRows database={database} />
);
// Verify that both Private Host and Public Host fields are rendered
expect(queryAllByText('Private Host')).toHaveLength(1);
expect(queryAllByText('Public Host')).toHaveLength(1);

// Verify that the Private hostname is rendered correctly
expect(queryAllByText(DEFAULT_PRIMARY)).toHaveLength(1);
// Verify that the Public hostname is rendered correctly
const privateStrIndex = DEFAULT_PRIMARY.indexOf('-');
const baseHostName = DEFAULT_PRIMARY.slice(privateStrIndex + 1);
const expectedPublicHostname = `public-${baseHostName}`;
expect(queryAllByText(expectedPublicHostname)).toHaveLength(1);
});

it('should display Read-only Host when read-only host is available', () => {
const database = databaseFactory.build({
hosts: {
primary: DEFAULT_PRIMARY,
secondary: undefined,
standby: DEFAULT_STANDBY,
},
platform: 'rdbms-default',
}) as Database;

const { queryAllByText } = renderWithTheme(
<ConnectionDetailsHostRows database={database} />
);

expect(queryAllByText('Read-only Host')).toHaveLength(1);
expect(queryAllByText(DEFAULT_STANDBY)).toHaveLength(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
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,
SUMMARY_PRIVATE_HOST_LEGACY_COPY,
} from '../constants';
import { getReadOnlyHost, isLegacyDatabase } from '../utilities';
import { ConnectionDetailsRow } from './ConnectionDetailsRow';
import { useStyles } from './DatabaseSummary/DatabaseSummaryConnectionDetails.style';

import type { Database } from '@linode/api-v4/lib/databases/types';

interface ConnectionDetailsHostRowsProps {
database: Database;
}

/**
* 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 = (
props: ConnectionDetailsHostRowsProps
) => {
const { database } = props;
const { classes } = useStyles();

const sxTooltipIcon = {
marginLeft: '4px',
padding: '0px',
};

const hostTooltipComponentProps = {
tooltip: {
style: {
minWidth: 285,
},
},
};

const isLegacy = isLegacyDatabase(database);
const hasVPC = Boolean(database?.private_network?.vpc_id);
const hasPublicVPC = hasVPC && database?.private_network?.public_access;

const getHostContent = (
mode: 'default' | 'private' | 'public' = 'default'
) => {
let primaryHostName = database.hosts?.primary;

if (mode === 'public' && primaryHostName) {
// Remove 'private-' substring at the beginning of the hostname and replace it with 'public-'
const privateStrIndex = database.hosts.primary.indexOf('-');
const baseHostName = database.hosts.primary.slice(privateStrIndex + 1);
primaryHostName = `public-${baseHostName}`;
}

if (primaryHostName) {
return (
<>
{primaryHostName}
<CopyTooltip
className={classes.inlineCopyToolTip}
text={primaryHostName}
/>
{!isLegacy && (
<TooltipIcon
componentsProps={hostTooltipComponentProps}
status="info"
sxTooltipIcon={sxTooltipIcon}
text={
mode === 'private'
? SUMMARY_PRIVATE_HOST_COPY
: SUMMARY_HOST_TOOLTIP_COPY
}
/>
)}
</>
);
}

return (
<Typography>
<span className={classes.provisioningText}>
Your hostname will appear here once it is available.
</span>
</Typography>
);
};

const getReadOnlyHostContent = () => {
const defaultValue = isLegacy ? '-' : 'N/A';
const value = getReadOnlyHost(database) || defaultValue;
const hasHost = value !== '-' && value !== 'N/A';
return (
<>
{value}
{value && hasHost && (
<CopyTooltip className={classes.inlineCopyToolTip} text={value} />
)}
{isLegacy && (
<TooltipIcon
status="info"
sxTooltipIcon={sxTooltipIcon}
text={SUMMARY_PRIVATE_HOST_LEGACY_COPY}
/>
)}
{!isLegacy && hasHost && (
<TooltipIcon
componentsProps={hostTooltipComponentProps}
status="info"
sxTooltipIcon={sxTooltipIcon}
text={SUMMARY_HOST_TOOLTIP_COPY}
/>
)}
</>
);
};

return (
<>
<ConnectionDetailsRow label={hasVPC ? 'Private Host' : 'Host'}>
{getHostContent(hasVPC ? 'private' : 'default')}
</ConnectionDetailsRow>
{hasPublicVPC && (
<ConnectionDetailsRow label="Public Host">
{getHostContent('public')}
</ConnectionDetailsRow>
)}
<ConnectionDetailsRow
label={isLegacy ? 'Private Network Host' : 'Read-only Host'}
>
{getReadOnlyHostContent()}
</ConnectionDetailsRow>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';

import { renderWithTheme } from 'src/utilities/testHelpers';

import { ConnectionDetailsRow } from './ConnectionDetailsRow';

describe('ConnectionDetailsRow', () => {
it('should render provided label and children', async () => {
const { getByText } = renderWithTheme(
<ConnectionDetailsRow label="Test Label">
<p>Test Children Prop</p>
</ConnectionDetailsRow>
);
const testLabel = getByText('Test Label');
const testChildrenProp = getByText('Test Children Prop');

expect(testLabel).toBeInTheDocument();
expect(testChildrenProp).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Grid } from '@mui/material';
import * as React from 'react';

import {
StyledLabelTypography,
StyledValueGrid,
} from './DatabaseSummary/DatabaseSummaryClusterConfiguration.style';

interface ConnectionDetailsRowProps {
children: React.ReactNode;
label: string;
}

export const ConnectionDetailsRow = (props: ConnectionDetailsRowProps) => {
const { children, label } = props;
return (
<>
<Grid
size={{
md: 4,
xs: 3,
}}
>
<StyledLabelTypography>{label}</StyledLabelTypography>
</Grid>
<StyledValueGrid size={{ md: 8, xs: 9 }}>{children}</StyledValueGrid>
</>
);
};
Loading
Loading