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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Changed
---

Updated getDatabaseConnectionPools signature to accept params for pagination ([#13195](https://github.com/linode/manager/pull/13195))
6 changes: 5 additions & 1 deletion packages/api-v4/src/databases/databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,12 +371,16 @@ export const getDatabaseEngineConfig = (engine: Engine) =>
/**
* Get a paginated list of connection pools for a database
*/
export const getDatabaseConnectionPools = (databaseID: number) =>
export const getDatabaseConnectionPools = (
databaseID: number,
params?: Params,
) =>
Request<Page<ConnectionPool>>(
setURL(
`${API_ROOT}/databases/postgresql/instances/${encodeURIComponent(databaseID)}/connection-pools`,
),
setMethod('GET'),
setParams(params),
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to accept params as we need to be able to pass pageSize and pageNumber to the request.


/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Changed
---

DBaaS table action menu wrapper and settings item styles are shared and connection pool queries updated for pagination ([#13195](https://github.com/linode/manager/pull/13195))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

DBaaS PgBouncer Connection Pools section to be displayed in Networking tab for PostgreSQL database clusters ([#13195](https://github.com/linode/manager/pull/13195))
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { screen } from '@testing-library/react';
import * as React from 'react';
import { describe, it } from 'vitest';

import {
databaseConnectionPoolFactory,
databaseFactory,
} from 'src/factories/databases';
import { makeResourcePage } from 'src/mocks/serverHandlers';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { DatabaseConnectionPools } from './DatabaseConnectionPools';

const mockDatabase = databaseFactory.build({
platform: 'rdbms-default',
private_network: null,
engine: 'postgresql',
id: 1,
});

const mockConnectionPool = databaseConnectionPoolFactory.build({
database: 'defaultdb',
label: 'pool-1',
mode: 'transaction',
size: 10,
username: null,
});

// Hoist query mocks
const queryMocks = vi.hoisted(() => {
return {
useDatabaseConnectionPoolsQuery: vi.fn(),
};
});

vi.mock('@linode/queries', async () => {
const actual = await vi.importActual('@linode/queries');
return {
...actual,
useDatabaseConnectionPoolsQuery: queryMocks.useDatabaseConnectionPoolsQuery,
};
});

describe('DatabaseManageNetworkingDrawer Component', () => {
beforeEach(() => {
vi.resetAllMocks();
});

it('should render PgBouncer Connection Pools field', () => {
queryMocks.useDatabaseConnectionPoolsQuery.mockReturnValue({
data: makeResourcePage([mockConnectionPool]),
isLoading: false,
});
renderWithTheme(<DatabaseConnectionPools database={mockDatabase} />);

const heading = screen.getByRole('heading');
expect(heading.textContent).toBe('Manage PgBouncer Connection Pools');
const addPoolBtnLabel = screen.getByText('Add Pool');
expect(addPoolBtnLabel).toBeInTheDocument();
});

it('should render loading state', () => {
queryMocks.useDatabaseConnectionPoolsQuery.mockReturnValue({
data: makeResourcePage([mockConnectionPool]),
isLoading: true,
});
const loadingTestId = 'circle-progress';
renderWithTheme(<DatabaseConnectionPools database={mockDatabase} />);

const loadingCircle = screen.getByTestId(loadingTestId);
expect(loadingCircle).toBeInTheDocument();
});

it('should render table with connection pool data', () => {
queryMocks.useDatabaseConnectionPoolsQuery.mockReturnValue({
data: makeResourcePage([mockConnectionPool]),
isLoading: false,
});

renderWithTheme(<DatabaseConnectionPools database={mockDatabase} />);

const connectionPoolLabel = screen.getByText(mockConnectionPool.label);
expect(connectionPoolLabel).toBeInTheDocument();
});

it('should render table empty state when no data is provided', () => {
queryMocks.useDatabaseConnectionPoolsQuery.mockReturnValue({
data: makeResourcePage([]),
isLoading: false,
});

renderWithTheme(<DatabaseConnectionPools database={mockDatabase} />);

const emptyStateText = screen.getByText(
"You don't have any connection pools added."
);
expect(emptyStateText).toBeInTheDocument();
});

it('should render error state state when backend responds with error', () => {
queryMocks.useDatabaseConnectionPoolsQuery.mockReturnValue({
error: new Error('Failed to fetch VPC'),
});

renderWithTheme(<DatabaseConnectionPools database={mockDatabase} />);
const errorStateText = screen.getByText(
'There was a problem retrieving your connection pools. Refresh the page or try again later.'
);
expect(errorStateText).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { useDatabaseConnectionPoolsQuery } from '@linode/queries';
import {
Button,
CircleProgress,
ErrorState,
Hidden,
Stack,
Typography,
} from '@linode/ui';
import { useTheme } from '@mui/material/styles';
import { Pagination } from 'akamai-cds-react-components/Pagination';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
} from 'akamai-cds-react-components/Table';
import React from 'react';

import { ActionMenu } from 'src/components/ActionMenu/ActionMenu';
import {
MIN_PAGE_SIZE,
PAGE_SIZES,
} from 'src/components/PaginationFooter/PaginationFooter.constants';
import { usePaginationV2 } from 'src/hooks/usePaginationV2';

import {
makeSettingsItemStyles,
StyledActionMenuWrapper,
} from '../../shared.styles';

import type { Database } from '@linode/api-v4';
import type { Action } from 'src/components/ActionMenu/ActionMenu';

interface Props {
database: Database;
disabled?: boolean;
}

export const DatabaseConnectionPools = ({ database }: Props) => {
const { classes } = makeSettingsItemStyles();
const theme = useTheme();
const poolLabelCellStyles = {
flex: '.5 1 20.5%',
};

const pagination = usePaginationV2({
currentRoute: '/databases/$engine/$databaseId/networking',
initialPage: 1,
preferenceKey: `database-connection-pools-pagination`,
});

const {
data: connectionPools,
error: connectionPoolsError,
isLoading: connectionPoolsLoading,
} = useDatabaseConnectionPoolsQuery(database.id, true, {
page: pagination.page,
page_size: pagination.pageSize,
});

const connectionPoolActions: Action[] = [
{
onClick: () => null,
title: 'Edit', // TODO: UIE-9395 Implement edit functionality

Check warning on line 67 in packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseConnectionPools.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Complete the task associated to this "TODO" comment. Raw Output: {"ruleId":"sonarjs/todo-tag","severity":1,"message":"Complete the task associated to this \"TODO\" comment.","line":67,"column":25,"nodeType":null,"messageId":"completeTODO","endLine":67,"endColumn":29}
},
{
onClick: () => null, // TODO: UIE-9430 Implement delete functionality

Check warning on line 70 in packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseConnectionPools.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Complete the task associated to this "TODO" comment. Raw Output: {"ruleId":"sonarjs/todo-tag","severity":1,"message":"Complete the task associated to this \"TODO\" comment.","line":70,"column":31,"nodeType":null,"messageId":"completeTODO","endLine":70,"endColumn":35}
title: 'Delete',
},
];

if (connectionPoolsLoading) {
return <CircleProgress />;
}
Comment on lines +75 to +77
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the desired loading and error state?

We could inline the loading state and error state into the table itself using https://design.linode.com/?path=/docs/components-table-tablerowloading--documentation

Copy link
Contributor Author

@smans-akamai smans-akamai Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bnussman-akamai So the DatabaseConnectionPool component is currently using the cds table web component that was integrated in a previous pull request (#12989) for the database landing page. I opted to use the same setup from the DatabaseLanding for this component for consistency.

After checking the code for the cds-web component table, that table doesn't have an inline error state/loading behavior like we see in the Linode Table component linked above. So we can't implement this behavior for the connection pool table. So for now, I'm planning to follow the same state pattern we see on the landing page.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right. My fault. I forgot DBaaS uses the CDS Web component table. If there isn't an equivalent I don't have any issues with the current approach


if (connectionPoolsError) {
return (
<ErrorState errorText="There was a problem retrieving your connection pools. Refresh the page or try again later." />
);
}

return (
<>
<div className={classes.topSection}>
<Stack spacing={0.5}>
<Typography variant="h3">
Manage PgBouncer Connection Pools
</Typography>
<Typography sx={{ maxWidth: '500px' }}>
Manage PgBouncer connection pools to minimize the use of your server
resources.
</Typography>
</Stack>
<Button
buttonType="outlined"
className={classes.actionBtn}
disabled={true}
onClick={() => null}
TooltipProps={{ placement: 'top' }}
>
Add Pool
</Button>
</div>
<div style={{ overflowX: 'auto', width: '100%' }}>
<Table
aria-label={'List of Connection pools'}
style={
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this style into a styled component since it's taking up a few lines?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean a styled table component?

{
border: `1px solid ${theme.tokens.alias.Border.Normal}`,
marginTop: '10px',
'--token-component-table-header-outlined-border':
theme.tokens.component.Table.Row.Border,
} as React.CSSProperties
}
>
<TableHead>
<TableRow
headerbackground={
theme.tokens.component.Table.HeaderNested.Background
}
headerborder
>
<TableHeaderCell style={poolLabelCellStyles}>
Pool Label
</TableHeaderCell>
<Hidden smDown>
<TableHeaderCell>Pool Mode</TableHeaderCell>
</Hidden>
<Hidden smDown>
<TableHeaderCell>Pool Size</TableHeaderCell>
</Hidden>
<Hidden smDown>
<TableHeaderCell>Username</TableHeaderCell>
</Hidden>
<TableHeaderCell style={{ maxWidth: 40 }} />
</TableRow>
</TableHead>
<TableBody>
{connectionPools?.data.length === 0 ? (
<TableRow data-testid={'table-row-empty'}>
<TableCell
style={{
display: 'flex',
justifyContent: 'center',
}}
>
You don&apos;t have any connection pools added.
</TableCell>
</TableRow>
) : (
connectionPools?.data.map((pool) => (
<TableRow key={`connection-pool-row-${pool.label}`} zebra>
<TableCell style={poolLabelCellStyles}>
{pool.label}
</TableCell>
<Hidden smDown>
<TableCell>
{`${pool.mode.charAt(0).toUpperCase()}${pool.mode.slice(1)}`}
</TableCell>
</Hidden>
<Hidden smDown>
<TableCell>{pool.size}</TableCell>
</Hidden>
<Hidden smDown>
<TableCell>
{pool.username === null
? 'Reuse inbound user'
: pool.username}
</TableCell>
</Hidden>
<StyledActionMenuWrapper>
<ActionMenu
actionsList={connectionPoolActions}
ariaLabel={`Action menu for connection pool ${pool.label}`}
/>
</StyledActionMenuWrapper>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{(connectionPools?.results || 0) > MIN_PAGE_SIZE && (
<Pagination
count={connectionPools?.results || 0}
onPageChange={(e: CustomEvent<number>) =>
pagination.handlePageChange(Number(e.detail))
}
onPageSizeChange={(
e: CustomEvent<{ page: number; pageSize: number }>
) => pagination.handlePageSizeChange(Number(e.detail.pageSize))}
page={pagination.page}
pageSize={pagination.pageSize}
pageSizes={PAGE_SIZES}
style={{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, can we move styles to a styled component?

Copy link
Contributor Author

@smans-akamai smans-akamai Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, we'll look into addressing the additional styled components separately.

borderLeft: `1px solid ${theme.tokens.alias.Border.Normal}`,
borderRight: `1px solid ${theme.tokens.alias.Border.Normal}`,
borderTop: 0,
marginTop: '0',
}}
/>
)}
</>
);
};
Loading