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 FormTemplate from 'component/common/FormTemplate/FormTemplate'; | ||||||
| import { SidebarModal } from 'component/common/SidebarModal/SidebarModal'; | import { SidebarModal } from 'component/common/SidebarModal/SidebarModal'; | ||||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | 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 { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
| import { usePersonalAPITokens } from 'hooks/api/getters/usePersonalAPITokens/usePersonalAPITokens'; | import { usePersonalAPITokens } from 'hooks/api/getters/usePersonalAPITokens/usePersonalAPITokens'; | ||||||
| import { usePersonalAPITokensApi } from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi'; | 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 { INewPersonalAPIToken } from 'interfaces/personalAPIToken'; | ||||||
| import { DateTimePicker } from 'component/common/DateTimePicker/DateTimePicker'; | import { | ||||||
|  |     calculateExpirationDate, | ||||||
|  |     ExpirationOption, | ||||||
|  |     IPersonalAPITokenFormErrors, | ||||||
|  |     PersonalAPITokenForm, | ||||||
|  | } from './PersonalAPITokenForm/PersonalAPITokenForm'; | ||||||
| 
 | 
 | ||||||
| const StyledForm = styled('form')(() => ({ | const StyledForm = styled('form')(() => ({ | ||||||
|     display: 'flex', |     display: 'flex', | ||||||
| @ -21,50 +21,6 @@ const StyledForm = styled('form')(() => ({ | |||||||
|     height: '100%', |     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 }) => ({ | const StyledButtonContainer = styled('div')(({ theme }) => ({ | ||||||
|     marginTop: 'auto', |     marginTop: 'auto', | ||||||
|     display: 'flex', |     display: 'flex', | ||||||
| @ -78,49 +34,7 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({ | |||||||
|     marginLeft: theme.spacing(3), |     marginLeft: theme.spacing(3), | ||||||
| })); | })); | ||||||
| 
 | 
 | ||||||
| enum ExpirationOption { | const DEFAULT_EXPIRATION = ExpirationOption['30DAYS']; | ||||||
|     '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; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| interface ICreatePersonalAPITokenProps { | interface ICreatePersonalAPITokenProps { | ||||||
|     open: boolean; |     open: boolean; | ||||||
| @ -137,50 +51,22 @@ export const CreatePersonalAPIToken: FC<ICreatePersonalAPITokenProps> = ({ | |||||||
|     const { createPersonalAPIToken, loading } = usePersonalAPITokensApi(); |     const { createPersonalAPIToken, loading } = usePersonalAPITokensApi(); | ||||||
|     const { setToastApiError } = useToast(); |     const { setToastApiError } = useToast(); | ||||||
|     const { uiConfig } = useUiConfig(); |     const { uiConfig } = useUiConfig(); | ||||||
|     const { locationSettings } = useLocationSettings(); |  | ||||||
| 
 | 
 | ||||||
|     const [description, setDescription] = useState(''); |     const [description, setDescription] = useState(''); | ||||||
|     const [expiration, setExpiration] = useState<ExpirationOption>( |     const [expiration, setExpiration] = | ||||||
|         ExpirationOption['30DAYS'] |         useState<ExpirationOption>(DEFAULT_EXPIRATION); | ||||||
|  |     const [expiresAt, setExpiresAt] = useState( | ||||||
|  |         calculateExpirationDate(DEFAULT_EXPIRATION) | ||||||
|     ); |     ); | ||||||
|     const [errors, setErrors] = useState<ICreatePersonalAPITokenErrors>({}); |     const [errors, setErrors] = useState<IPersonalAPITokenFormErrors>({}); | ||||||
| 
 |  | ||||||
|     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()); |  | ||||||
| 
 | 
 | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         setDescription(''); |         setDescription(''); | ||||||
|  |         setExpiration(DEFAULT_EXPIRATION); | ||||||
|  |         setExpiresAt(calculateExpirationDate(DEFAULT_EXPIRATION)); | ||||||
|         setErrors({}); |         setErrors({}); | ||||||
|         setExpiration(ExpirationOption['30DAYS']); |  | ||||||
|     }, [open]); |     }, [open]); | ||||||
| 
 | 
 | ||||||
|     useEffect(() => { |  | ||||||
|         clearError(ErrorField.EXPIRES_AT); |  | ||||||
|         setExpiresAt(calculateDate()); |  | ||||||
|     }, [expiration]); |  | ||||||
| 
 |  | ||||||
|     const getPersonalAPITokenPayload = () => { |     const getPersonalAPITokenPayload = () => { | ||||||
|         return { |         return { | ||||||
|             description, |             description, | ||||||
| @ -220,22 +106,6 @@ export const CreatePersonalAPIToken: FC<ICreatePersonalAPITokenProps> = ({ | |||||||
|         isDescriptionUnique(description) && |         isDescriptionUnique(description) && | ||||||
|         expiresAt > new Date(); |         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 ( |     return ( | ||||||
|         <SidebarModal |         <SidebarModal | ||||||
|             open={open} |             open={open} | ||||||
| @ -257,89 +127,16 @@ export const CreatePersonalAPIToken: FC<ICreatePersonalAPITokenProps> = ({ | |||||||
|             > |             > | ||||||
|                 <StyledForm onSubmit={handleSubmit}> |                 <StyledForm onSubmit={handleSubmit}> | ||||||
|                     <div> |                     <div> | ||||||
|                         <StyledInputDescription> |                         <PersonalAPITokenForm | ||||||
|                             Describe what this token will be used for |                             description={description} | ||||||
|                         </StyledInputDescription> |                             setDescription={setDescription} | ||||||
|                         <StyledInput |                             isDescriptionUnique={isDescriptionUnique} | ||||||
|                             autoFocus |                             expiration={expiration} | ||||||
|                             label="Description" |                             setExpiration={setExpiration} | ||||||
|                             error={Boolean(errors.description)} |                             expiresAt={expiresAt} | ||||||
|                             errorText={errors.description} |                             setExpiresAt={setExpiresAt} | ||||||
|                             value={description} |                             errors={errors} | ||||||
|                             onChange={e => onSetDescription(e.target.value)} |                             setErrors={setErrors} | ||||||
|                             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> |  | ||||||
|                             } |  | ||||||
|                         /> |                         /> | ||||||
|                     </div> |                     </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