mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: service accounts (UI - extract PAT form) (#2733)
https://linear.app/unleash/issue/2-540/extract-pat-form By refactoring the UI logic and extracting the PAT form we can use the same component on our service account creation form.
This commit is contained in:
		
							parent
							
								
									11f4435a9e
								
							
						
					
					
						commit
						4005bb8a8b
					
				| @ -1,4 +1,4 @@ | ||||
| import { Alert, Button, styled, Typography } from '@mui/material'; | ||||
| import { Button, styled } from '@mui/material'; | ||||
| import FormTemplate from 'component/common/FormTemplate/FormTemplate'; | ||||
| import { SidebarModal } from 'component/common/SidebarModal/SidebarModal'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| @ -7,13 +7,13 @@ import { FC, FormEvent, useEffect, useState } from 'react'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { usePersonalAPITokens } from 'hooks/api/getters/usePersonalAPITokens/usePersonalAPITokens'; | ||||
| import { usePersonalAPITokensApi } from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi'; | ||||
| import Input from 'component/common/Input/Input'; | ||||
| import SelectMenu from 'component/common/select'; | ||||
| import { formatDateYMD } from 'utils/formatDate'; | ||||
| import { useLocationSettings } from 'hooks/useLocationSettings'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { INewPersonalAPIToken } from 'interfaces/personalAPIToken'; | ||||
| import { DateTimePicker } from 'component/common/DateTimePicker/DateTimePicker'; | ||||
| import { | ||||
|     calculateExpirationDate, | ||||
|     ExpirationOption, | ||||
|     IPersonalAPITokenFormErrors, | ||||
|     PersonalAPITokenForm, | ||||
| } from './PersonalAPITokenForm/PersonalAPITokenForm'; | ||||
| 
 | ||||
| const StyledForm = styled('form')(() => ({ | ||||
|     display: 'flex', | ||||
| @ -21,50 +21,6 @@ const StyledForm = styled('form')(() => ({ | ||||
|     height: '100%', | ||||
| })); | ||||
| 
 | ||||
| const StyledInputDescription = styled('p')(({ theme }) => ({ | ||||
|     color: theme.palette.text.secondary, | ||||
|     marginBottom: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| const StyledInput = styled(Input)(({ theme }) => ({ | ||||
|     width: '100%', | ||||
|     maxWidth: theme.spacing(50), | ||||
|     marginBottom: theme.spacing(2), | ||||
| })); | ||||
| 
 | ||||
| const StyledExpirationPicker = styled('div')<{ custom?: boolean }>( | ||||
|     ({ theme, custom }) => ({ | ||||
|         display: 'flex', | ||||
|         alignItems: custom ? 'start' : 'center', | ||||
|         gap: theme.spacing(1.5), | ||||
|         marginBottom: theme.spacing(2), | ||||
|         [theme.breakpoints.down('sm')]: { | ||||
|             flexDirection: 'column', | ||||
|             alignItems: 'flex-start', | ||||
|         }, | ||||
|     }) | ||||
| ); | ||||
| 
 | ||||
| const StyledSelectMenu = styled(SelectMenu)(({ theme }) => ({ | ||||
|     minWidth: theme.spacing(20), | ||||
|     marginRight: theme.spacing(0.5), | ||||
|     [theme.breakpoints.down('sm')]: { | ||||
|         width: theme.spacing(50), | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledDateTimePicker = styled(DateTimePicker)(({ theme }) => ({ | ||||
|     width: theme.spacing(28), | ||||
|     [theme.breakpoints.down('sm')]: { | ||||
|         width: theme.spacing(50), | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledAlert = styled(Alert)(({ theme }) => ({ | ||||
|     marginBottom: theme.spacing(2), | ||||
|     maxWidth: theme.spacing(50), | ||||
| })); | ||||
| 
 | ||||
| const StyledButtonContainer = styled('div')(({ theme }) => ({ | ||||
|     marginTop: 'auto', | ||||
|     display: 'flex', | ||||
| @ -78,49 +34,7 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({ | ||||
|     marginLeft: theme.spacing(3), | ||||
| })); | ||||
| 
 | ||||
| enum ExpirationOption { | ||||
|     '7DAYS' = '7d', | ||||
|     '30DAYS' = '30d', | ||||
|     '60DAYS' = '60d', | ||||
|     NEVER = 'never', | ||||
|     CUSTOM = 'custom', | ||||
| } | ||||
| 
 | ||||
| const expirationOptions = [ | ||||
|     { | ||||
|         key: ExpirationOption['7DAYS'], | ||||
|         days: 7, | ||||
|         label: '7 days', | ||||
|     }, | ||||
|     { | ||||
|         key: ExpirationOption['30DAYS'], | ||||
|         days: 30, | ||||
|         label: '30 days', | ||||
|     }, | ||||
|     { | ||||
|         key: ExpirationOption['60DAYS'], | ||||
|         days: 60, | ||||
|         label: '60 days', | ||||
|     }, | ||||
|     { | ||||
|         key: ExpirationOption.NEVER, | ||||
|         label: 'Never', | ||||
|     }, | ||||
|     { | ||||
|         key: ExpirationOption.CUSTOM, | ||||
|         label: 'Custom', | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| enum ErrorField { | ||||
|     DESCRIPTION = 'description', | ||||
|     EXPIRES_AT = 'expiresAt', | ||||
| } | ||||
| 
 | ||||
| interface ICreatePersonalAPITokenErrors { | ||||
|     [ErrorField.DESCRIPTION]?: string; | ||||
|     [ErrorField.EXPIRES_AT]?: string; | ||||
| } | ||||
| const DEFAULT_EXPIRATION = ExpirationOption['30DAYS']; | ||||
| 
 | ||||
| interface ICreatePersonalAPITokenProps { | ||||
|     open: boolean; | ||||
| @ -137,50 +51,22 @@ export const CreatePersonalAPIToken: FC<ICreatePersonalAPITokenProps> = ({ | ||||
|     const { createPersonalAPIToken, loading } = usePersonalAPITokensApi(); | ||||
|     const { setToastApiError } = useToast(); | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const { locationSettings } = useLocationSettings(); | ||||
| 
 | ||||
|     const [description, setDescription] = useState(''); | ||||
|     const [expiration, setExpiration] = useState<ExpirationOption>( | ||||
|         ExpirationOption['30DAYS'] | ||||
|     const [expiration, setExpiration] = | ||||
|         useState<ExpirationOption>(DEFAULT_EXPIRATION); | ||||
|     const [expiresAt, setExpiresAt] = useState( | ||||
|         calculateExpirationDate(DEFAULT_EXPIRATION) | ||||
|     ); | ||||
|     const [errors, setErrors] = useState<ICreatePersonalAPITokenErrors>({}); | ||||
| 
 | ||||
|     const clearError = (field: ErrorField) => { | ||||
|         setErrors(errors => ({ ...errors, [field]: undefined })); | ||||
|     }; | ||||
| 
 | ||||
|     const setError = (field: ErrorField, error: string) => { | ||||
|         setErrors(errors => ({ ...errors, [field]: error })); | ||||
|     }; | ||||
| 
 | ||||
|     const calculateDate = () => { | ||||
|         const expiresAt = new Date(); | ||||
|         const expirationOption = expirationOptions.find( | ||||
|             ({ key }) => key === expiration | ||||
|         ); | ||||
|         if (expiration === ExpirationOption.NEVER) { | ||||
|             expiresAt.setFullYear(expiresAt.getFullYear() + 1000); | ||||
|         } else if (expiration === ExpirationOption.CUSTOM) { | ||||
|             expiresAt.setMinutes(expiresAt.getMinutes() + 30); | ||||
|         } else if (expirationOption?.days) { | ||||
|             expiresAt.setDate(expiresAt.getDate() + expirationOption.days); | ||||
|         } | ||||
|         return expiresAt; | ||||
|     }; | ||||
| 
 | ||||
|     const [expiresAt, setExpiresAt] = useState(calculateDate()); | ||||
|     const [errors, setErrors] = useState<IPersonalAPITokenFormErrors>({}); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setDescription(''); | ||||
|         setExpiration(DEFAULT_EXPIRATION); | ||||
|         setExpiresAt(calculateExpirationDate(DEFAULT_EXPIRATION)); | ||||
|         setErrors({}); | ||||
|         setExpiration(ExpirationOption['30DAYS']); | ||||
|     }, [open]); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         clearError(ErrorField.EXPIRES_AT); | ||||
|         setExpiresAt(calculateDate()); | ||||
|     }, [expiration]); | ||||
| 
 | ||||
|     const getPersonalAPITokenPayload = () => { | ||||
|         return { | ||||
|             description, | ||||
| @ -220,22 +106,6 @@ export const CreatePersonalAPIToken: FC<ICreatePersonalAPITokenProps> = ({ | ||||
|         isDescriptionUnique(description) && | ||||
|         expiresAt > new Date(); | ||||
| 
 | ||||
|     const onSetDescription = (description: string) => { | ||||
|         clearError(ErrorField.DESCRIPTION); | ||||
|         if (!isDescriptionUnique(description)) { | ||||
|             setError( | ||||
|                 ErrorField.DESCRIPTION, | ||||
|                 'A personal API token with that description already exists.' | ||||
|             ); | ||||
|         } | ||||
|         setDescription(description); | ||||
|     }; | ||||
| 
 | ||||
|     const customExpiration = expiration === ExpirationOption.CUSTOM; | ||||
| 
 | ||||
|     const neverExpires = | ||||
|         expiresAt.getFullYear() > new Date().getFullYear() + 100; | ||||
| 
 | ||||
|     return ( | ||||
|         <SidebarModal | ||||
|             open={open} | ||||
| @ -257,89 +127,16 @@ export const CreatePersonalAPIToken: FC<ICreatePersonalAPITokenProps> = ({ | ||||
|             > | ||||
|                 <StyledForm onSubmit={handleSubmit}> | ||||
|                     <div> | ||||
|                         <StyledInputDescription> | ||||
|                             Describe what this token will be used for | ||||
|                         </StyledInputDescription> | ||||
|                         <StyledInput | ||||
|                             autoFocus | ||||
|                             label="Description" | ||||
|                             error={Boolean(errors.description)} | ||||
|                             errorText={errors.description} | ||||
|                             value={description} | ||||
|                             onChange={e => onSetDescription(e.target.value)} | ||||
|                             required | ||||
|                         /> | ||||
|                         <StyledInputDescription> | ||||
|                             Token expiration date | ||||
|                         </StyledInputDescription> | ||||
|                         <StyledExpirationPicker custom={customExpiration}> | ||||
|                             <StyledSelectMenu | ||||
|                                 name="expiration" | ||||
|                                 id="expiration" | ||||
|                                 label="Token will expire in" | ||||
|                                 value={expiration} | ||||
|                                 onChange={e => | ||||
|                                     setExpiration( | ||||
|                                         e.target.value as ExpirationOption | ||||
|                                     ) | ||||
|                                 } | ||||
|                                 options={expirationOptions} | ||||
|                             /> | ||||
|                             <ConditionallyRender | ||||
|                                 condition={customExpiration} | ||||
|                                 show={() => ( | ||||
|                                     <StyledDateTimePicker | ||||
|                                         label="Date" | ||||
|                                         value={expiresAt} | ||||
|                                         onChange={date => { | ||||
|                                             clearError(ErrorField.EXPIRES_AT); | ||||
|                                             if (date < new Date()) { | ||||
|                                                 setError( | ||||
|                                                     ErrorField.EXPIRES_AT, | ||||
|                                                     'Invalid date, must be in the future' | ||||
|                                                 ); | ||||
|                                             } | ||||
|                                             setExpiresAt(date); | ||||
|                                         }} | ||||
|                                         min={new Date()} | ||||
|                                         error={Boolean(errors.expiresAt)} | ||||
|                                         errorText={errors.expiresAt} | ||||
|                                         required | ||||
|                                     /> | ||||
|                                 )} | ||||
|                                 elseShow={ | ||||
|                                     <ConditionallyRender | ||||
|                                         condition={neverExpires} | ||||
|                                         show={ | ||||
|                                             <Typography variant="body2"> | ||||
|                                                 The token will{' '} | ||||
|                                                 <strong>never</strong> expire! | ||||
|                                             </Typography> | ||||
|                                         } | ||||
|                                         elseShow={() => ( | ||||
|                                             <Typography variant="body2"> | ||||
|                                                 Token will expire on{' '} | ||||
|                                                 <strong> | ||||
|                                                     {formatDateYMD( | ||||
|                                                         expiresAt!, | ||||
|                                                         locationSettings.locale | ||||
|                                                     )} | ||||
|                                                 </strong> | ||||
|                                             </Typography> | ||||
|                                         )} | ||||
|                                     /> | ||||
|                                 } | ||||
|                             /> | ||||
|                         </StyledExpirationPicker> | ||||
|                         <ConditionallyRender | ||||
|                             condition={neverExpires} | ||||
|                             show={ | ||||
|                                 <StyledAlert severity="warning"> | ||||
|                                     We strongly recommend that you set an | ||||
|                                     expiration date for your token to help keep | ||||
|                                     your information secure. | ||||
|                                 </StyledAlert> | ||||
|                             } | ||||
|                         <PersonalAPITokenForm | ||||
|                             description={description} | ||||
|                             setDescription={setDescription} | ||||
|                             isDescriptionUnique={isDescriptionUnique} | ||||
|                             expiration={expiration} | ||||
|                             setExpiration={setExpiration} | ||||
|                             expiresAt={expiresAt} | ||||
|                             setExpiresAt={setExpiresAt} | ||||
|                             errors={errors} | ||||
|                             setErrors={setErrors} | ||||
|                         /> | ||||
|                     </div> | ||||
| 
 | ||||
|  | ||||
| @ -0,0 +1,254 @@ | ||||
| import Input from 'component/common/Input/Input'; | ||||
| import SelectMenu from 'component/common/select'; | ||||
| import { formatDateYMD } from 'utils/formatDate'; | ||||
| import { useLocationSettings } from 'hooks/useLocationSettings'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { DateTimePicker } from 'component/common/DateTimePicker/DateTimePicker'; | ||||
| import { Alert, styled, Typography } from '@mui/material'; | ||||
| import { useEffect } from 'react'; | ||||
| 
 | ||||
| const StyledInputDescription = styled('p')(({ theme }) => ({ | ||||
|     color: theme.palette.text.secondary, | ||||
|     marginBottom: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| const StyledInput = styled(Input)(({ theme }) => ({ | ||||
|     width: '100%', | ||||
|     maxWidth: theme.spacing(50), | ||||
|     marginBottom: theme.spacing(2), | ||||
| })); | ||||
| 
 | ||||
| const StyledExpirationPicker = styled('div')<{ custom?: boolean }>( | ||||
|     ({ theme, custom }) => ({ | ||||
|         display: 'flex', | ||||
|         alignItems: custom ? 'start' : 'center', | ||||
|         gap: theme.spacing(1.5), | ||||
|         marginBottom: theme.spacing(2), | ||||
|         [theme.breakpoints.down('sm')]: { | ||||
|             flexDirection: 'column', | ||||
|             alignItems: 'flex-start', | ||||
|         }, | ||||
|     }) | ||||
| ); | ||||
| 
 | ||||
| const StyledSelectMenu = styled(SelectMenu)(({ theme }) => ({ | ||||
|     minWidth: theme.spacing(20), | ||||
|     marginRight: theme.spacing(0.5), | ||||
|     [theme.breakpoints.down('sm')]: { | ||||
|         width: theme.spacing(50), | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledDateTimePicker = styled(DateTimePicker)(({ theme }) => ({ | ||||
|     width: theme.spacing(28), | ||||
|     [theme.breakpoints.down('sm')]: { | ||||
|         width: theme.spacing(50), | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledAlert = styled(Alert)(({ theme }) => ({ | ||||
|     marginBottom: theme.spacing(2), | ||||
|     maxWidth: theme.spacing(50), | ||||
| })); | ||||
| 
 | ||||
| export enum ExpirationOption { | ||||
|     '7DAYS' = '7d', | ||||
|     '30DAYS' = '30d', | ||||
|     '60DAYS' = '60d', | ||||
|     NEVER = 'never', | ||||
|     CUSTOM = 'custom', | ||||
| } | ||||
| 
 | ||||
| export const expirationOptions = [ | ||||
|     { | ||||
|         key: ExpirationOption['7DAYS'], | ||||
|         days: 7, | ||||
|         label: '7 days', | ||||
|     }, | ||||
|     { | ||||
|         key: ExpirationOption['30DAYS'], | ||||
|         days: 30, | ||||
|         label: '30 days', | ||||
|     }, | ||||
|     { | ||||
|         key: ExpirationOption['60DAYS'], | ||||
|         days: 60, | ||||
|         label: '60 days', | ||||
|     }, | ||||
|     { | ||||
|         key: ExpirationOption.NEVER, | ||||
|         label: 'Never', | ||||
|     }, | ||||
|     { | ||||
|         key: ExpirationOption.CUSTOM, | ||||
|         label: 'Custom', | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| export const calculateExpirationDate = (expiration: ExpirationOption) => { | ||||
|     const expiresAt = new Date(); | ||||
|     const expirationOption = expirationOptions.find( | ||||
|         ({ key }) => key === expiration | ||||
|     ); | ||||
|     if (expiration === ExpirationOption.NEVER) { | ||||
|         expiresAt.setFullYear(expiresAt.getFullYear() + 1000); | ||||
|     } else if (expiration === ExpirationOption.CUSTOM) { | ||||
|         expiresAt.setMinutes(expiresAt.getMinutes() + 30); | ||||
|     } else if (expirationOption?.days) { | ||||
|         expiresAt.setDate(expiresAt.getDate() + expirationOption.days); | ||||
|     } | ||||
|     return expiresAt; | ||||
| }; | ||||
| 
 | ||||
| enum ErrorField { | ||||
|     DESCRIPTION = 'description', | ||||
|     EXPIRES_AT = 'expiresAt', | ||||
| } | ||||
| 
 | ||||
| export interface IPersonalAPITokenFormErrors { | ||||
|     [ErrorField.DESCRIPTION]?: string; | ||||
|     [ErrorField.EXPIRES_AT]?: string; | ||||
| } | ||||
| 
 | ||||
| interface IPersonalAPITokenFormProps { | ||||
|     description: string; | ||||
|     setDescription: React.Dispatch<React.SetStateAction<string>>; | ||||
|     isDescriptionUnique?: (description: string) => boolean; | ||||
|     expiration: ExpirationOption; | ||||
|     setExpiration: (expiration: ExpirationOption) => void; | ||||
|     expiresAt: Date; | ||||
|     setExpiresAt: React.Dispatch<React.SetStateAction<Date>>; | ||||
|     errors: IPersonalAPITokenFormErrors; | ||||
|     setErrors: React.Dispatch< | ||||
|         React.SetStateAction<IPersonalAPITokenFormErrors> | ||||
|     >; | ||||
| } | ||||
| 
 | ||||
| export const PersonalAPITokenForm = ({ | ||||
|     description, | ||||
|     setDescription, | ||||
|     isDescriptionUnique, | ||||
|     expiration, | ||||
|     setExpiration, | ||||
|     expiresAt, | ||||
|     setExpiresAt, | ||||
|     errors, | ||||
|     setErrors, | ||||
| }: IPersonalAPITokenFormProps) => { | ||||
|     const { locationSettings } = useLocationSettings(); | ||||
| 
 | ||||
|     const clearError = (field: ErrorField) => { | ||||
|         setErrors(errors => ({ ...errors, [field]: undefined })); | ||||
|     }; | ||||
| 
 | ||||
|     const setError = (field: ErrorField, error: string) => { | ||||
|         setErrors(errors => ({ ...errors, [field]: error })); | ||||
|     }; | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         clearError(ErrorField.EXPIRES_AT); | ||||
|         setExpiresAt(calculateExpirationDate(expiration)); | ||||
|     }, [expiration]); | ||||
| 
 | ||||
|     const onSetDescription = (description: string) => { | ||||
|         clearError(ErrorField.DESCRIPTION); | ||||
|         if (isDescriptionUnique && !isDescriptionUnique(description)) { | ||||
|             setError( | ||||
|                 ErrorField.DESCRIPTION, | ||||
|                 'A personal API token with that description already exists.' | ||||
|             ); | ||||
|         } | ||||
|         setDescription(description); | ||||
|     }; | ||||
| 
 | ||||
|     const customExpiration = expiration === ExpirationOption.CUSTOM; | ||||
| 
 | ||||
|     const neverExpires = | ||||
|         expiresAt.getFullYear() > new Date().getFullYear() + 100; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <StyledInputDescription> | ||||
|                 Describe what this token will be used for | ||||
|             </StyledInputDescription> | ||||
|             <StyledInput | ||||
|                 autoFocus | ||||
|                 label="Description" | ||||
|                 error={Boolean(errors.description)} | ||||
|                 errorText={errors.description} | ||||
|                 value={description} | ||||
|                 onChange={e => onSetDescription(e.target.value)} | ||||
|                 required | ||||
|             /> | ||||
|             <StyledInputDescription> | ||||
|                 Token expiration date | ||||
|             </StyledInputDescription> | ||||
|             <StyledExpirationPicker custom={customExpiration}> | ||||
|                 <StyledSelectMenu | ||||
|                     name="expiration" | ||||
|                     id="expiration" | ||||
|                     label="Token will expire in" | ||||
|                     value={expiration} | ||||
|                     onChange={e => | ||||
|                         setExpiration(e.target.value as ExpirationOption) | ||||
|                     } | ||||
|                     options={expirationOptions} | ||||
|                 /> | ||||
|                 <ConditionallyRender | ||||
|                     condition={customExpiration} | ||||
|                     show={() => ( | ||||
|                         <StyledDateTimePicker | ||||
|                             label="Date" | ||||
|                             value={expiresAt} | ||||
|                             onChange={date => { | ||||
|                                 clearError(ErrorField.EXPIRES_AT); | ||||
|                                 if (date < new Date()) { | ||||
|                                     setError( | ||||
|                                         ErrorField.EXPIRES_AT, | ||||
|                                         'Invalid date, must be in the future' | ||||
|                                     ); | ||||
|                                 } | ||||
|                                 setExpiresAt(date); | ||||
|                             }} | ||||
|                             min={new Date()} | ||||
|                             error={Boolean(errors.expiresAt)} | ||||
|                             errorText={errors.expiresAt} | ||||
|                             required | ||||
|                         /> | ||||
|                     )} | ||||
|                     elseShow={ | ||||
|                         <ConditionallyRender | ||||
|                             condition={neverExpires} | ||||
|                             show={ | ||||
|                                 <Typography variant="body2"> | ||||
|                                     The token will <strong>never</strong>{' '} | ||||
|                                     expire! | ||||
|                                 </Typography> | ||||
|                             } | ||||
|                             elseShow={() => ( | ||||
|                                 <Typography variant="body2"> | ||||
|                                     Token will expire on{' '} | ||||
|                                     <strong> | ||||
|                                         {formatDateYMD( | ||||
|                                             expiresAt!, | ||||
|                                             locationSettings.locale | ||||
|                                         )} | ||||
|                                     </strong> | ||||
|                                 </Typography> | ||||
|                             )} | ||||
|                         /> | ||||
|                     } | ||||
|                 /> | ||||
|             </StyledExpirationPicker> | ||||
|             <ConditionallyRender | ||||
|                 condition={neverExpires} | ||||
|                 show={ | ||||
|                     <StyledAlert severity="warning"> | ||||
|                         We strongly recommend that you set an expiration date | ||||
|                         for your token to help keep your information secure. | ||||
|                     </StyledAlert> | ||||
|                 } | ||||
|             /> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user