From 16776bd0fdbac226b07bb1dd7fd220ac0d6b0768 Mon Sep 17 00:00:00 2001 From: Derrick <76881293+D-B-Hawk@users.noreply.github.com> Date: Tue, 6 Feb 2024 15:49:40 -0800 Subject: [PATCH] Refactor do token validation (#454) * add useDebouncedPromise hook * validate do token with a debounced call so users can type with a delay of requests to digital ocean. when textbox is empty we will clear the state of the field which will allow user to view helper text --- .../clusterForms/shared/authForm/index.tsx | 40 ++++++++++++++----- containers/clusterManagement/index.tsx | 2 - hooks/useDebouncedPromise.ts | 30 ++++++++++++++ 3 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 hooks/useDebouncedPromise.ts diff --git a/containers/clusterForms/shared/authForm/index.tsx b/containers/clusterForms/shared/authForm/index.tsx index 8f456e78..c38a2475 100644 --- a/containers/clusterForms/shared/authForm/index.tsx +++ b/containers/clusterForms/shared/authForm/index.tsx @@ -41,6 +41,7 @@ import Column from '@/components/column'; import { hasProjectId } from '@/utils/hasProjectId'; import { getDigitalOceanUser } from '@/redux/thunks/digitalOcean.thunk'; import { GIT_PROVIDER_DISPLAY_NAME } from '@/constants'; +import { useDebouncedPromise } from '@/hooks/useDebouncedPromise'; const AuthForm: FunctionComponent = () => { const [showGoogleKeyFile, setShowGoogleKeyFile] = useState(false); @@ -53,9 +54,6 @@ const AuthForm: FunctionComponent = () => { githubUserOrganizations, gitlabUser, gitlabGroups, - doUser, - doStateLoading, - doTokenValid, gitStateLoading, installationType, isGitSelected, @@ -90,7 +88,7 @@ const AuthForm: FunctionComponent = () => { formState: { errors }, } = useFormContext(); - const [googleKeyFile, doToken] = watch(['google_auth.key_file', 'do_auth.token']); + const [googleKeyFile] = watch(['google_auth.key_file']); const isGitHub = useMemo(() => gitProvider === GitProvider.GITHUB, [gitProvider]); @@ -143,13 +141,30 @@ const AuthForm: FunctionComponent = () => { setShowGoogleKeyFile(e.target.checked); }, []); - const validateDoToken = useCallback( + const validateDOToken = useCallback( (token: string) => { - dispatch(getDigitalOceanUser(token)); + if (token) { + return dispatch(getDigitalOceanUser(token)) + .unwrap() + .then(() => true) + .catch(() => false); + } + return new Promise((resolve) => resolve(true)); }, [dispatch], ); + const debouncedDOTokenValidate = useDebouncedPromise(validateDOToken, 1000); + + const handleDoTokenBlur = useCallback( + (value: string) => { + if (!value) { + resetField('do_auth.token', { keepError: false, keepDirty: false, keepTouched: false }); + } + }, + [resetField], + ); + const gitLabel = useMemo( () => GIT_PROVIDER_DISPLAY_NAME[gitProvider as GitProvider], [gitProvider], @@ -324,11 +339,14 @@ const AuthForm: FunctionComponent = () => { helperText={helperText} required rules={{ - required: true, + required: 'Required.', + validate: { + validDOToken: async (token) => + (await debouncedDOTokenValidate(token as string)) || 'Invalid token.', + }, }} - onChange={validateDoToken} - error={!!doToken && !doUser && !doStateLoading && !doTokenValid} - onErrorText="Invalid Token" + onBlur={handleDoTokenBlur} + onErrorText={errors.do_auth?.token?.message} /> ) : ( { helperText={helperText} required rules={{ - required: true, + required: 'Required.', }} /> ), diff --git a/containers/clusterManagement/index.tsx b/containers/clusterManagement/index.tsx index c479c9a3..5b9c0c96 100644 --- a/containers/clusterManagement/index.tsx +++ b/containers/clusterManagement/index.tsx @@ -1,6 +1,5 @@ 'use client'; import React, { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'; -import axios from 'axios'; import Box from '@mui/material/Box'; import Tabs from '@mui/material/Tabs'; import Joyride, { ACTIONS, CallBackProps } from 'react-joyride'; @@ -54,7 +53,6 @@ import { getClusterTourStatus, updateClusterTourStatus } from '@/redux/thunks/se import usePaywall from '@/hooks/usePaywall'; import UpgradeModal from '@/components/upgradeModal'; import { selectUpgradeLicenseDefinition } from '@/redux/selectors/subscription.selector'; -import { getCloudProviderAuth } from '@/utils/getCloudProviderAuth'; import KubeConfigModal from '@/components/kubeConfigModal'; import { createNotification } from '@/redux/slices/notifications.slice'; diff --git a/hooks/useDebouncedPromise.ts b/hooks/useDebouncedPromise.ts new file mode 100644 index 00000000..7e155ef7 --- /dev/null +++ b/hooks/useDebouncedPromise.ts @@ -0,0 +1,30 @@ +import { useEffect, useState } from 'react'; + +export function useDebouncedPromise( + func: (param: P) => Promise, + delay: number, +): (param: P) => Promise { + const [timeoutId, setTimeoutId] = useState(null); + + useEffect(() => { + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [timeoutId]); + + return (param: P) => { + return new Promise((resolve, reject) => { + if (timeoutId) { + clearTimeout(timeoutId); + } + + const newTimeoutId = setTimeout(() => { + func(param).then(resolve).catch(reject); + }, delay); + + setTimeoutId(newTimeoutId); + }); + }; +}