-
Notifications
You must be signed in to change notification settings - Fork 363
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: [M3-8719] - Add a 'Mask Sensitive Data' setting to Cloud Manager (
#11143) * Add boolean for redacted data to user preferences * Add RedactableText component * Apply RedactableText to Linode landing IP Addresses * Try a styling fix * Fix logic error and rename variables to be less confusing * Redact text on Account Login History tab and Profile Login & Auth tab * Update copy * Create and test util for masked string * Mask text in IPAddress component * Replace instances of RedactableText with MaskableText * Mask billing contact info * Mask user pages * Hide masking toggle icon for IPAddresses when setting off * Mask top menu * Clean up props * Revert top menu * Fix copy icon not copying plaintext value * Rename preference key for consistency * Clean up - typo, linting, comments * Unit test IPAddress component and fix issues * Rename MaskableTextTooltip to VisibilityTooltip * Add test for MaskableText * Add stories * Organize tooltip story with rest of tooltips * Mask fields in AccessTable * Mask IP addresses in Linode Details Network tab * Mask credit card and third party payment details * Fix doubly masked third party payment * Added changeset: Mask Sensitive Data preference to Profile Settings * Clean up of a variable name for consistency in naming * Fix Tooltip import * Update packages/manager/src/utilities/createMaskedText.ts Co-authored-by: Hana Xu <[email protected]> * Address feedback: clean up unnecessary prop; update test * Address feedback: add constant for unmaskedText * Address feedback: Replace displayText with masked prop in CopyTooltip * Add masked visibility toggle to CopyTooltip * Move VisibilityTooltip to ui package * Address feedback: misalignment of AccessTable row components * Fix circular dependency and update IPAddress unit test * Update unit test coverage * Address feedback: tooltip interactivity, placement, hover color * Mask Kube Details page * Mask Firewall rules and lint * Mask Service Transfers * Mask OBJ hostname and access keys * Mask Trusted Devices * Address feedback: update children prop, remove fragments * Remove problematic Storybook story * Address feedback: update MaskableText and util to use consistent lengths * Set the length of masked data fields * Fix console errors and test failures * Fix the unit test that escaped me * Add documentation of feature * Fix a markdown issue in the PR template --------- Co-authored-by: Hana Xu <[email protected]>
- Loading branch information
1 parent
2e6defb
commit 42f6cc2
Showing
41 changed files
with
865 additions
and
149 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@linode/manager": Added | ||
--- | ||
|
||
Mask Sensitive Data preference to Profile Settings ([#11143](https://github.com/linode/manager/pull/11143)) |
88 changes: 88 additions & 0 deletions
88
packages/manager/src/components/CopyTooltip/CopyTooltip.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import { act, waitFor } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import * as React from 'react'; | ||
|
||
import { renderWithTheme } from 'src/utilities/testHelpers'; | ||
|
||
import { CopyTooltip } from './CopyTooltip'; | ||
|
||
import type { CopyTooltipProps } from './CopyTooltip'; | ||
|
||
const mockText = 'Hello world'; | ||
|
||
const defaultProps: CopyTooltipProps = { | ||
text: mockText, | ||
}; | ||
|
||
describe('CopyTooltip', () => { | ||
it('should render the copy icon button and tooltip', async () => { | ||
const { getByLabelText, getByRole } = renderWithTheme( | ||
<CopyTooltip {...defaultProps} /> | ||
); | ||
|
||
const copyIconButton = getByLabelText(`Copy ${mockText} to clipboard`); | ||
|
||
await act(() => userEvent.hover(copyIconButton)); | ||
|
||
await waitFor(() => { | ||
const copyTooltip = getByRole('tooltip'); | ||
expect(copyTooltip).toBeInTheDocument(); | ||
expect(copyTooltip).toHaveTextContent('Copy'); | ||
}); | ||
|
||
await userEvent.click(copyIconButton); | ||
|
||
await waitFor(() => { | ||
const copiedTooltip = getByRole('tooltip', {}); | ||
expect(copiedTooltip).toBeInTheDocument(); | ||
expect(copiedTooltip).toHaveTextContent('Copied!'); | ||
}); | ||
}); | ||
|
||
it('should render text with the copyableText property', async () => { | ||
const { getByLabelText, getByText } = renderWithTheme( | ||
<CopyTooltip {...defaultProps} copyableText /> | ||
); | ||
|
||
expect(getByLabelText(`Copy ${mockText} to clipboard`)).toBeInTheDocument(); | ||
expect(getByText(mockText)).toBeVisible(); | ||
}); | ||
|
||
it('should disable the tooltip text with the disable property', async () => { | ||
const { getByLabelText } = renderWithTheme( | ||
<CopyTooltip {...defaultProps} disabled /> | ||
); | ||
|
||
const copyIconButton = getByLabelText(`Copy ${mockText} to clipboard`); | ||
expect(copyIconButton).toBeDisabled(); | ||
}); | ||
|
||
it('should mask and toggle visibility of tooltip text with the masked property', async () => { | ||
const { | ||
getByLabelText, | ||
getByTestId, | ||
getByText, | ||
queryByText, | ||
} = renderWithTheme( | ||
<CopyTooltip | ||
{...defaultProps} | ||
copyableText | ||
masked | ||
maskedTextLength="plaintext" | ||
/> | ||
); | ||
|
||
const copyIconButton = getByLabelText(`Copy ${mockText} to clipboard`); | ||
const visibilityToggle = getByTestId('VisibilityIcon'); | ||
|
||
// Text should be masked | ||
expect(copyIconButton).toBeInTheDocument(); | ||
expect(getByText('•••••••••••')).toBeVisible(); | ||
expect(queryByText(mockText)).toBeNull(); | ||
|
||
await act(() => userEvent.click(visibilityToggle)); | ||
|
||
// Text should be unmasked | ||
expect(getByText('Hello world')).toBeVisible(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
107 changes: 107 additions & 0 deletions
107
packages/manager/src/components/MaskableText/MaskableText.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import userEvent from '@testing-library/user-event'; | ||
import React from 'react'; | ||
|
||
import { renderWithTheme } from 'src/utilities/testHelpers'; | ||
|
||
import { MaskableText } from './MaskableText'; | ||
|
||
import type { MaskableTextProps } from './MaskableText'; | ||
import type { ManagerPreferences } from 'src/types/ManagerPreferences'; | ||
|
||
describe('MaskableText', () => { | ||
const maskedText = '•••••••••••'; | ||
const plainText = 'my-username'; | ||
|
||
const defaultProps: MaskableTextProps = { | ||
isToggleable: false, | ||
length: 'plaintext', | ||
text: plainText, | ||
}; | ||
|
||
const preferences: ManagerPreferences = { | ||
maskSensitiveData: true, | ||
}; | ||
|
||
const queryMocks = vi.hoisted(() => ({ | ||
usePreferences: vi.fn().mockReturnValue({}), | ||
})); | ||
|
||
vi.mock('src/queries/profile/preferences', async () => { | ||
const actual = await vi.importActual('src/queries/profile/preferences'); | ||
return { | ||
...actual, | ||
usePreferences: queryMocks.usePreferences, | ||
}; | ||
}); | ||
|
||
queryMocks.usePreferences.mockReturnValue({ | ||
data: preferences, | ||
}); | ||
|
||
it('should render masked text if the maskSensitiveData preference is enabled', () => { | ||
queryMocks.usePreferences.mockReturnValue({ | ||
data: preferences, | ||
}); | ||
|
||
const { getByText, queryByText } = renderWithTheme( | ||
<MaskableText {...defaultProps} /> | ||
); | ||
|
||
// Original text should be masked | ||
expect(getByText(maskedText)).toBeVisible(); | ||
expect(queryByText(plainText)).not.toBeInTheDocument(); | ||
}); | ||
|
||
it('should not render masked text if the maskSensitiveData preference is disabled', () => { | ||
queryMocks.usePreferences.mockReturnValue({ | ||
data: { | ||
maskSensitiveData: false, | ||
}, | ||
}); | ||
|
||
const { getByText, queryByText } = renderWithTheme( | ||
<MaskableText {...defaultProps} /> | ||
); | ||
|
||
// Original text should be visible | ||
expect(getByText(plainText)).toBeVisible(); | ||
expect(queryByText(maskedText)).not.toBeInTheDocument(); | ||
}); | ||
|
||
it("should render MaskableText's children if the maskSensitiveData preference is disabled and children are provided", () => { | ||
queryMocks.usePreferences.mockReturnValue({ | ||
data: { | ||
maskSensitiveData: false, | ||
}, | ||
}); | ||
|
||
const plainTextElement = <div>{plainText}</div>; | ||
const { getByText, queryByText } = renderWithTheme( | ||
<MaskableText {...defaultProps}>{plainTextElement}</MaskableText> | ||
); | ||
|
||
// Original text should be visible | ||
expect(getByText(plainText)).toBeInTheDocument(); | ||
expect(queryByText(maskedText)).not.toBeInTheDocument(); | ||
}); | ||
|
||
it('should render a toggleable VisibilityIcon tooltip if isToggleable is provided', async () => { | ||
queryMocks.usePreferences.mockReturnValue({ | ||
data: preferences, | ||
}); | ||
|
||
const { getByTestId, getByText } = renderWithTheme( | ||
<MaskableText {...defaultProps} isToggleable /> | ||
); | ||
|
||
const visibilityToggle = getByTestId('VisibilityIcon'); | ||
|
||
// Original text should be masked | ||
expect(getByText(maskedText)).toBeVisible(); | ||
|
||
await userEvent.click(visibilityToggle); | ||
|
||
// Original text should be unmasked | ||
expect(getByText(plainText)).toBeVisible(); | ||
}); | ||
}); |
Oops, something went wrong.