From d012b8e880f4fff647f263fe791350c2a03a4dc4 Mon Sep 17 00:00:00 2001 From: ecarrill Date: Tue, 13 Aug 2024 11:00:11 -0700 Subject: [PATCH 01/15] refactor: [M3-6908] - Replace react-select in Profile --- packages/manager/src/MainContent.tsx | 6 +- .../PhoneVerification.styles.ts | 8 +- .../PhoneVerification/PhoneVerification.tsx | 71 ++++++++------- .../SecurityQuestions/Question.tsx | 34 +++---- .../QuestionAndAnswerPair.tsx | 7 +- .../SecurityQuestions/SecurityQuestions.tsx | 6 +- .../DisplaySettings/DisplaySettings.tsx | 4 +- .../DisplaySettings/TimezoneForm.test.tsx | 10 +-- .../Profile/DisplaySettings/TimezoneForm.tsx | 88 ++++++++++--------- .../Profile/LishSettings/LishSettings.tsx | 27 +++--- .../manager/src/features/Profile/Profile.tsx | 8 +- 11 files changed, 146 insertions(+), 123 deletions(-) diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index e84915eac47..f965574f599 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -140,7 +140,11 @@ const Kubernetes = React.lazy(() => })) ); const ObjectStorage = React.lazy(() => import('src/features/ObjectStorage')); -const Profile = React.lazy(() => import('src/features/Profile/Profile')); +const Profile = React.lazy(() => + import('src/features/Profile/Profile').then((module) => ({ + default: module.Profile, + })) +); const NodeBalancers = React.lazy( () => import('src/features/NodeBalancers/NodeBalancers') ); diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.styles.ts b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.styles.ts index 4a05ca75936..0ead4fdfa13 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.styles.ts +++ b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.styles.ts @@ -1,10 +1,10 @@ import { styled } from '@mui/material/styles'; +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { Box } from 'src/components/Box'; -import Select from 'src/components/EnhancedSelect/Select'; +import { FormHelperText } from 'src/components/FormHelperText'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; -import { FormHelperText } from 'src/components/FormHelperText'; export const StyledCodeSentMessageBox = styled(Box, { label: 'StyledCodeSentMessageBox', @@ -61,10 +61,10 @@ export const StyledPhoneNumberInput = styled(TextField, { minWidth: '300px', })); -export const StyledSelect = styled(Select, { +export const StyledSelect = styled(Autocomplete, { label: 'StyledSelect', })(({ theme }) => ({ - '& .MuiInputBase-input .react-select__indicators svg': { + '& .MuiInputBase-input svg': { color: `${theme.palette.primary.main} !important`, opacity: '1 !important', }, diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx index 4d94c22a06f..87f8bab3080 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx @@ -1,9 +1,8 @@ -import { APIError } from '@linode/api-v4/lib/types'; +import { useQueryClient } from '@tanstack/react-query'; import { useFormik } from 'formik'; -import { CountryCode, parsePhoneNumber } from 'libphonenumber-js'; +import { parsePhoneNumber } from 'libphonenumber-js'; import { useSnackbar } from 'notistack'; import * as React from 'react'; -import { useQueryClient } from '@tanstack/react-query'; import { Box } from 'src/components/Box'; import { Button } from 'src/components/Button/Button'; @@ -19,6 +18,8 @@ import { useVerifyPhoneVerificationCodeMutation, } from 'src/queries/profile/profile'; +import { countries } from './countries'; +import { getCountryFlag, getCountryName, getFormattedNumber } from './helpers'; import { StyledButtonContainer, StyledCodeSentMessageBox, @@ -29,14 +30,18 @@ import { StyledPhoneNumberTitle, StyledSelect, } from './PhoneVerification.styles'; -import { countries } from './countries'; -import { getCountryFlag, getCountryName, getFormattedNumber } from './helpers'; import type { SendPhoneVerificationCodePayload, VerifyVerificationCodePayload, } from '@linode/api-v4/lib/profile/types'; -import type { Item } from 'src/components/EnhancedSelect/Select'; +import type { APIError } from '@linode/api-v4/lib/types'; +import type { CountryCode } from 'libphonenumber-js'; + +export interface SelectPhoneVerificationOption { + label: string; + value: string; +} export const PhoneVerification = ({ phoneNumberRef, @@ -67,18 +72,22 @@ export const PhoneVerification = ({ mutateAsync: sendPhoneVerificationCode, reset: resetSendCodeMutation, } = useSendPhoneVerificationCodeMutation(); + const { error: verifyError, mutateAsync: sendVerificationCode, reset: resetCodeMutation, } = useVerifyPhoneVerificationCodeMutation(); + const isCodeSent = data !== undefined; + const onSubmitPhoneNumber = async ( values: SendPhoneVerificationCodePayload ) => { resetCodeMutation(); return await sendPhoneVerificationCode(values); }; + const onSubmitVerificationCode = async ( values: VerifyVerificationCodePayload ) => { @@ -164,22 +173,10 @@ export const PhoneVerification = ({ ); }; - const customStyles = { - menu: () => ({ - marginLeft: '-1px !important', - marginTop: '0px !important', - width: '500px', - }), - singleValue: (provided: React.CSSProperties) => - ({ - ...provided, - fontSize: '20px', - textAlign: 'center', - } as const), - }; const selectedCountry = countries.find( (country) => country.code === sendCodeForm.values.iso_code ); + const isFormSubmitting = isCodeSent ? verifyCodeForm.isSubmitting : sendCodeForm.isSubmitting; @@ -258,32 +255,34 @@ export const PhoneVerification = ({ isPhoneInputFocused={isPhoneInputFocused} > - sendCodeForm.values.iso_code === option.value - } - onChange={(item: Item) => - sendCodeForm.setFieldValue('iso_code', item.value) - } - options={countries.map((counrty) => ({ - label: `${getCountryName(counrty.name)} ${ - counrty.dialingCode - } ${getCountryFlag(counrty.code)}`, - value: counrty.code, + onChange={(_, item: SelectPhoneVerificationOption) => { + sendCodeForm.setFieldValue('iso_code', item.value); + }} + options={countries.map((country) => ({ + label: `${getCountryName(country.name)} ${ + country.dialingCode + } ${getCountryFlag(country.code)}`, + value: country.code, }))} + sx={{ + fontSize: '20px', + marginLeft: '-1px !important', + marginTop: '0px !important', + textAlign: 'center', + // width: '500px', + }} + textFieldProps={{ + hideLabel: true, + }} value={{ label: getCountryFlag(sendCodeForm.values.iso_code), - value: sendCodeForm.values.iso_code, }} - hideLabel + disableClearable id="iso_code" - isClearable={false} label="ISO Code" - menuPlacement="bottom" - name="iso_code" noMarginTop onBlur={() => setIsPhoneInputFocused(false)} onFocus={() => setIsPhoneInputFocused(true)} - styles={customStyles} /> void; - options: Item[]; + options: SelectQuestionOption[]; questionResponse: SecurityQuestion | undefined; setFieldValue: (field: string, value: SecurityQuestion) => void; } @@ -32,15 +38,8 @@ export const Question = (props: Props) => { } : undefined; - const name = `security_questions[${index}].id`; const label = `Question ${index + 1}`; - const onChange = (item: Item) => { - setFieldValue(`security_questions[${index}]`, { - id: item.value, - question: item.label, - response: '', - }); - }; + if (isReadOnly) { return ( <> @@ -55,12 +54,17 @@ export const Question = (props: Props) => { ); } return ( - + + + option.value === timeZoneValue + )} + data-qa-tz-select + defaultValue={defaultTimeZone} + disableClearable + errorText={error?.[0].reason} + label="Time Zone" + onChange={(_, option) => handleTimeZoneChange(option.value)} + options={timeZoneList} + placeholder="Choose a Time Zone" + sx={{ width: '416px' }} + /> + { ); }; -const StyledRootContainer = styled(Box, { +const StyledRootContainer = styled('div', { label: 'StyledRootContainer', })(({ theme }) => ({ + alignItems: 'flex-end', + display: 'flex', + justifyContent: 'space-between', [theme.breakpoints.down('md')]: { + alignItems: 'flex-start', flexDirection: 'column', }, })); diff --git a/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx b/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx index 1aa5aaa06d5..c06af05e5c9 100644 --- a/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx +++ b/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx @@ -1,44 +1,51 @@ -import { Profile } from '@linode/api-v4/lib/profile'; -import { APIError } from '@linode/api-v4/lib/types'; import { useTheme } from '@mui/material/styles'; import { equals, lensPath, remove, set } from 'ramda'; import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { Box } from 'src/components/Box'; import { Button } from 'src/components/Button/Button'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; -import Select, { Item } from 'src/components/EnhancedSelect/Select'; +import { FormControl } from 'src/components/FormControl'; import { Notice } from 'src/components/Notice/Notice'; import { Paper } from 'src/components/Paper'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; -import { FormControl } from 'src/components/FormControl'; import { useMutateProfile, useProfile } from 'src/queries/profile/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; +import type { Profile } from '@linode/api-v4/lib/profile'; +import type { APIError } from '@linode/api-v4/lib/types'; +import type { Item } from 'src/components/EnhancedSelect/Select'; + export const LishSettings = () => { const theme = useTheme(); const { data: profile, isLoading } = useProfile(); const { mutateAsync: updateProfile } = useMutateProfile(); const [submitting, setSubmitting] = React.useState(false); + const [errors, setErrors] = React.useState([]); + const [success, setSuccess] = React.useState(); + const thirdPartyEnabled = profile?.authentication_type !== 'password'; + const [lishAuthMethod, setLishAuthMethod] = React.useState< Profile['lish_auth_method'] | undefined >(profile?.lish_auth_method || 'password_keys'); + const [authorizedKeys, setAuthorizedKeys] = React.useState( profile?.authorized_keys || [] ); + const [authorizedKeysCount, setAuthorizedKeysCount] = React.useState( profile?.authorized_keys ? profile!.authorized_keys.length : 1 ); - const [errors, setErrors] = React.useState([]); - const [success, setSuccess] = React.useState(); - const thirdPartyEnabled = profile?.authentication_type !== 'password'; + const tooltipText = thirdPartyEnabled ? 'Password is disabled because Third-Party Authentication has been enabled.' : ''; + const hasErrorFor = getAPIErrorFor( { authorized_keys: 'ssh public keys', @@ -46,6 +53,7 @@ export const LishSettings = () => { }, errors ); + const generalError = hasErrorFor('none'); const authMethodError = hasErrorFor('lish_auth_method'); const authorizedKeysError = hasErrorFor('authorized_keys'); @@ -133,7 +141,7 @@ export const LishSettings = () => { {isLoading ? null : ( <> - + form.setFieldValue('expiry', selected.value) + } + disableClearable label="Expiry" - name="expiry" - onChange={handleExpiryChange} options={expiryList} value={expiryList.find((item) => item.value === form.values.expiry)} /> From 230812bf8ec67227ffa3369f61ce88c3ec861ac0 Mon Sep 17 00:00:00 2001 From: ecarrill Date: Wed, 21 Aug 2024 08:53:03 -0700 Subject: [PATCH 06/15] Add changeset --- .../.changeset/pr-10780-tech-stories-1724255560445.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10780-tech-stories-1724255560445.md diff --git a/packages/manager/.changeset/pr-10780-tech-stories-1724255560445.md b/packages/manager/.changeset/pr-10780-tech-stories-1724255560445.md new file mode 100644 index 00000000000..e4311cecf31 --- /dev/null +++ b/packages/manager/.changeset/pr-10780-tech-stories-1724255560445.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Replace 'react-select' with Autocomplete in Profile ([#10780](https://github.com/linode/manager/pull/10780)) From c9308f28e9e4378b24ca6be1d223ef014a394065 Mon Sep 17 00:00:00 2001 From: ecarrill Date: Thu, 22 Aug 2024 08:39:55 -0700 Subject: [PATCH 07/15] Revert changes to TimezoneForm --- .../DisplaySettings/DisplaySettings.tsx | 4 +- .../DisplaySettings/TimezoneForm.test.tsx | 10 ++-- .../Profile/DisplaySettings/TimezoneForm.tsx | 50 +++++++++---------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/manager/src/features/Profile/DisplaySettings/DisplaySettings.tsx b/packages/manager/src/features/Profile/DisplaySettings/DisplaySettings.tsx index 9e21bcba941..1a1ba9ae96b 100644 --- a/packages/manager/src/features/Profile/DisplaySettings/DisplaySettings.tsx +++ b/packages/manager/src/features/Profile/DisplaySettings/DisplaySettings.tsx @@ -17,7 +17,7 @@ import { RESTRICTED_FIELD_TOOLTIP } from 'src/features/Account/constants'; import { useNotificationsQuery } from 'src/queries/account/notifications'; import { useMutateProfile, useProfile } from 'src/queries/profile/profile'; -import { TimeZoneForm } from './TimeZoneForm'; +import { TimezoneForm } from './TimezoneForm'; import type { ApplicationState } from 'src/store'; @@ -154,7 +154,7 @@ export const DisplaySettings = () => { type="email" /> - + ); }; diff --git a/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.test.tsx b/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.test.tsx index 0097dc20008..e3e94fe5e58 100644 --- a/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.test.tsx +++ b/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.test.tsx @@ -6,7 +6,7 @@ import { HttpResponse, http, server } from 'src/mocks/testServer'; import { queryClientFactory } from 'src/queries/base'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import { TimeZoneForm, formatOffset } from './TimeZoneForm'; +import { TimezoneForm, formatOffset } from './TimezoneForm'; const queryClient = queryClientFactory(); @@ -23,7 +23,7 @@ describe('Timezone change form', () => { it('should render input label', async () => { const { getByTestId, getByText } = renderWithTheme( - , + , { queryClient } ); @@ -36,7 +36,7 @@ describe('Timezone change form', () => { it('should show a message if an admin is logged in as a customer', () => { const { getByTestId } = renderWithTheme( - , + , { queryClient } ); @@ -45,7 +45,7 @@ describe('Timezone change form', () => { it('should not show a message if the user is logged in normally', () => { const { queryByTestId } = renderWithTheme( - , + , { queryClient } ); @@ -54,7 +54,7 @@ describe('Timezone change form', () => { it("should include text with the user's current time zone", async () => { const { getByText } = renderWithTheme( - , + , { queryClient } ); diff --git a/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.tsx b/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.tsx index 1c9dc623590..e6b4856572f 100644 --- a/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.tsx +++ b/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.tsx @@ -15,18 +15,18 @@ interface Props { loggedInAsCustomer: boolean; } -interface TimeZone { +interface Timezone { label: string; name: string; offset: number; } -interface TimeZoneOption { +interface TimezoneOption { label: string; value: string; } -export const formatOffset = ({ label, offset }: TimeZone) => { +export const formatOffset = ({ label, offset }: Timezone) => { const minutes = (Math.abs(offset) % 60).toLocaleString(undefined, { minimumIntegerDigits: 2, useGrouping: false, @@ -37,46 +37,46 @@ export const formatOffset = ({ label, offset }: TimeZone) => { return `\(GMT ${isPositive}${hours}:${minutes}\) ${label}`; }; -const renderTimeZonesList = () => { +const renderTimezonesList = () => { return timezones .map((tz) => ({ ...tz, offset: DateTime.now().setZone(tz.name).offset })) .sort((a, b) => a.offset - b.offset) - .map((tz: TimeZone) => { + .map((tz: Timezone) => { const label = formatOffset(tz); return { label, value: tz.name }; }); }; -const timeZoneList: TimeZoneOption[] = renderTimeZonesList(); +const timezoneList: TimezoneOption[] = renderTimezonesList(); -export const TimeZoneForm = (props: Props) => { +export const TimezoneForm = (props: Props) => { const { loggedInAsCustomer } = props; const { enqueueSnackbar } = useSnackbar(); const { data: profile } = useProfile(); const { error, isLoading, mutateAsync: updateProfile } = useMutateProfile(); - const [timeZoneValue, setTimeZoneValue] = React.useState(null); - const timeZone = profile?.timezone ?? ''; + const [timezoneValue, setTimezoneValue] = React.useState(null); + const timezone = profile?.timezone ?? ''; - const handleTimeZoneChange = (timezone: string) => { - setTimeZoneValue(timezone); + const handleTimezoneChange = (timezone: string) => { + setTimezoneValue(timezone); }; const onSubmit = () => { - if (timeZoneValue === null) { + if (timezoneValue === null) { return; } - updateProfile({ timezone: timeZoneValue }).then(() => { + updateProfile({ timezone: timezoneValue }).then(() => { enqueueSnackbar('Successfully updated timezone', { variant: 'success' }); }); }; - const defaultTimeZone = timeZoneList.find((eachZone) => { - return eachZone.value === timeZone; + const defaultTimezone = timezoneList.find((eachZone) => { + return eachZone.value === timezone; }); const disabled = - timeZoneValue === null || defaultTimeZone?.value === timeZoneValue; + timezoneValue === null || defaultTimezone?.value === timezoneValue; if (!profile) { return ; @@ -88,31 +88,31 @@ export const TimeZoneForm = (props: Props) => { While you are logged in as a customer, all times, dates, and graphs - will be displayed in your browser’s time zone ({timeZone}). + will be displayed in your browser’s timezone ({timezone}). ) : null} option.value === timeZoneValue + value={timezoneList.find( + (option) => option.value === timezoneValue )} data-qa-tz-select - defaultValue={defaultTimeZone} + defaultValue={defaultTimezone} disableClearable errorText={error?.[0].reason} - label="Time Zone" - onChange={(_, option) => handleTimeZoneChange(option.value)} - options={timeZoneList} - placeholder="Choose a Time Zone" + label="Timezone" + onChange={(_, option) => handleTimezoneChange(option.value)} + options={timezoneList} + placeholder="Choose a Timezone" sx={{ width: '416px' }} /> Date: Thu, 22 Aug 2024 14:08:57 -0700 Subject: [PATCH 08/15] Fix more styling issues and mobile view --- .../Autocomplete/Autocomplete.styles.tsx | 4 +++- .../Profile/APITokens/CreateAPITokenDrawer.tsx | 14 ++++++++++++++ .../PhoneVerification/PhoneVerification.tsx | 9 ++++++--- .../Profile/DisplaySettings/TimezoneForm.tsx | 4 ++++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/components/Autocomplete/Autocomplete.styles.tsx b/packages/manager/src/components/Autocomplete/Autocomplete.styles.tsx index d87e298ea51..9f31b39af99 100644 --- a/packages/manager/src/components/Autocomplete/Autocomplete.styles.tsx +++ b/packages/manager/src/components/Autocomplete/Autocomplete.styles.tsx @@ -1,10 +1,12 @@ import DoneIcon from '@mui/icons-material/Done'; -import Popper, { PopperProps } from '@mui/material/Popper'; +import Popper from '@mui/material/Popper'; import { styled } from '@mui/material/styles'; import React from 'react'; import { omittedProps } from 'src/utilities/omittedProps'; +import type { PopperProps } from '@mui/material/Popper'; + export const StyledListItem = styled('li', { label: 'StyledListItem', shouldForwardProp: omittedProps(['selectAllOption']), diff --git a/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.tsx b/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.tsx index 80face98a60..86022bc269e 100644 --- a/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.tsx +++ b/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.tsx @@ -215,6 +215,20 @@ export const CreateAPITokenDrawer = (props: Props) => { onChange={(_, selected) => form.setFieldValue('expiry', selected.value) } + sx={{ + '&& .MuiAutocomplete-inputRoot': { + paddingLeft: 1, + paddingRight: 0, + }, + '&& .MuiInput-input': { + padding: '0px 2px', + }, + }} + sxPopperComponent={{ + '&& .MuiAutocomplete-listbox': { + padding: 0, + }, + }} disableClearable label="Expiry" options={expiryList} diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx index b56bf892276..0dbafcc1c14 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx @@ -268,9 +268,12 @@ export const PhoneVerification = ({ value: country.code, }))} sxPopperComponent={{ - marginLeft: '213px !important', - marginTop: '0px !important', - width: '500px !important', + '& .MuiPaper-root.MuiAutocomplete-paper': { + maxHeight: '285px', + overflow: 'hidden', + textWrap: 'nowrap', + width: 'fit-content', + }, }} textFieldProps={{ hideLabel: true, diff --git a/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.tsx b/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.tsx index e6b4856572f..6c69aca1e27 100644 --- a/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.tsx +++ b/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.tsx @@ -95,6 +95,10 @@ export const TimezoneForm = (props: Props) => { option.value === timezoneValue )} From 25983a1d20a4c4690da06566242ef1938e2a24a3 Mon Sep 17 00:00:00 2001 From: ecarrill Date: Fri, 23 Aug 2024 08:16:04 -0700 Subject: [PATCH 09/15] Fix broken unit tests in CreateAPITokenDrawer --- .../APITokens/CreateAPITokenDrawer.test.tsx | 18 ++++++++++-------- .../PhoneVerification.styles.ts | 2 -- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.test.tsx b/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.test.tsx index 124ffd92732..96de8bb2306 100644 --- a/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.test.tsx +++ b/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.test.tsx @@ -32,16 +32,16 @@ const props = { describe('Create API Token Drawer', () => { it('checks API Token Drawer rendering', () => { - const { getByTestId, getByText } = renderWithTheme( + const { getAllByTestId, getByTestId, getByText } = renderWithTheme( ); const drawerTitle = getByText('Add Personal Access Token'); expect(drawerTitle).toBeVisible(); const labelTitle = getByText(/Label/); - const labelField = getByTestId('textfield-input'); + const labelField = getAllByTestId('textfield-input'); expect(labelTitle).toBeVisible(); - expect(labelField).toBeEnabled(); + expect(labelField[0]).toBeEnabled(); const expiry = getByText(/Expiry/); expect(expiry).toBeVisible(); @@ -67,12 +67,12 @@ describe('Create API Token Drawer', () => { }) ); - const { getByLabelText, getByTestId, getByText } = renderWithTheme( + const { getAllByTestId, getByLabelText, getByText } = renderWithTheme( ); - const labelField = getByTestId('textfield-input'); - await userEvent.type(labelField, 'my-test-token'); + const labelField = getAllByTestId('textfield-input'); + await userEvent.type(labelField[0], 'my-test-token'); const selectAllNoAccessPermRadioButton = getByLabelText( 'Select no access for all' @@ -110,8 +110,10 @@ describe('Create API Token Drawer', () => { }); it('Should default to 6 months for expiration', () => { - const { getByText } = renderWithTheme(); - getByText('In 6 months'); + const { getAllByRole } = renderWithTheme( + + ); + expect(getAllByRole('combobox')[0]).toHaveDisplayValue('In 6 months'); }); it('Should show the Child Account Access scope for a parent user account with the parent/child feature flag on', () => { diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.styles.ts b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.styles.ts index 9c0a866dc4f..1f40386a932 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.styles.ts +++ b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.styles.ts @@ -68,8 +68,6 @@ export const StyledSelect = styled(Autocomplete, { boxShadow: 'unset', }, border: 'none', - font: '20px', - height: '34px', })); export const StyledPhoneNumberInput = styled(TextField, { From ff18e58674a3f0818f07213bc395ccbbfbf4fe34 Mon Sep 17 00:00:00 2001 From: ecarrill Date: Fri, 23 Aug 2024 08:54:07 -0700 Subject: [PATCH 10/15] Fix broken unit test in TimezoneForm --- .../features/Profile/DisplaySettings/TimezoneForm.test.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.test.tsx b/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.test.tsx index e3e94fe5e58..d011a5ce24d 100644 --- a/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.test.tsx +++ b/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.test.tsx @@ -53,13 +53,11 @@ describe('Timezone change form', () => { }); it("should include text with the user's current time zone", async () => { - const { getByText } = renderWithTheme( + const { queryByTestId } = renderWithTheme( , { queryClient } ); - - expect(getByText('New York', { exact: false })).toBeInTheDocument(); - expect(getByText('Eastern Time', { exact: false })).toBeInTheDocument(); + expect(queryByTestId('admin-notice')).toHaveTextContent('America/New_York'); }); }); From 9b8bd244cda8743721195e121638a615728b7ed2 Mon Sep 17 00:00:00 2001 From: ecarrill Date: Fri, 23 Aug 2024 11:28:05 -0700 Subject: [PATCH 11/15] Fix e2e test in Billing --- .../src/features/Profile/DisplaySettings/TimezoneForm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.tsx b/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.tsx index 6c69aca1e27..79176e66794 100644 --- a/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.tsx +++ b/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.tsx @@ -102,6 +102,7 @@ export const TimezoneForm = (props: Props) => { value={timezoneList.find( (option) => option.value === timezoneValue )} + autoHighlight data-qa-tz-select defaultValue={defaultTimezone} disableClearable From 438dc0c0a37c966e4e2aafddac3c118b0dfad12f Mon Sep 17 00:00:00 2001 From: ecarrill Date: Tue, 27 Aug 2024 10:44:13 -0700 Subject: [PATCH 12/15] Fix failing e2e and console errors mentioned in PR review --- .../core/account/security-questions.spec.ts | 2 +- .../SecurityQuestions/Question.tsx | 1 + .../Profile/DisplaySettings/TimezoneForm.tsx | 24 ++++++++++--------- .../Profile/LishSettings/LishSettings.tsx | 16 ++++++++----- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/manager/cypress/e2e/core/account/security-questions.spec.ts b/packages/manager/cypress/e2e/core/account/security-questions.spec.ts index aab84a220d3..2205b57e5cd 100644 --- a/packages/manager/cypress/e2e/core/account/security-questions.spec.ts +++ b/packages/manager/cypress/e2e/core/account/security-questions.spec.ts @@ -93,7 +93,7 @@ const setSecurityQuestionAnswer = ( answer: string ) => { getSecurityQuestion(questionNumber).within(() => { - cy.get('[data-qa-enhanced-select]') + cy.findByLabelText(`Question ${questionNumber}`) .should('be.visible') .click() .type(`${question}{enter}`); diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/SecurityQuestions/Question.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/SecurityQuestions/Question.tsx index ac1b382ea0f..411644165ef 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/SecurityQuestions/Question.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/SecurityQuestions/Question.tsx @@ -62,6 +62,7 @@ export const Question = (props: Props) => { response: '', }); }} + autoHighlight defaultValue={currentOption} disableClearable label={label} diff --git a/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.tsx b/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.tsx index 79176e66794..85f0baaca5d 100644 --- a/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.tsx +++ b/packages/manager/src/features/Profile/DisplaySettings/TimezoneForm.tsx @@ -21,9 +21,9 @@ interface Timezone { offset: number; } -interface TimezoneOption { - label: string; - value: string; +export interface TimezoneOption { + label: L; + value: T; } export const formatOffset = ({ label, offset }: Timezone) => { @@ -37,7 +37,7 @@ export const formatOffset = ({ label, offset }: Timezone) => { return `\(GMT ${isPositive}${hours}:${minutes}\) ${label}`; }; -const renderTimezonesList = () => { +const renderTimezonesList = (): TimezoneOption[] => { return timezones .map((tz) => ({ ...tz, offset: DateTime.now().setZone(tz.name).offset })) .sort((a, b) => a.offset - b.offset) @@ -47,26 +47,28 @@ const renderTimezonesList = () => { }); }; -const timezoneList: TimezoneOption[] = renderTimezonesList(); +const timezoneList = renderTimezonesList(); export const TimezoneForm = (props: Props) => { const { loggedInAsCustomer } = props; const { enqueueSnackbar } = useSnackbar(); const { data: profile } = useProfile(); const { error, isLoading, mutateAsync: updateProfile } = useMutateProfile(); - const [timezoneValue, setTimezoneValue] = React.useState(null); + const [timezoneValue, setTimezoneValue] = React.useState< + TimezoneOption | string + >(''); const timezone = profile?.timezone ?? ''; - const handleTimezoneChange = (timezone: string) => { + const handleTimezoneChange = (timezone: TimezoneOption) => { setTimezoneValue(timezone); }; const onSubmit = () => { - if (timezoneValue === null) { + if (timezoneValue === '') { return; } - updateProfile({ timezone: timezoneValue }).then(() => { + updateProfile({ timezone: String(timezoneValue) }).then(() => { enqueueSnackbar('Successfully updated timezone', { variant: 'success' }); }); }; @@ -76,7 +78,7 @@ export const TimezoneForm = (props: Props) => { }); const disabled = - timezoneValue === null || defaultTimezone?.value === timezoneValue; + timezoneValue === '' || defaultTimezone?.value === timezoneValue; if (!profile) { return ; @@ -108,7 +110,7 @@ export const TimezoneForm = (props: Props) => { disableClearable errorText={error?.[0].reason} label="Timezone" - onChange={(_, option) => handleTimezoneChange(option.value)} + onChange={(_, option) => handleTimezoneChange(option)} options={timezoneList} placeholder="Choose a Timezone" sx={{ width: '416px' }} diff --git a/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx b/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx index c06af05e5c9..7bacbe35d20 100644 --- a/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx +++ b/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx @@ -19,7 +19,11 @@ import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import type { Profile } from '@linode/api-v4/lib/profile'; import type { APIError } from '@linode/api-v4/lib/types'; -import type { Item } from 'src/components/EnhancedSelect/Select'; + +export interface LishAuthOption { + label: L; + value: T; +} export const LishSettings = () => { const theme = useTheme(); @@ -31,7 +35,7 @@ export const LishSettings = () => { const thirdPartyEnabled = profile?.authentication_type !== 'password'; const [lishAuthMethod, setLishAuthMethod] = React.useState< - Profile['lish_auth_method'] | undefined + Profile['lish_auth_method'] | string >(profile?.lish_auth_method || 'password_keys'); const [authorizedKeys, setAuthorizedKeys] = React.useState( @@ -111,9 +115,6 @@ export const LishSettings = () => { }); }; - const onListAuthMethodChange = (e: Item) => - setLishAuthMethod(e.value); - const onPublicKeyChange = (idx: number) => ( e: React.ChangeEvent ) => { @@ -148,12 +149,15 @@ export const LishSettings = () => { }, tooltipText, }} + value={modeOptions.find( + (option) => option.value === lishAuthMethod + )} defaultValue={defaultMode} disableClearable errorText={authMethodError} id="mode-select" label="Authentication Mode" - onChange={onListAuthMethodChange as any} + onChange={(_, item) => setLishAuthMethod(item.value)} options={modeOptions} /> From ec934c1d0597003c55ec7e110be932bcb992f902 Mon Sep 17 00:00:00 2001 From: ecarrill Date: Wed, 28 Aug 2024 11:43:23 -0700 Subject: [PATCH 13/15] Fix styling issues from PR review --- .../PhoneVerification.styles.ts | 28 ++++++++++--------- .../PhoneVerification/PhoneVerification.tsx | 13 ++++++--- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.styles.ts b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.styles.ts index 1f40386a932..9734f87af46 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.styles.ts +++ b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.styles.ts @@ -33,9 +33,10 @@ export const StyledLabel = styled(Typography, { export const StyledInputContainer = styled(Box, { label: 'StyledInputContainer', })<{ isPhoneInputFocused: boolean }>(({ isPhoneInputFocused, theme }) => ({ + backgroundColor: theme.name === 'dark' ? '#343438' : undefined, border: theme.name === 'light' ? '1px solid #ccc' : '1px solid #222', transition: 'border-color 225ms ease-in-out', - width: '370px', + width: 'fit-content', ...(isPhoneInputFocused && (theme.name === 'light' ? { @@ -48,26 +49,26 @@ export const StyledInputContainer = styled(Box, { })), })); -export const StyledSelect = styled(Autocomplete, { - label: 'StyledSelect', +export const StyledISOCodeSelect = styled(Autocomplete, { + label: 'StyledISOCodeSelect', })(({ theme }) => ({ - '& .Mui-focused': { - borderColor: 'unset', - boxShadow: 'none', - }, '& .MuiAutocomplete-inputRoot': { - border: 'none', - }, - '& .MuiInputBase-root svg': { border: 'unset', - color: `${theme.palette.primary.main} !important`, - opacity: '1 !important', + }, + '&& .MuiInputBase-root svg': { + color: `${theme.palette.primary.main}`, + opacity: '1', + }, + '&.Mui-focused': { + borderColor: 'unset', + boxShadow: 'none', }, '&:focus': { borderColor: 'unset', boxShadow: 'unset', }, - border: 'none', + height: '34px', + width: '70px !important', })); export const StyledPhoneNumberInput = styled(TextField, { @@ -82,6 +83,7 @@ export const StyledPhoneNumberInput = styled(TextField, { boxShadow: 'unset', }, border: 'unset', + minWidth: '300px', })); export const StyledFormHelperText = styled(FormHelperText, { diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx index 0dbafcc1c14..c1b75b5c224 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx @@ -24,11 +24,11 @@ import { StyledButtonContainer, StyledCodeSentMessageBox, StyledFormHelperText, + StyledISOCodeSelect, StyledInputContainer, StyledLabel, StyledPhoneNumberInput, StyledPhoneNumberTitle, - StyledSelect, } from './PhoneVerification.styles'; import type { @@ -257,7 +257,7 @@ export const PhoneVerification = ({ display="flex" isPhoneInputFocused={isPhoneInputFocused} > - { sendCodeForm.setFieldValue('iso_code', item.value); }} @@ -269,6 +269,7 @@ export const PhoneVerification = ({ }))} sxPopperComponent={{ '& .MuiPaper-root.MuiAutocomplete-paper': { + border: '1px solid #3683dc', maxHeight: '285px', overflow: 'hidden', textWrap: 'nowrap', @@ -277,18 +278,22 @@ export const PhoneVerification = ({ }} textFieldProps={{ hideLabel: true, - style: { minWidth: '72px' }, + style: { + border: 'none', + minWidth: '72px', + }, }} value={{ label: getCountryFlag(sendCodeForm.values.iso_code), }} + autoHighlight disableClearable disablePortal={true} id="iso_code" label="ISO Code" - noMarginTop onBlur={() => setIsPhoneInputFocused(false)} onFocus={() => setIsPhoneInputFocused(true)} + placeholder="" /> Date: Wed, 4 Sep 2024 10:37:02 -0700 Subject: [PATCH 14/15] Fix selected option highlight in Question Component --- .../AuthenticationSettings/SecurityQuestions/Question.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/SecurityQuestions/Question.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/SecurityQuestions/Question.tsx index 411644165ef..15a109290e8 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/SecurityQuestions/Question.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/SecurityQuestions/Question.tsx @@ -62,12 +62,12 @@ export const Question = (props: Props) => { response: '', }); }} - autoHighlight defaultValue={currentOption} disableClearable label={label} options={options} placeholder="Select a question" + value={options.find((option) => option.value === questionResponse?.id)} /> ); }; From 965139daac9b26152fc4b6c09dc5f9370939c591 Mon Sep 17 00:00:00 2001 From: ecarrill Date: Wed, 11 Sep 2024 13:07:35 -0700 Subject: [PATCH 15/15] Fix failing test, ui bug, and 3rd party auth bug --- .../e2e/core/account/security-questions.spec.ts | 2 +- .../PhoneVerification.styles.ts | 12 +++++++----- .../PhoneVerification/PhoneVerification.tsx | 3 +++ .../SecurityQuestions/Question.tsx | 1 + .../Profile/LishSettings/LishSettings.tsx | 16 ++++++++++------ 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/manager/cypress/e2e/core/account/security-questions.spec.ts b/packages/manager/cypress/e2e/core/account/security-questions.spec.ts index 2205b57e5cd..bc280f36e5a 100644 --- a/packages/manager/cypress/e2e/core/account/security-questions.spec.ts +++ b/packages/manager/cypress/e2e/core/account/security-questions.spec.ts @@ -100,7 +100,7 @@ const setSecurityQuestionAnswer = ( }); getSecurityQuestionAnswer(questionNumber).within(() => { - cy.get('[data-testid="textfield-input"]') + cy.findByLabelText(`Answer ${questionNumber}`) .should('be.visible') .should('be.enabled') .click() diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.styles.ts b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.styles.ts index 9734f87af46..d6caaa14b50 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.styles.ts +++ b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.styles.ts @@ -5,6 +5,7 @@ import { Box } from 'src/components/Box'; import { FormHelperText } from 'src/components/FormHelperText'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; +import { omittedProps } from 'src/utilities/omittedProps'; export const StyledCodeSentMessageBox = styled(Box, { label: 'StyledCodeSentMessageBox', @@ -32,6 +33,7 @@ export const StyledLabel = styled(Typography, { export const StyledInputContainer = styled(Box, { label: 'StyledInputContainer', + shouldForwardProp: omittedProps(['isPhoneInputFocused']), })<{ isPhoneInputFocused: boolean }>(({ isPhoneInputFocused, theme }) => ({ backgroundColor: theme.name === 'dark' ? '#343438' : undefined, border: theme.name === 'light' ? '1px solid #ccc' : '1px solid #222', @@ -52,17 +54,17 @@ export const StyledInputContainer = styled(Box, { export const StyledISOCodeSelect = styled(Autocomplete, { label: 'StyledISOCodeSelect', })(({ theme }) => ({ - '& .MuiAutocomplete-inputRoot': { + '& div.Mui-focused': { + borderColor: 'unset', + boxShadow: 'none', + }, + '& div.MuiAutocomplete-inputRoot': { border: 'unset', }, '&& .MuiInputBase-root svg': { color: `${theme.palette.primary.main}`, opacity: '1', }, - '&.Mui-focused': { - borderColor: 'unset', - boxShadow: 'none', - }, '&:focus': { borderColor: 'unset', boxShadow: 'unset', diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx index f96f458c617..d6aa0d7bc31 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx @@ -258,6 +258,9 @@ export const PhoneVerification = ({ isPhoneInputFocused={isPhoneInputFocused} > + option.label === value.label + } onChange={(_, item: SelectPhoneVerificationOption) => { sendCodeForm.setFieldValue('iso_code', item.value); }} diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/SecurityQuestions/Question.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/SecurityQuestions/Question.tsx index 15a109290e8..a77e0e8b22d 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/SecurityQuestions/Question.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/SecurityQuestions/Question.tsx @@ -62,6 +62,7 @@ export const Question = (props: Props) => { response: '', }); }} + autoHighlight defaultValue={currentOption} disableClearable label={label} diff --git a/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx b/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx index 7bacbe35d20..6cfcc2c5e13 100644 --- a/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx +++ b/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx @@ -35,7 +35,7 @@ export const LishSettings = () => { const thirdPartyEnabled = profile?.authentication_type !== 'password'; const [lishAuthMethod, setLishAuthMethod] = React.useState< - Profile['lish_auth_method'] | string + Profile['lish_auth_method'] | undefined >(profile?.lish_auth_method || 'password_keys'); const [authorizedKeys, setAuthorizedKeys] = React.useState( @@ -64,7 +64,7 @@ export const LishSettings = () => { const modeOptions = [ { - isDisabled: profile?.authentication_type !== 'password', + disabled: profile?.authentication_type !== 'password', label: 'Allow both password and key authentication', value: 'password_keys', }, @@ -80,9 +80,9 @@ export const LishSettings = () => { const defaultMode = modeOptions.find((eachMode) => { if (profile?.authentication_type !== 'password') { - return (eachMode.value as any) === 'keys_only'; + return (eachMode.value as Profile['lish_auth_method']) === 'keys_only'; } else { - return (eachMode.value as any) === lishAuthMethod; + return (eachMode.value as Profile['lish_auth_method']) === lishAuthMethod; } }); @@ -97,7 +97,7 @@ export const LishSettings = () => { updateProfile({ authorized_keys: keys, - lish_auth_method: lishAuthMethod as any, + lish_auth_method: lishAuthMethod as Profile['lish_auth_method'], }) .then((profileData) => { setSubmitting(false); @@ -143,6 +143,10 @@ export const LishSettings = () => { <> + ) => setLishAuthMethod(item.value)} textFieldProps={{ dataAttrs: { 'data-qa-mode-select': true, @@ -155,9 +159,9 @@ export const LishSettings = () => { defaultValue={defaultMode} disableClearable errorText={authMethodError} + getOptionDisabled={(option) => option.disabled === true} id="mode-select" label="Authentication Mode" - onChange={(_, item) => setLishAuthMethod(item.value)} options={modeOptions} />