From 4005bb8a8be858112bdbc9d24af7abe8df50e155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Wed, 4 Jan 2023 14:51:30 +0000 Subject: [PATCH] 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. --- .../CreatePersonalAPIToken.tsx | 253 ++--------------- .../PersonalAPITokenForm.tsx | 254 ++++++++++++++++++ 2 files changed, 279 insertions(+), 228 deletions(-) create mode 100644 frontend/src/component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/PersonalAPITokenForm/PersonalAPITokenForm.tsx diff --git a/frontend/src/component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/CreatePersonalAPIToken.tsx b/frontend/src/component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/CreatePersonalAPIToken.tsx index 87c87caf48..d42f7898e2 100644 --- a/frontend/src/component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/CreatePersonalAPIToken.tsx +++ b/frontend/src/component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/CreatePersonalAPIToken.tsx @@ -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 = ({ const { createPersonalAPIToken, loading } = usePersonalAPITokensApi(); const { setToastApiError } = useToast(); const { uiConfig } = useUiConfig(); - const { locationSettings } = useLocationSettings(); const [description, setDescription] = useState(''); - const [expiration, setExpiration] = useState( - ExpirationOption['30DAYS'] + const [expiration, setExpiration] = + useState(DEFAULT_EXPIRATION); + const [expiresAt, setExpiresAt] = useState( + calculateExpirationDate(DEFAULT_EXPIRATION) ); - const [errors, setErrors] = useState({}); - - 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({}); 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 = ({ 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 ( = ({ >
- - Describe what this token will be used for - - onSetDescription(e.target.value)} - required - /> - - Token expiration date - - - - setExpiration( - e.target.value as ExpirationOption - ) - } - options={expirationOptions} - /> - ( - { - 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={ - - The token will{' '} - never expire! - - } - elseShow={() => ( - - Token will expire on{' '} - - {formatDateYMD( - expiresAt!, - locationSettings.locale - )} - - - )} - /> - } - /> - - - We strongly recommend that you set an - expiration date for your token to help keep - your information secure. - - } +
diff --git a/frontend/src/component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/PersonalAPITokenForm/PersonalAPITokenForm.tsx b/frontend/src/component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/PersonalAPITokenForm/PersonalAPITokenForm.tsx new file mode 100644 index 0000000000..de8f854b9e --- /dev/null +++ b/frontend/src/component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/PersonalAPITokenForm/PersonalAPITokenForm.tsx @@ -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>; + isDescriptionUnique?: (description: string) => boolean; + expiration: ExpirationOption; + setExpiration: (expiration: ExpirationOption) => void; + expiresAt: Date; + setExpiresAt: React.Dispatch>; + errors: IPersonalAPITokenFormErrors; + setErrors: React.Dispatch< + React.SetStateAction + >; +} + +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 ( + <> + + Describe what this token will be used for + + onSetDescription(e.target.value)} + required + /> + + Token expiration date + + + + setExpiration(e.target.value as ExpirationOption) + } + options={expirationOptions} + /> + ( + { + 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={ + + The token will never{' '} + expire! + + } + elseShow={() => ( + + Token will expire on{' '} + + {formatDateYMD( + expiresAt!, + locationSettings.locale + )} + + + )} + /> + } + /> + + + We strongly recommend that you set an expiration date + for your token to help keep your information secure. + + } + /> + + ); +};