Skip to content

Commit ca83906

Browse files
authored
fix: [UIE-10522] - Disable credential buttons for resuming state (#13505)
Disable buttons to get credentials when the Database cluster is resuming
1 parent 2cbecd4 commit ca83906

11 files changed

Lines changed: 88 additions & 46 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/api-v4": Removed
3+
---
4+
5+
Old `failed` property from `DatabaseStatus` ([#13505](https://github.com/linode/manager/pull/13505))

packages/api-v4/src/databases/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ export interface DatabaseEngine {
3232
export type DatabaseStatus =
3333
| 'active'
3434
| 'degraded'
35-
| 'failed'
3635
| 'migrated'
3736
| 'migrating'
3837
| 'provisioning'
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Fixed
3+
---
4+
5+
Disable Database credential buttons for resuming state ([#13505](https://github.com/linode/manager/pull/13505))

packages/manager/src/components/CopyTooltip/CopyTooltip.test.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('CopyTooltip', () => {
4444
expect(getByText(mockText)).toBeVisible();
4545
});
4646

47-
it('should disable the tooltip text with the disable property', async () => {
47+
it('should disable the tooltip with the disable property', async () => {
4848
const { getByLabelText } = renderWithTheme(
4949
<CopyTooltip {...defaultProps} disabled />
5050
);
@@ -53,6 +53,23 @@ describe('CopyTooltip', () => {
5353
expect(copyIconButton).toBeDisabled();
5454
});
5555

56+
it('should display tooltip reason with the disabledReason property', async () => {
57+
const { getByLabelText, findByRole } = renderWithTheme(
58+
<CopyTooltip
59+
{...defaultProps}
60+
disabled
61+
disabledReason="Tooltip disabled"
62+
/>
63+
);
64+
65+
const copyIconButton = getByLabelText(`Copy ${mockText} to clipboard`);
66+
67+
await userEvent.hover(copyIconButton);
68+
const copiedTooltip = await findByRole('tooltip');
69+
expect(copiedTooltip).toBeInTheDocument();
70+
expect(copiedTooltip).toHaveTextContent('Tooltip disabled');
71+
});
72+
5673
it('should mask and toggle visibility of tooltip text with the masked property', async () => {
5774
const { getByLabelText, getByTestId, getByText, queryByText } =
5875
renderWithTheme(

packages/manager/src/components/CopyTooltip/CopyTooltip.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export interface CopyTooltipProps {
2424
* @default false
2525
*/
2626
disabled?: boolean;
27+
/**
28+
* Optionally display disabled reason as tooltip
29+
*/
30+
disabledReason?: string;
2731
/**
2832
* If true, the component is in controlled mode for text masking, meaning the parent component handles the visibility toggle.
2933
* @default false
@@ -63,6 +67,7 @@ export const CopyTooltip = (props: CopyTooltipProps) => {
6367
className,
6468
copyableText,
6569
disabled,
70+
disabledReason,
6671
isMaskingControlled,
6772
masked,
6873
maskedTextLength,
@@ -105,6 +110,20 @@ export const CopyTooltip = (props: CopyTooltipProps) => {
105110
</StyledIconButton>
106111
);
107112

113+
if (disabled && disabledReason) {
114+
return (
115+
<Tooltip
116+
className="copy-tooltip"
117+
data-qa-copied
118+
disableInteractive
119+
placement={placement ?? 'top'}
120+
title={disabledReason}
121+
>
122+
<span>{CopyButton}</span>
123+
</Tooltip>
124+
);
125+
}
126+
108127
if (disabled) {
109128
return CopyButton;
110129
}
@@ -134,6 +153,7 @@ export const StyledIconButton = styled('button', {
134153
label: 'StyledIconButton',
135154
shouldForwardProp: omittedProps([
136155
'copyableText',
156+
'disabledReason',
137157
'text',
138158
'onClickCallback',
139159
'masked',

packages/manager/src/factories/databases.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { Factory } from '@linode/utilities';
1717
export const possibleStatuses: DatabaseStatus[] = [
1818
'active',
1919
'degraded',
20-
'failed',
2120
'migrating',
2221
'migrated',
2322
'provisioning',

packages/manager/src/features/Databases/DatabaseDetail/DatabaseStatusDisplay.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import type { Status } from 'src/components/StatusIcon/StatusIcon';
1515
export const databaseStatusMap: Record<DatabaseStatus, Status> = {
1616
active: 'active',
1717
degraded: 'inactive',
18-
failed: 'error',
1918
migrated: 'inactive',
2019
migrating: 'other',
2120
provisioning: 'other',

packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { DB_ROOT_USERNAME } from 'src/constants';
1010
import {
1111
CLUSTER_PROVISIONING_TEXT,
1212
CREDENTIALS_ERROR_TEXT,
13+
DISABLE_CREDENTIAL_STATES,
1314
DISABLED_PASSWORD_BUTTON_TEXT,
1415
} from 'src/features/Databases/constants';
1516
import { useFlags } from 'src/hooks/useFlags';
@@ -74,9 +75,7 @@ export const DatabaseSummaryConnectionDetails = (props: Props) => {
7475
}
7576
}, [showCredentials, credentialsError]);
7677

77-
const disableShowBtn = ['failed', 'provisioning', 'suspended'].includes(
78-
database.status
79-
);
78+
const disableShowBtn = DISABLE_CREDENTIAL_STATES.includes(database.status);
8079

8180
const credentialsBtn = (handleClick: () => void, btnText: string) => {
8281
return (

packages/manager/src/features/Databases/DatabaseDetail/ServiceURI.test.tsx

Lines changed: 26 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { renderWithTheme } from 'src/utilities/testHelpers';
88

99
import { ServiceURI } from './ServiceURI';
1010

11+
import type { DatabaseStatus } from '@linode/api-v4';
12+
1113
const mockCredentials = {
1214
password: 'password123',
1315
username: 'lnroot',
@@ -163,11 +165,12 @@ vi.mock('@linode/queries', async () => {
163165
});
164166

165167
describe('ServiceURI', () => {
166-
it('should render the service URI component and copy icon', async () => {
167-
queryMocks.useDatabaseCredentialsQuery.mockReturnValue({
168-
data: mockCredentials,
169-
});
168+
queryMocks.useDatabaseCredentialsQuery.mockReturnValue({
169+
data: mockCredentials,
170+
refetch: vi.fn(),
171+
});
170172

173+
it('should render the service URI component and copy icon', async () => {
171174
const { container } = renderWithTheme(
172175
<ServiceURI database={databaseWithNoVPC} />
173176
);
@@ -188,11 +191,6 @@ describe('ServiceURI', () => {
188191
});
189192

190193
it('should reveal password after clicking reveal button', async () => {
191-
queryMocks.useDatabaseCredentialsQuery.mockReturnValue({
192-
data: mockCredentials,
193-
refetch: vi.fn(),
194-
});
195-
196194
renderWithTheme(<ServiceURI database={databaseWithNoVPC} />);
197195

198196
const revealPasswordBtn = screen.getByRole('button', {
@@ -208,10 +206,6 @@ describe('ServiceURI', () => {
208206
});
209207

210208
it('should render general service URI if isGeneralServiceURI is true', () => {
211-
queryMocks.useDatabaseCredentialsQuery.mockReturnValue({
212-
data: mockCredentials,
213-
});
214-
215209
renderWithTheme(
216210
<ServiceURI database={databaseWithNoVPC} isGeneralServiceURI />
217211
);
@@ -228,10 +222,6 @@ describe('ServiceURI', () => {
228222
});
229223

230224
it('should reveal general service URI password after clicking reveal button', async () => {
231-
queryMocks.useDatabaseCredentialsQuery.mockReturnValue({
232-
data: mockCredentials,
233-
refetch: vi.fn(),
234-
});
235225
renderWithTheme(
236226
<ServiceURI database={databaseWithNoVPC} isGeneralServiceURI />
237227
);
@@ -249,10 +239,6 @@ describe('ServiceURI', () => {
249239
});
250240

251241
it('should render private service URI component if there is a private-only VPC', async () => {
252-
queryMocks.useDatabaseCredentialsQuery.mockReturnValue({
253-
data: mockCredentials,
254-
});
255-
256242
renderWithTheme(<ServiceURI database={databaseWithPrivateVPC} />);
257243

258244
const revealPasswordBtn = screen.getByRole('button', {
@@ -267,10 +253,6 @@ describe('ServiceURI', () => {
267253
});
268254

269255
it('should render private general service URI component if there is a private-only VPC', async () => {
270-
queryMocks.useDatabaseCredentialsQuery.mockReturnValue({
271-
data: mockCredentials,
272-
});
273-
274256
renderWithTheme(
275257
<ServiceURI database={databaseWithPrivateVPC} isGeneralServiceURI />
276258
);
@@ -287,10 +269,6 @@ describe('ServiceURI', () => {
287269
});
288270

289271
it('should render public service URI component if there is a VPC with public access', async () => {
290-
queryMocks.useDatabaseCredentialsQuery.mockReturnValue({
291-
data: mockCredentials,
292-
});
293-
294272
renderWithTheme(<ServiceURI database={databaseWithPublicVPC} />);
295273

296274
const revealPasswordBtn = screen.getByRole('button', {
@@ -305,10 +283,6 @@ describe('ServiceURI', () => {
305283
});
306284

307285
it('should render private service URI component if there is a VPC with public access and showPrivateVPC is true', async () => {
308-
queryMocks.useDatabaseCredentialsQuery.mockReturnValue({
309-
data: mockCredentials,
310-
});
311-
312286
renderWithTheme(
313287
<ServiceURI database={databaseWithPublicVPC} showPrivateVPC />
314288
);
@@ -325,10 +299,6 @@ describe('ServiceURI', () => {
325299
});
326300

327301
it('should render general private service URI if there is a VPC with public access, isGeneralServiceURI is true, and showPrivateVPC is true', () => {
328-
queryMocks.useDatabaseCredentialsQuery.mockReturnValue({
329-
data: mockCredentials,
330-
});
331-
332302
renderWithTheme(
333303
<ServiceURI
334304
database={databaseWithPublicVPC}
@@ -347,4 +317,23 @@ describe('ServiceURI', () => {
347317
`postgres://{click to reveal password}@${PRIVATE_PRIMARY}:3306/defaultdb?sslmode=require`
348318
);
349319
});
320+
321+
it('should disable the reveal password and copy icon if the Database is suspended', async () => {
322+
const mockDatabase = {
323+
...databaseWithNoVPC,
324+
status: 'suspended' as DatabaseStatus,
325+
};
326+
327+
const { container } = renderWithTheme(
328+
<ServiceURI database={mockDatabase} />
329+
);
330+
331+
const revealPasswordBtn = screen.getByRole('button', {
332+
name: '{click to reveal password}',
333+
});
334+
// eslint-disable-next-line testing-library/no-container
335+
const copyButton = container.querySelector('[data-qa-copy-btn]');
336+
expect(revealPasswordBtn).toBeDisabled();
337+
expect(copyButton).toBeDisabled();
338+
});
350339
});

packages/manager/src/features/Databases/DatabaseDetail/ServiceURI.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip';
1010
import {
1111
CLUSTER_PROVISIONING_TEXT,
1212
CREDENTIALS_ERROR_TEXT,
13+
DISABLE_CREDENTIAL_STATES,
1314
DISABLED_PASSWORD_BUTTON_TEXT,
1415
} from 'src/features/Databases/constants';
1516
import { StyledValueGrid } from 'src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.style';
@@ -101,9 +102,10 @@ export const ServiceURI = (props: ServiceURIProps) => {
101102
const showBtnLoading =
102103
!hidePassword && !isCopying && (credentialsLoading || credentialsFetching);
103104

104-
const disablePasswordBtn = ['failed', 'provisioning', 'suspended'].includes(
105+
const disablePasswordBtn = DISABLE_CREDENTIAL_STATES.includes(
105106
database.status
106107
);
108+
107109
const disabledPasswordTooltipText =
108110
database.status === 'provisioning'
109111
? CLUSTER_PROVISIONING_TEXT
@@ -177,6 +179,8 @@ export const ServiceURI = (props: ServiceURIProps) => {
177179
) : (
178180
<Grid alignContent="center" size="auto">
179181
<StyledCopyTooltip
182+
disabled={disablePasswordBtn}
183+
disabledReason={disabledPasswordTooltipText}
180184
onClickCallback={handleCopy}
181185
text={getServiceURIText(credentials, isGeneralServiceURI)}
182186
/>

0 commit comments

Comments
 (0)