-
Notifications
You must be signed in to change notification settings - Fork 399
upcoming: [UIE-9380] - Service URI PG Bouncer Connection Details Section #13182
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
2f2606f
44df3a6
59a8a9e
2ec1e50
0871d02
0cff520
61759e5
bbf4887
3f8e5f8
ec6561f
bd2140a
378067e
73c6d88
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/manager": Upcoming Features | ||
| --- | ||
|
|
||
| Added PG Bouncer ServiceURI component ([#13182](https://github.com/linode/manager/pull/13182)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| import { screen } from '@testing-library/react'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import * as React from 'react'; | ||
| import { describe, it } from 'vitest'; | ||
|
|
||
| import { databaseFactory } from 'src/factories/databases'; | ||
| import { renderWithTheme } from 'src/utilities/testHelpers'; | ||
|
|
||
| import { ServiceURI } from './ServiceURI'; | ||
|
|
||
| const mockDatabase = databaseFactory.build({ | ||
| connection_pool_port: 100, | ||
| engine: 'postgresql', | ||
| id: 1, | ||
| platform: 'rdbms-default', | ||
| private_network: null, | ||
| }); | ||
|
|
||
| const mockCredentials = { | ||
| password: 'password123', | ||
|
Check warning on line 20 in packages/manager/src/features/Databases/DatabaseDetail/ServiceURI.test.tsx
|
||
| username: 'lnroot', | ||
| }; | ||
|
|
||
| // Hoist query mocks | ||
| const queryMocks = vi.hoisted(() => { | ||
| return { | ||
| useDatabaseCredentialsQuery: vi.fn(), | ||
| }; | ||
| }); | ||
|
|
||
| vi.mock('@linode/queries', async () => { | ||
| const actual = await vi.importActual('@linode/queries'); | ||
| return { | ||
| ...actual, | ||
| useDatabaseCredentialsQuery: queryMocks.useDatabaseCredentialsQuery, | ||
| }; | ||
| }); | ||
|
|
||
| describe('ServiceURI', () => { | ||
| it('should render the service URI component and copy icon', async () => { | ||
| queryMocks.useDatabaseCredentialsQuery.mockReturnValue({ | ||
| data: mockCredentials, | ||
| }); | ||
| const { container } = renderWithTheme( | ||
| <ServiceURI database={mockDatabase} /> | ||
| ); | ||
|
|
||
| const revealPasswordBtn = screen.getByRole('button', { | ||
| name: '{click to reveal password}', | ||
| }); | ||
| const serviceURIText = screen.getByTestId('service-uri').textContent; | ||
|
|
||
| expect(revealPasswordBtn).toBeInTheDocument(); | ||
| expect(serviceURIText).toBe( | ||
| `postgres://{click to reveal password}@db-mysql-primary-0.b.linodeb.net:{connection pool port}/{connection pool label}?sslmode=require` | ||
| ); | ||
|
|
||
| // eslint-disable-next-line testing-library/no-container | ||
| const copyButton = container.querySelector('[data-qa-copy-btn]'); | ||
|
Check warning on line 59 in packages/manager/src/features/Databases/DatabaseDetail/ServiceURI.test.tsx
|
||
| expect(copyButton).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should reveal password after clicking reveal button', async () => { | ||
| queryMocks.useDatabaseCredentialsQuery.mockReturnValue({ | ||
| data: mockCredentials, | ||
| refetch: vi.fn(), | ||
| }); | ||
| renderWithTheme(<ServiceURI database={mockDatabase} />); | ||
|
|
||
| const revealPasswordBtn = screen.getByRole('button', { | ||
| name: '{click to reveal password}', | ||
| }); | ||
| await userEvent.click(revealPasswordBtn); | ||
|
|
||
| const serviceURIText = screen.getByTestId('service-uri').textContent; | ||
| expect(revealPasswordBtn).not.toBeInTheDocument(); | ||
| expect(serviceURIText).toBe( | ||
| `postgres://lnroot:password123@db-mysql-primary-0.b.linodeb.net:{connection pool port}/{connection pool label}?sslmode=require` | ||
| ); | ||
| }); | ||
|
|
||
| it('should render error retry button if the credentials call fails', () => { | ||
| queryMocks.useDatabaseCredentialsQuery.mockReturnValue({ | ||
| error: new Error('Failed to fetch credentials'), | ||
| }); | ||
|
|
||
| renderWithTheme(<ServiceURI database={mockDatabase} />); | ||
|
|
||
| const errorRetryBtn = screen.getByRole('button', { | ||
| name: '{error. click to retry}', | ||
| }); | ||
| expect(errorRetryBtn).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| import { useDatabaseCredentialsQuery } from '@linode/queries'; | ||
| import { Button } from '@linode/ui'; | ||
| import { Grid, styled } from '@mui/material'; | ||
| import copy from 'copy-to-clipboard'; | ||
| import { enqueueSnackbar } from 'notistack'; | ||
| import React, { useState } from 'react'; | ||
|
|
||
| import { Code } from 'src/components/Code/Code'; | ||
| import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; | ||
| import { | ||
| StyledGridContainer, | ||
| StyledLabelTypography, | ||
| StyledValueGrid, | ||
| } from 'src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.style'; | ||
|
|
||
| import type { Database } from '@linode/api-v4'; | ||
|
|
||
| interface ServiceURIProps { | ||
| database: Database; | ||
| } | ||
|
|
||
| export const ServiceURI = (props: ServiceURIProps) => { | ||
| const { database } = props; | ||
|
|
||
| const [hidePassword, setHidePassword] = useState(true); | ||
| const [isCopying, setIsCopying] = useState(false); | ||
|
|
||
| const { | ||
| data: credentials, | ||
| error: credentialsError, | ||
| isLoading: credentialsLoading, | ||
| isFetching: credentialsFetching, | ||
| refetch: getDatabaseCredentials, | ||
| } = useDatabaseCredentialsQuery(database.engine, database.id, !hidePassword); | ||
|
|
||
| const handleCopy = async () => { | ||
| if (!credentials) { | ||
| try { | ||
| setIsCopying(true); | ||
| const { data } = await getDatabaseCredentials(); | ||
| if (data) { | ||
| // copy with username/password data | ||
| copy( | ||
| `postgres://${data?.username}:${data?.password}@${database.hosts?.primary}?sslmode=require` | ||
| ); | ||
| } else { | ||
| enqueueSnackbar( | ||
| 'There was an error retrieving cluster credentials. Please try again.', | ||
| { variant: 'error' } | ||
| ); | ||
| } | ||
| setIsCopying(false); | ||
| } catch { | ||
| setIsCopying(false); | ||
| enqueueSnackbar( | ||
| 'There was an error retrieving cluster credentials. Please try again.', | ||
| { variant: 'error' } | ||
| ); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| const serviceURI = `postgres://${credentials?.username}:${credentials?.password}@${database.hosts?.primary}?sslmode=require`; | ||
|
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. Is the assumption here that, after copying, they'll know to add the When they copy this, those will be missing and it looks like we're excluding the placeholders from the copy output for it. This looks like the intention, so I assume this behavior was already agreed upon.
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. ^ I have this question as well
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. Just added If the credentials call is successful, the pool port and label will be present in the copied string because we are copying again in https://github.com/hana-akamai/manager/blob/UIE-9380-service-uri-pg-bouncer/packages/manager/src/features/Databases/DatabaseDetail/ServiceURI.tsx#L43 |
||
|
|
||
| // hide loading state if the user clicks on the copy icon | ||
| const showBtnLoading = | ||
| !isCopying && (credentialsLoading || credentialsFetching); | ||
|
|
||
| return ( | ||
| <StyledGridContainer display="flex"> | ||
| <Grid | ||
| size={{ | ||
| md: 1.5, | ||
| xs: 3, | ||
| }} | ||
| > | ||
| <StyledLabelTypography>Service URI</StyledLabelTypography> | ||
| </Grid> | ||
| <Grid display="contents"> | ||
| <StyledValueGrid | ||
| data-testid="service-uri" | ||
| size="grow" | ||
| sx={{ overflowX: 'auto', overflowY: 'hidden' }} | ||
| whiteSpace="pre" | ||
| > | ||
| postgres:// | ||
| {credentialsError ? ( | ||
| <Button | ||
| loading={showBtnLoading} | ||
| onClick={() => getDatabaseCredentials()} | ||
| sx={(theme) => ({ | ||
| p: 0, | ||
| color: theme.tokens.alias.Content.Text.Negative, | ||
| '&:hover, &:focus': { | ||
| color: theme.tokens.alias.Content.Text.Negative, | ||
| }, | ||
| })} | ||
| > | ||
| {`{error. click to retry}`} | ||
| </Button> | ||
| ) : hidePassword || (!credentialsError && !credentials) ? ( | ||
| <Button | ||
| loading={showBtnLoading} | ||
| onClick={() => { | ||
| setHidePassword(false); | ||
| getDatabaseCredentials(); | ||
| }} | ||
| sx={{ p: 0 }} | ||
| > | ||
| {`{click to reveal password}`} | ||
| </Button> | ||
hana-akamai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) : ( | ||
| `${credentials?.username}:${credentials?.password}` | ||
| )} | ||
| @{database.hosts?.primary}: | ||
| <StyledCode>{'{connection pool port}'}</StyledCode>/ | ||
| <StyledCode>{'{connection pool label}'}</StyledCode>?sslmode=require | ||
| </StyledValueGrid> | ||
| {isCopying ? ( | ||
| <Button loading sx={{ paddingLeft: 2 }}> | ||
| {' '} | ||
| </Button> | ||
| ) : ( | ||
| <Grid alignContent="center" size="auto"> | ||
| <StyledCopyTooltip onClickCallback={handleCopy} text={serviceURI} /> | ||
| </Grid> | ||
| )} | ||
| </Grid> | ||
| </StyledGridContainer> | ||
| ); | ||
| }; | ||
|
|
||
| export const StyledCode = styled(Code, { | ||
| label: 'StyledCode', | ||
| })(() => ({ | ||
| margin: 0, | ||
| })); | ||
|
|
||
| export const StyledCopyTooltip = styled(CopyTooltip, { | ||
| label: 'StyledCopyTooltip', | ||
| })(({ theme }) => ({ | ||
| alignSelf: 'center', | ||
| '& svg': { | ||
| height: theme.spacingFunction(16), | ||
| width: theme.spacingFunction(16), | ||
| }, | ||
| '&:hover': { | ||
| backgroundColor: 'transparent', | ||
| }, | ||
| display: 'flex', | ||
| margin: `0 ${theme.spacingFunction(4)}`, | ||
| })); | ||
Uh oh!
There was an error while loading. Please reload this page.
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.
Since there will be a general case for the ServiceURI component outside of PgBouncer with UIE-9327, we should consider making this component reusable for both scenarios (Connection Pool URI and General Service URI)
Since we haven't confirmed the general format yet, this could be taken up as part of the next UIE ticket that adds to the Service URI to the Summary tab. But I just wanted to bring that up here for consideration.