From 5d45f8c1a9f30bdb419d01a804bc3f9b90211b1b Mon Sep 17 00:00:00 2001 From: Anastasiia Alekseenko Date: Fri, 27 Mar 2026 15:20:35 +0100 Subject: [PATCH] feat: [UIE-10535] - IAM: MaskableText component --- .../ImageSelect/ImageSelectTable.tsx | 4 +- .../ImageSelect/ImageSelectTableRow.tsx | 2 +- .../SwitchAccounts/ChildAccountsTable.tsx | 4 +- .../DatabaseCreate/DatabaseCreate.style.ts | 2 +- .../DatabaseDetail/AccessControls.tsx | 2 +- .../DatabaseAdvancedConfiguration.tsx | 2 +- .../DatabaseAdvancedConfigurationDrawer.tsx | 2 +- .../DatabaseConfigurationItem.tsx | 2 +- .../DatabaseConnectionPoolRow.tsx | 2 +- .../DatabaseConnectionPools.tsx | 4 +- .../DatabaseResize/DatabaseResize.style.ts | 2 +- .../DatabaseSettingsMaintenance.tsx | 2 +- .../DatabaseSettingsMenuItem.tsx | 2 +- .../DatabaseSettings/MaintenanceWindow.tsx | 3 +- .../DatabaseSummary/DatabaseCaCert.tsx | 2 +- .../DatabaseSummaryConnectionDetails.tsx | 2 +- .../DatabaseLanding/DatabaseLandingTable.tsx | 4 +- .../Databases/DatabaseLanding/DatabaseRow.tsx | 2 +- .../src/features/Databases/shared.styles.ts | 2 +- .../IAM/Roles/RolesTable/RolesTable.tsx | 6 +- .../MaskableText/MaskableText.styles.ts | 32 ++++++ .../Shared/MaskableText/MaskableText.test.tsx | 105 ++++++++++++++++++ .../IAM/Shared/MaskableText/MaskableText.tsx | 104 +++++++++++++++++ .../Users/UserDetails/UserDetailsPanel.tsx | 2 +- .../features/IAM/Users/UsersTable/UserRow.tsx | 2 +- 25 files changed, 270 insertions(+), 28 deletions(-) create mode 100644 packages/manager/src/features/IAM/Shared/MaskableText/MaskableText.styles.ts create mode 100644 packages/manager/src/features/IAM/Shared/MaskableText/MaskableText.test.tsx create mode 100644 packages/manager/src/features/IAM/Shared/MaskableText/MaskableText.tsx diff --git a/packages/manager/src/components/ImageSelect/ImageSelectTable.tsx b/packages/manager/src/components/ImageSelect/ImageSelectTable.tsx index 46c5456320c..e2296485ece 100644 --- a/packages/manager/src/components/ImageSelect/ImageSelectTable.tsx +++ b/packages/manager/src/components/ImageSelect/ImageSelectTable.tsx @@ -17,7 +17,7 @@ import { useTheme, } from '@linode/ui'; import useMediaQuery from '@mui/material/useMediaQuery'; -import { Pagination } from 'akamai-cds-react-components/Pagination'; +import { Pagination } from '@akamai/cds-components/react/Pagination'; import { Table, TableBody, @@ -25,7 +25,7 @@ import { TableHead, TableHeaderCell, TableRow, -} from 'akamai-cds-react-components/Table'; +} from '@akamai/cds-components/react/Table'; import React, { useState } from 'react'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; diff --git a/packages/manager/src/components/ImageSelect/ImageSelectTableRow.tsx b/packages/manager/src/components/ImageSelect/ImageSelectTableRow.tsx index 98dd251e9a4..199e6dbb2f9 100644 --- a/packages/manager/src/components/ImageSelect/ImageSelectTableRow.tsx +++ b/packages/manager/src/components/ImageSelect/ImageSelectTableRow.tsx @@ -7,7 +7,7 @@ import { } from '@linode/ui'; import { convertStorageUnit, pluralize } from '@linode/utilities'; import useMediaQuery from '@mui/material/useMediaQuery'; -import { TableCell, TableRow } from 'akamai-cds-react-components/Table'; +import { TableCell, TableRow } from '@akamai/cds-components/react/Table'; import React from 'react'; import CloudInitIcon from 'src/assets/icons/cloud-init.svg'; diff --git a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx index 524960b2cb0..6ccad3cc06f 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx +++ b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx @@ -1,11 +1,11 @@ import { Box, CircleProgress, LinkButton, useTheme } from '@linode/ui'; -import { Pagination } from 'akamai-cds-react-components/Pagination'; +import { Pagination } from '@akamai/cds-components/react/Pagination'; import { Table, TableBody, TableCell, TableRow, -} from 'akamai-cds-react-components/Table'; +} from '@akamai/cds-components/react/Table'; import React from 'react'; import { MIN_PAGE_SIZE } from 'src/components/PaginationFooter/PaginationFooter.constants'; diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.style.ts b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.style.ts index a19ea764dff..25ca9d71bda 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.style.ts +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.style.ts @@ -1,6 +1,6 @@ import { Box, TextField, Typography } from '@linode/ui'; import { Grid, styled } from '@mui/material'; -import { Button } from 'akamai-cds-react-components'; +import { Button } from '@akamai/cds-components/react/Button'; import { PlansPanel } from 'src/features/components/PlansPanel/PlansPanel'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/AccessControls.tsx b/packages/manager/src/features/Databases/DatabaseDetail/AccessControls.tsx index be62f9b5cb5..d17087da33c 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/AccessControls.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/AccessControls.tsx @@ -1,6 +1,6 @@ import { useDatabaseMutation } from '@linode/queries'; import { ActionsPanel, Notice, Typography } from '@linode/ui'; -import { Button } from 'akamai-cds-react-components'; +import { Button } from '@akamai/cds-components/react/Button'; import * as React from 'react'; import type { JSX } from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfiguration.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfiguration.tsx index e7d54305105..8e40196e9b2 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfiguration.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfiguration.tsx @@ -1,7 +1,7 @@ import { Box, Paper, Typography } from '@linode/ui'; import Grid from '@mui/material/Grid'; import { useNavigate } from '@tanstack/react-router'; -import { Button } from 'akamai-cds-react-components'; +import { Button } from '@akamai/cds-components/react/Button'; import React from 'react'; import { Link } from 'src/components/Link'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfigurationDrawer.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfigurationDrawer.tsx index 033206afd43..cb245f9e70a 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfigurationDrawer.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfigurationDrawer.tsx @@ -12,7 +12,7 @@ import { import { scrollErrorIntoViewV2 } from '@linode/utilities'; import { createDynamicAdvancedConfigSchema } from '@linode/validation'; import Grid from '@mui/material/Grid'; -import { Button } from 'akamai-cds-react-components'; +import { Button } from '@akamai/cds-components/react/Button'; import { enqueueSnackbar } from 'notistack'; import React, { useEffect, useMemo, useState } from 'react'; import { Controller, get, useFieldArray, useForm } from 'react-hook-form'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.tsx index 9cb39fa948b..aadfb04d0ad 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.tsx @@ -6,7 +6,7 @@ import { Toggle, Typography, } from '@linode/ui'; -import { Button } from 'akamai-cds-react-components'; +import { Button } from '@akamai/cds-components/react/Button'; import React from 'react'; import { diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseConnectionPoolRow.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseConnectionPoolRow.tsx index a8fe2fe0c43..39e72ae2392 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseConnectionPoolRow.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseConnectionPoolRow.tsx @@ -1,5 +1,5 @@ import { Hidden } from '@linode/ui'; -import { TableCell, TableRow } from 'akamai-cds-react-components/Table'; +import { TableCell, TableRow } from '@akamai/cds-components/react/Table'; import * as React from 'react'; import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseConnectionPools.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseConnectionPools.tsx index b6664b2cc9f..4eee774475b 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseConnectionPools.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseConnectionPools.tsx @@ -9,7 +9,7 @@ import { } from '@linode/ui'; import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; -import { Pagination } from 'akamai-cds-react-components/Pagination'; +import { Pagination } from '@akamai/cds-components/react/Pagination'; import { Table, TableBody, @@ -17,7 +17,7 @@ import { TableHead, TableHeaderCell, TableRow, -} from 'akamai-cds-react-components/Table'; +} from '@akamai/cds-components/react/Table'; import React from 'react'; import { Link } from 'src/components/Link'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.style.ts b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.style.ts index d30b3c71db5..ad229b31d20 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.style.ts +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.style.ts @@ -1,6 +1,6 @@ import Grid from '@mui/material/Grid'; import { styled } from '@mui/material/styles'; -import { Button } from 'akamai-cds-react-components'; +import { Button } from '@akamai/cds-components/react/Button'; import { PlansPanel } from 'src/features/components/PlansPanel/PlansPanel'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMaintenance.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMaintenance.tsx index 8d76a33f127..6179a499445 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMaintenance.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMaintenance.tsx @@ -1,7 +1,7 @@ import { useDatabaseEnginesQuery } from '@linode/queries'; import { TooltipIcon, Typography } from '@linode/ui'; import { GridLegacy, styled } from '@mui/material'; -import { Button } from 'akamai-cds-react-components'; +import { Button } from '@akamai/cds-components/react/Button'; import * as React from 'react'; import { diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMenuItem.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMenuItem.tsx index 9d285dc4a32..6801bb47762 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMenuItem.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsMenuItem.tsx @@ -1,5 +1,5 @@ import { Typography } from '@linode/ui'; -import { Button } from 'akamai-cds-react-components'; +import { Button } from '@akamai/cds-components/react/Button'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/MaintenanceWindow.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/MaintenanceWindow.tsx index 9b7048eb6dc..8b0a578ecdf 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/MaintenanceWindow.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/MaintenanceWindow.tsx @@ -15,7 +15,8 @@ import { } from '@linode/ui'; import { updateMaintenanceSchema } from '@linode/validation'; import { styled } from '@mui/material/styles'; -import { Button, Select } from 'akamai-cds-react-components'; +import { Button } from '@akamai/cds-components/react/Button'; +import { Select } from '@akamai/cds-components/react/Select'; import { DateTime } from 'luxon'; import { useSnackbar } from 'notistack'; import * as React from 'react'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseCaCert.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseCaCert.tsx index 9d8e840467c..dbcb8b8161c 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseCaCert.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseCaCert.tsx @@ -2,7 +2,7 @@ import { getSSLFields } from '@linode/api-v4/lib/databases/databases'; import { TooltipIcon } from '@linode/ui'; import { downloadFile } from '@linode/utilities'; import { styled } from '@mui/material/styles'; -import { Button } from 'akamai-cds-react-components'; +import { Button } from '@akamai/cds-components/react/Button'; import { useSnackbar } from 'notistack'; import * as React from 'react'; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx index 94f6f00fb0a..79f8ee0b30a 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails.tsx @@ -1,6 +1,6 @@ import { useDatabaseCredentialsQuery } from '@linode/queries'; import { Box, CircleProgress, TooltipIcon, Typography } from '@linode/ui'; -import { Button } from 'akamai-cds-react-components'; +import { Button } from '@akamai/cds-components/react/Button'; import { enqueueSnackbar } from 'notistack'; import * as React from 'react'; diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingTable.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingTable.tsx index 3eaeba8e5e9..938d366b995 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingTable.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingTable.tsx @@ -1,13 +1,13 @@ import { Hidden } from '@linode/ui'; import { useTheme } from '@mui/material/styles'; -import { Pagination } from 'akamai-cds-react-components/Pagination'; +import { Pagination } from '@akamai/cds-components/react/Pagination'; import { Table, TableBody, TableHead, TableHeaderCell, TableRow, -} from 'akamai-cds-react-components/Table'; +} from '@akamai/cds-components/react/Table'; import React from 'react'; import { MIN_PAGE_SIZE } from 'src/components/PaginationFooter/PaginationFooter.constants'; diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseRow.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseRow.tsx index decaf48b49a..9734c4219e0 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseRow.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseRow.tsx @@ -5,7 +5,7 @@ import { } from '@linode/queries'; import { Chip, Hidden } from '@linode/ui'; import { formatStorageUnits } from '@linode/utilities'; -import { TableCell, TableRow } from 'akamai-cds-react-components/Table'; +import { TableCell, TableRow } from '@akamai/cds-components/react/Table'; import * as React from 'react'; import { Link } from 'src/components/Link'; diff --git a/packages/manager/src/features/Databases/shared.styles.ts b/packages/manager/src/features/Databases/shared.styles.ts index fe33a83bd58..596e4d83f99 100644 --- a/packages/manager/src/features/Databases/shared.styles.ts +++ b/packages/manager/src/features/Databases/shared.styles.ts @@ -1,5 +1,5 @@ import { styled } from '@linode/ui'; -import { TableCell } from 'akamai-cds-react-components/Table'; +import { TableCell } from '@akamai/cds-components/react/Table'; import { makeStyles } from 'tss-react/mui'; import type { Theme } from '@mui/material'; diff --git a/packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx b/packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx index d16de2aa1a9..301c2b89c65 100644 --- a/packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx +++ b/packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx @@ -4,7 +4,7 @@ import { useTheme } from '@mui/material'; import Grid from '@mui/material/Grid'; import Paper from '@mui/material/Paper'; import { useLocation, useNavigate, useSearch } from '@tanstack/react-router'; -import { Pagination } from 'akamai-cds-react-components/Pagination'; +import { Pagination } from '@akamai/cds-components/react/Pagination'; import { sortRows, Table, @@ -14,7 +14,7 @@ import { TableHeaderCell, TableRow, TableRowExpanded, -} from 'akamai-cds-react-components/Table'; +} from '@akamai/cds-components/react/Table'; import React, { useState } from 'react'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; @@ -38,7 +38,7 @@ import { import type { RoleView } from '../../Shared/types'; import type { SelectOption } from '@linode/ui'; -import type { Order } from 'akamai-cds-react-components/Table'; +import type { Order } from '@akamai/cds-components/react/Table'; const ALL_ROLES_OPTION: SelectOption = { label: 'All Roles', diff --git a/packages/manager/src/features/IAM/Shared/MaskableText/MaskableText.styles.ts b/packages/manager/src/features/IAM/Shared/MaskableText/MaskableText.styles.ts new file mode 100644 index 00000000000..5d4960c3e1f --- /dev/null +++ b/packages/manager/src/features/IAM/Shared/MaskableText/MaskableText.styles.ts @@ -0,0 +1,32 @@ +import { Button } from '@akamai/cds-components/react/Button'; +import { Icon } from '@akamai/cds-components/react/Icon'; +import { styled } from '@mui/material/styles'; + +export const StyledWrapper = styled('div', { + label: 'StyledWrapper', +})(() => ({ + alignItems: 'center', + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + height: '20px', +})); + +export const StyledToggleButton = styled(Button, { + label: 'StyledToggleButton', +})(({ theme }) => ({ + marginLeft: theme.tokens.spacing.S8, + minHeight: 'auto', + minWidth: 'auto', + padding: 0, + display: 'flex', +})); + +export const StyledIcon = styled(Icon, { + label: 'StyledIcon', +})(({ theme }) => ({ + color: theme.palette.grey[500], + '&:hover': { + color: theme.palette.primary.main, + }, +})); diff --git a/packages/manager/src/features/IAM/Shared/MaskableText/MaskableText.test.tsx b/packages/manager/src/features/IAM/Shared/MaskableText/MaskableText.test.tsx new file mode 100644 index 00000000000..e11181dcb16 --- /dev/null +++ b/packages/manager/src/features/IAM/Shared/MaskableText/MaskableText.test.tsx @@ -0,0 +1,105 @@ +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { MaskableText } from './MaskableText'; + +const SECRET_TEXT = 'text-to-be-masked'; +const TOGGLE_TEST_ID = 'maskable-text-toggle'; + +const queryMocks = vi.hoisted(() => ({ + usePreferences: vi.fn(), +})); + +vi.mock('@linode/queries', async () => { + const actual = await vi.importActual('@linode/queries'); + return { ...actual, usePreferences: queryMocks.usePreferences }; +}); + +describe('MaskableText', () => { + describe('when masking is disabled', () => { + beforeEach(() => { + queryMocks.usePreferences.mockReturnValue({ data: false }); + }); + + it('renders the plain text unmasked', () => { + renderWithTheme(); + expect(screen.getByText(SECRET_TEXT)).toBeVisible(); + }); + + it('renders children instead of text when provided', () => { + renderWithTheme( + + custom child + + ); + expect(screen.getByText('custom child')).toBeVisible(); + expect(screen.queryByText(SECRET_TEXT)).not.toBeInTheDocument(); + }); + + it('renders nothing when text is empty', () => { + const { container } = renderWithTheme(); + expect(container).toBeEmptyDOMElement(); + }); + + it('does not render the toggle button', () => { + renderWithTheme(); + expect(screen.queryByTestId(TOGGLE_TEST_ID)).not.toBeInTheDocument(); + }); + }); + + describe('when masking is enabled', () => { + beforeEach(() => { + queryMocks.usePreferences.mockReturnValue({ data: true }); + }); + + it('renders masked dots instead of plain text by default', () => { + renderWithTheme(); + expect(screen.queryByText('secret-value')).not.toBeInTheDocument(); + expect(screen.getByText('•'.repeat(12))).toBeVisible(); + }); + + it('renders masked dots with custom length', () => { + renderWithTheme(); + expect(screen.getByText('•'.repeat(6))).toBeVisible(); + }); + + it('renders nothing when text is empty', () => { + const { container } = renderWithTheme(); + expect(container).toBeEmptyDOMElement(); + }); + + describe('toggle behavior', () => { + it('does not render toggle button when isToggleable is false', () => { + renderWithTheme(); + expect(screen.queryByTestId(TOGGLE_TEST_ID)).not.toBeInTheDocument(); + }); + + it('renders toggle button when isToggleable is true', () => { + renderWithTheme(); + screen.getByTestId(TOGGLE_TEST_ID); + }); + + it('reveals text when toggle button is clicked', async () => { + renderWithTheme(); + expect(screen.queryByText(SECRET_TEXT)).not.toBeInTheDocument(); + + await userEvent.click(screen.getByTestId(TOGGLE_TEST_ID)); + + expect(screen.getByText(SECRET_TEXT)).toBeVisible(); + }); + + it('masks text again when toggle button is clicked twice', async () => { + renderWithTheme(); + + await userEvent.click(screen.getByTestId(TOGGLE_TEST_ID)); + await userEvent.click(screen.getByTestId(TOGGLE_TEST_ID)); + + expect(screen.queryByText(SECRET_TEXT)).not.toBeInTheDocument(); + expect(screen.getByText('•'.repeat(12))).toBeVisible(); + }); + }); + }); +}); diff --git a/packages/manager/src/features/IAM/Shared/MaskableText/MaskableText.tsx b/packages/manager/src/features/IAM/Shared/MaskableText/MaskableText.tsx new file mode 100644 index 00000000000..d1668045483 --- /dev/null +++ b/packages/manager/src/features/IAM/Shared/MaskableText/MaskableText.tsx @@ -0,0 +1,104 @@ +import { Tooltip } from '@akamai/cds-components/react/Tooltip'; +import { usePreferences } from '@linode/queries'; +import * as React from 'react'; +import type { JSX } from 'react'; + +import { + StyledIcon, + StyledToggleButton, + StyledWrapper, +} from './MaskableText.styles'; + +const DEFAULT_MASKED_TEXT_LENGTH = 12; + +export interface MaskableTextProps { + /** + * (Optional) original JSX element to render if the text is not masked. + */ + children?: JSX.Element | JSX.Element[]; + /** + * If true, displays a VisibilityTooltip icon to toggle the masked and unmasked text. + * @default false + */ + isToggleable?: boolean; + /** + * Optionally specifies the length of the masked text; if not provided, will use a default length. + */ + length?: number; + /** + * Optional styling for the masked and unmasked Typography + */ + styleTypography?: React.CSSProperties; + /** + * The original, maskable content; can be a string or any JSX/ReactNode. + * If the text is not masked, render this text or the styled text via children. + */ + text: React.ReactNode | string | undefined; +} + +export const MaskableText = (props: MaskableTextProps) => { + const { + children, + isToggleable = false, + length, + styleTypography, + text, + } = props; + + const { data: maskedPreferenceSetting } = usePreferences( + (preferences) => preferences?.maskSensitiveData + ); + + const [isMasked, setIsMasked] = React.useState(maskedPreferenceSetting); + + const unmaskedText = + children ?? + (typeof text === 'string' ? ( +

{text}

+ ) : ( + text // JSX (ReactNode) + )); + + // Return early based on the preference setting and the original text. + + if (!text) { + return; + } + + if (!maskedPreferenceSetting) { + return unmaskedText; + } + + const maskedText = '•'.repeat(length ?? DEFAULT_MASKED_TEXT_LENGTH); + + return ( + + {isMasked ? ( +

+ {maskedText} +

+ ) : ( + unmaskedText + )} + {isToggleable && ( + + setIsMasked(!isMasked)} + variant="icon" + > + {isMasked ? ( + + ) : ( + + )} + + + )} +
+ ); +}; diff --git a/packages/manager/src/features/IAM/Users/UserDetails/UserDetailsPanel.tsx b/packages/manager/src/features/IAM/Users/UserDetails/UserDetailsPanel.tsx index dafb504a4b3..7c2b7415994 100644 --- a/packages/manager/src/features/IAM/Users/UserDetails/UserDetailsPanel.tsx +++ b/packages/manager/src/features/IAM/Users/UserDetails/UserDetailsPanel.tsx @@ -3,10 +3,10 @@ import Grid from '@mui/material/Grid'; import React from 'react'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; -import { MaskableText } from 'src/components/MaskableText/MaskableText'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; import { TextTooltip } from 'src/components/TextTooltip'; +import { MaskableText } from '../../Shared/MaskableText/MaskableText'; import { getTotalAssignedRoles } from './utils'; import type { IamUserRoles, User } from '@linode/api-v4'; diff --git a/packages/manager/src/features/IAM/Users/UsersTable/UserRow.tsx b/packages/manager/src/features/IAM/Users/UsersTable/UserRow.tsx index f944c896cad..10374181065 100644 --- a/packages/manager/src/features/IAM/Users/UsersTable/UserRow.tsx +++ b/packages/manager/src/features/IAM/Users/UsersTable/UserRow.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { Avatar } from 'src/components/Avatar/Avatar'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; import { Link } from 'src/components/Link'; -import { MaskableText } from 'src/components/MaskableText/MaskableText'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; @@ -20,6 +19,7 @@ import { IAM_DELEGATE_USERS_PENDO_IDS, IAM_PARENT_USERS_PENDO_IDS, } from '../../Shared/constants'; +import { MaskableText } from '../../Shared/MaskableText/MaskableText'; import { UsersActionMenu } from './UsersActionMenu'; import type { User } from '@linode/api-v4';