Skip to content

Commit

Permalink
Refactor do token validation (#454)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
D-B-Hawk authored Feb 6, 2024
1 parent 4014ec5 commit 16776bd
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 13 deletions.
40 changes: 29 additions & 11 deletions containers/clusterForms/shared/authForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -53,9 +54,6 @@ const AuthForm: FunctionComponent = () => {
githubUserOrganizations,
gitlabUser,
gitlabGroups,
doUser,
doStateLoading,
doTokenValid,
gitStateLoading,
installationType,
isGitSelected,
Expand Down Expand Up @@ -90,7 +88,7 @@ const AuthForm: FunctionComponent = () => {
formState: { errors },
} = useFormContext<InstallValues>();

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]);

Expand Down Expand Up @@ -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<boolean>((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],
Expand Down Expand Up @@ -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}
/>
) : (
<ControlledPassword
Expand All @@ -341,7 +359,7 @@ const AuthForm: FunctionComponent = () => {
helperText={helperText}
required
rules={{
required: true,
required: 'Required.',
}}
/>
),
Expand Down
2 changes: 0 additions & 2 deletions containers/clusterManagement/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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';

Expand Down
30 changes: 30 additions & 0 deletions hooks/useDebouncedPromise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useEffect, useState } from 'react';

export function useDebouncedPromise<T, P>(
func: (param: P) => Promise<T>,
delay: number,
): (param: P) => Promise<T> {
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | null>(null);

useEffect(() => {
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [timeoutId]);

return (param: P) => {
return new Promise<T>((resolve, reject) => {
if (timeoutId) {
clearTimeout(timeoutId);
}

const newTimeoutId = setTimeout(() => {
func(param).then(resolve).catch(reject);
}, delay);

setTimeoutId(newTimeoutId);
});
};
}

0 comments on commit 16776bd

Please sign in to comment.