-
Notifications
You must be signed in to change notification settings - Fork 399
upcoming: [UIE-9378] - DBaaS - Display connection pool section and table in Networking tab #13195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b4776f3
a66b053
c9abc3f
eb4c5cd
8fee513
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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)) |
| 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
|
||
| }, | ||
| { | ||
| onClick: () => null, // TODO: UIE-9430 Implement delete functionality | ||
|
Check warning on line 70 in packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseConnectionPools.tsx
|
||
| title: 'Delete', | ||
| }, | ||
| ]; | ||
|
|
||
| if (connectionPoolsLoading) { | ||
| return <CircleProgress />; | ||
| } | ||
|
Comment on lines
+75
to
+77
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bnussman-akamai So the 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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={ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'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={{ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, can we move styles to a styled component?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', | ||
| }} | ||
| /> | ||
| )} | ||
| </> | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
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
pageSizeandpageNumberto the request.