From 92e2b1890cc53950dec71b6fdfd2586afbc33664 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Tue, 7 Nov 2023 09:19:55 +0100 Subject: [PATCH] Refactor/project overview api calls (#5279) This PR reduces the overhead of making API calls on pages with heavy renders. We forego loading states and default error handling in favor of more speed by avoiding triggering multiple re-renders from the API call. --- .../FeatureToggleListTable.tsx | 20 +++-- .../feature/FeatureView/FeatureView.tsx | 16 ++-- .../src/component/project/Project/Project.tsx | 16 ++-- .../LegacyProjectFeatureToggles.tsx | 21 +++-- .../ProjectFeatureToggles.tsx | 18 ++-- .../project/ProjectCard/ProjectCard.tsx | 16 ++-- .../src/hooks/api/actions/useApi/useApi.ts | 82 ++++++++++++++++++- .../useFavoriteFeaturesApi.ts | 10 +-- .../useFavoriteProjectsApi.ts | 10 +-- .../actions/useFeatureApi/useFeatureApi.ts | 15 ++-- 10 files changed, 171 insertions(+), 53 deletions(-) diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index c6a4278edd..cacf524a9f 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -38,6 +38,7 @@ import { ExportDialog } from './ExportDialog'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { focusable } from 'themes/themeStyles'; import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell'; +import useToast from 'hooks/useToast'; export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({ name: 'Name of the feature', @@ -69,6 +70,7 @@ export const FeatureToggleListTable: VFC = () => { const [showExportDialog, setShowExportDialog] = useState(false); const { features = [], loading, refetchFeatures } = useFeatures(); const [searchParams, setSearchParams] = useSearchParams(); + const { setToastApiError } = useToast(); const { uiConfig } = useUiConfig(); const showEnvironmentLastSeen = Boolean( uiConfig.flags.lastSeenByEnvironment, @@ -97,14 +99,20 @@ export const FeatureToggleListTable: VFC = () => { const { favorite, unfavorite } = useFavoriteFeaturesApi(); const onFavorite = useCallback( async (feature: any) => { - if (feature?.favorite) { - await unfavorite(feature.project, feature.name); - } else { - await favorite(feature.project, feature.name); + try { + if (feature?.favorite) { + await unfavorite(feature.project, feature.name); + } else { + await favorite(feature.project, feature.name); + } + refetchFeatures(); + } catch (error) { + setToastApiError( + 'Something went wrong, could not update favorite', + ); } - refetchFeatures(); }, - [favorite, refetchFeatures, unfavorite], + [favorite, refetchFeatures, unfavorite, setToastApiError], ); const columns = useMemo( diff --git a/frontend/src/component/feature/FeatureView/FeatureView.tsx b/frontend/src/component/feature/FeatureView/FeatureView.tsx index 2deeed8c08..80215d4f24 100644 --- a/frontend/src/component/feature/FeatureView/FeatureView.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureView.tsx @@ -144,7 +144,7 @@ export const FeatureView = () => { const { favorite, unfavorite } = useFavoriteFeaturesApi(); const { refetchFeature } = useFeature(projectId, featureId); const dependentFeatures = useUiFlag('dependentFeatures'); - const { setToastData } = useToast(); + const { setToastData, setToastApiError } = useToast(); const [openTagDialog, setOpenTagDialog] = useState(false); const [showDelDialog, setShowDelDialog] = useState(false); @@ -187,12 +187,16 @@ export const FeatureView = () => { tabData.find((tab) => tab.path === pathname) ?? tabData[0]; const onFavorite = async () => { - if (feature?.favorite) { - await unfavorite(projectId, feature.name); - } else { - await favorite(projectId, feature.name); + try { + if (feature?.favorite) { + await unfavorite(projectId, feature.name); + } else { + await favorite(projectId, feature.name); + } + refetchFeature(); + } catch (error) { + setToastApiError('Something went wrong, could not update favorite'); } - refetchFeature(); }; if (status === 404) { diff --git a/frontend/src/component/project/Project/Project.tsx b/frontend/src/component/project/Project/Project.tsx index 8bb4b71bdd..f84c3eeb41 100644 --- a/frontend/src/component/project/Project/Project.tsx +++ b/frontend/src/component/project/Project/Project.tsx @@ -64,7 +64,7 @@ export const Project = () => { const params = useQueryParams(); const { project, loading, error, refetch } = useProject(projectId); const ref = useLoading(loading); - const { setToastData } = useToast(); + const { setToastData, setToastApiError } = useToast(); const [modalOpen, setModalOpen] = useState(false); const navigate = useNavigate(); const { pathname } = useLocation(); @@ -155,12 +155,16 @@ export const Project = () => { } const onFavorite = async () => { - if (project?.favorite) { - await unfavorite(projectId); - } else { - await favorite(projectId); + try { + if (project?.favorite) { + await unfavorite(projectId); + } else { + await favorite(projectId); + } + refetch(); + } catch (error) { + setToastApiError('Something went wrong, could not update favorite'); } - refetch(); }; const enterpriseIcon = ( diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/LegacyProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/LegacyProjectFeatureToggles.tsx index 2b00ce23c1..37b3dded05 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/LegacyProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/LegacyProjectFeatureToggles.tsx @@ -65,6 +65,7 @@ import { RowSelectCell } from './RowSelectCell/RowSelectCell'; import { BatchSelectionActionsBar } from '../../../common/BatchSelectionActionsBar/BatchSelectionActionsBar'; import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions'; import { FeatureEnvironmentSeenCell } from '../../../common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell'; +import useToast from 'hooks/useToast'; const StyledResponsiveButton = styled(ResponsiveButton)(() => ({ whiteSpace: 'nowrap', @@ -135,7 +136,7 @@ export const ProjectFeatureToggles = ({ string | undefined >(); const projectId = useRequiredPathParam('projectId'); - + const { setToastApiError } = useToast(); const { value: storedParams, setValue: setStoredParams } = createLocalStorage( `${projectId}:FeatureToggleListTable:v1`, @@ -171,14 +172,20 @@ export const ProjectFeatureToggles = ({ const onFavorite = useCallback( async (feature: IFeatureToggleListItem) => { - if (feature?.favorite) { - await unfavorite(projectId, feature.name); - } else { - await favorite(projectId, feature.name); + try { + if (feature?.favorite) { + await unfavorite(projectId, feature.name); + } else { + await favorite(projectId, feature.name); + } + refetch(); + } catch (error) { + setToastApiError( + 'Something went wrong, could not update favorite', + ); } - refetch(); }, - [projectId, refetch], + [projectId, refetch, setToastApiError], ); const showTagsColumn = useMemo( diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx index 1a0930426c..103e97ab39 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -63,6 +63,7 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; import { ListItemType } from './ProjectFeatureToggles.types'; import { createFeatureToggleCell } from './FeatureToggleSwitch/createFeatureToggleCell'; import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch'; +import useToast from 'hooks/useToast'; const StyledResponsiveButton = styled(ResponsiveButton)(() => ({ whiteSpace: 'nowrap', @@ -91,6 +92,7 @@ export const ProjectFeatureToggles = ({ }: IProjectFeatureTogglesProps) => { const { classes: styles } = useStyles(); const theme = useTheme(); + const { setToastApiError } = useToast(); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const [strategiesDialogState, setStrategiesDialogState] = useState({ open: false, @@ -138,12 +140,18 @@ export const ProjectFeatureToggles = ({ const onFavorite = useCallback( async (feature: IFeatureToggleListItem) => { - if (feature?.favorite) { - await unfavorite(projectId, feature.name); - } else { - await favorite(projectId, feature.name); + try { + if (feature?.favorite) { + await unfavorite(projectId, feature.name); + } else { + await favorite(projectId, feature.name); + } + onChange(); + } catch (error) { + setToastApiError( + 'Something went wrong, could not update favorite', + ); } - onChange(); }, [projectId, onChange], ); diff --git a/frontend/src/component/project/ProjectCard/ProjectCard.tsx b/frontend/src/component/project/ProjectCard/ProjectCard.tsx index 8be8412ed2..52ff4a5eac 100644 --- a/frontend/src/component/project/ProjectCard/ProjectCard.tsx +++ b/frontend/src/component/project/ProjectCard/ProjectCard.tsx @@ -27,6 +27,7 @@ import { StyledDivInfoContainer, StyledParagraphInfo, } from './ProjectCard.styles'; +import useToast from 'hooks/useToast'; interface IProjectCardProps { name: string; @@ -48,6 +49,7 @@ export const ProjectCard = ({ isFavorite = false, }: IProjectCardProps) => { const { hasAccess } = useContext(AccessContext); + const { setToastApiError } = useToast(); const { isOss } = useUiConfig(); const [anchorEl, setAnchorEl] = useState(null); const [showDelDialog, setShowDelDialog] = useState(false); @@ -62,12 +64,16 @@ export const ProjectCard = ({ const onFavorite = async (e: React.SyntheticEvent) => { e.preventDefault(); - if (isFavorite) { - await unfavorite(id); - } else { - await favorite(id); + try { + if (isFavorite) { + await unfavorite(id); + } else { + await favorite(id); + } + refetch(); + } catch (error) { + setToastApiError('Something went wrong, could not update favorite'); } - refetch(); }; return ( diff --git a/frontend/src/hooks/api/actions/useApi/useApi.ts b/frontend/src/hooks/api/actions/useApi/useApi.ts index ef7d5f60b1..b8ff9c6628 100644 --- a/frontend/src/hooks/api/actions/useApi/useApi.ts +++ b/frontend/src/hooks/api/actions/useApi/useApi.ts @@ -24,6 +24,13 @@ type ApiErrorHandler = ( requestId: string, ) => void; +type ApiCaller = () => Promise; +type RequestFunction = ( + apiCaller: ApiCaller, + requestId: string, + loadingOn?: boolean, +) => Promise; + interface IUseAPI { handleBadRequest?: ApiErrorHandler; handleNotFound?: ApiErrorHandler; @@ -33,6 +40,29 @@ interface IUseAPI { propagateErrors?: boolean; } +const timeApiCallStart = (requestId: string) => { + // Store the start time in milliseconds + console.log(`Starting timing for request: ${requestId}`); + return Date.now(); +}; + +const timeApiCallEnd = (startTime: number, requestId: string) => { + // Calculate the end time and subtract the start time + const endTime = Date.now(); + const duration = endTime - startTime; + console.log(`Timing for request ${requestId}: ${duration} ms`); + + if (duration > 500) { + console.error( + 'API call took over 500ms. This may indicate a rendering performance problem in your React component.', + requestId, + duration, + ); + } + + return duration; +}; + const useAPI = ({ handleBadRequest, handleNotFound, @@ -157,6 +187,27 @@ const useAPI = ({ ], ); + const requestWithTimer = (requestFunction: RequestFunction) => { + return async ( + apiCaller: () => Promise, + requestId: string, + loadingOn: boolean = true, + ) => { + const start = timeApiCallStart( + requestId || `Uknown request happening on ${apiCaller}`, + ); + + const res = await requestFunction(apiCaller, requestId, loadingOn); + + timeApiCallEnd( + start, + requestId || `Uknown request happening on ${apiCaller}`, + ); + + return res; + }; + }; + const makeRequest = useCallback( async ( apiCaller: () => Promise, @@ -187,6 +238,27 @@ const useAPI = ({ [handleResponses], ); + const makeLightRequest = useCallback( + async ( + apiCaller: () => Promise, + requestId: string, + loadingOn: boolean = true, + ): Promise => { + try { + const res = await apiCaller(); + + if (!res.ok) { + throw new Error(); + } + + return res; + } catch (e) { + throw new Error('Could not make request | makeLightRequest'); + } + }, + [], + ); + const createRequest = useCallback( (path: string, options: any, requestId: string = '') => { const defaultOptions: RequestInit = { @@ -207,9 +279,17 @@ const useAPI = ({ [], ); + const makeRequestWithTimer = requestWithTimer(makeRequest); + const makeLightRequestWithTimer = requestWithTimer(makeLightRequest); + + const isDevelopment = process.env.NODE_ENV === 'development'; + return { loading, - makeRequest, + makeRequest: isDevelopment ? makeRequestWithTimer : makeRequest, + makeLightRequest: isDevelopment + ? makeLightRequestWithTimer + : makeLightRequest, createRequest, errors, }; diff --git a/frontend/src/hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi.ts b/frontend/src/hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi.ts index 6b35adb9ea..38a30b3c5a 100644 --- a/frontend/src/hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi.ts +++ b/frontend/src/hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi.ts @@ -5,7 +5,7 @@ import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import useAPI from '../useApi/useApi'; export const useFavoriteFeaturesApi = () => { - const { makeRequest, createRequest, errors, loading } = useAPI({ + const { makeLightRequest, createRequest, errors, loading } = useAPI({ propagateErrors: true, }); const { setToastData, setToastApiError } = useToast(); @@ -21,7 +21,7 @@ export const useFavoriteFeaturesApi = () => { ); try { - await makeRequest(req.caller, req.id); + await makeLightRequest(req.caller, req.id); setToastData({ title: 'Toggle added to favorites', @@ -36,7 +36,7 @@ export const useFavoriteFeaturesApi = () => { setToastApiError(formatUnknownError(error)); } }, - [createRequest, makeRequest], + [createRequest, makeLightRequest], ); const unfavorite = useCallback( @@ -49,7 +49,7 @@ export const useFavoriteFeaturesApi = () => { ); try { - await makeRequest(req.caller, req.id); + await makeLightRequest(req.caller, req.id); setToastData({ title: 'Toggle removed from favorites', @@ -64,7 +64,7 @@ export const useFavoriteFeaturesApi = () => { setToastApiError(formatUnknownError(error)); } }, - [createRequest, makeRequest], + [createRequest, makeLightRequest], ); return { diff --git a/frontend/src/hooks/api/actions/useFavoriteProjectsApi/useFavoriteProjectsApi.ts b/frontend/src/hooks/api/actions/useFavoriteProjectsApi/useFavoriteProjectsApi.ts index b749006e10..09ac4bbcfb 100644 --- a/frontend/src/hooks/api/actions/useFavoriteProjectsApi/useFavoriteProjectsApi.ts +++ b/frontend/src/hooks/api/actions/useFavoriteProjectsApi/useFavoriteProjectsApi.ts @@ -5,7 +5,7 @@ import useAPI from '../useApi/useApi'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; export const useFavoriteProjectsApi = () => { - const { makeRequest, createRequest, errors, loading } = useAPI({ + const { makeLightRequest, createRequest, errors, loading } = useAPI({ propagateErrors: true, }); const { setToastData, setToastApiError } = useToast(); @@ -21,7 +21,7 @@ export const useFavoriteProjectsApi = () => { ); try { - await makeRequest(req.caller, req.id); + await makeLightRequest(req.caller, req.id); setToastData({ title: 'Project added to favorites', @@ -36,7 +36,7 @@ export const useFavoriteProjectsApi = () => { setToastApiError(formatUnknownError(error)); } }, - [createRequest, makeRequest], + [createRequest, makeLightRequest], ); const unfavorite = useCallback( @@ -49,7 +49,7 @@ export const useFavoriteProjectsApi = () => { ); try { - await makeRequest(req.caller, req.id); + await makeLightRequest(req.caller, req.id); setToastData({ title: 'Project removed from favorites', @@ -64,7 +64,7 @@ export const useFavoriteProjectsApi = () => { setToastApiError(formatUnknownError(error)); } }, - [createRequest, makeRequest], + [createRequest, makeLightRequest], ); return { diff --git a/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts b/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts index 83f38e41a1..92a67af066 100644 --- a/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts +++ b/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts @@ -7,9 +7,10 @@ import useAPI from '../useApi/useApi'; import { IFeatureVariant } from 'interfaces/featureToggle'; const useFeatureApi = () => { - const { makeRequest, createRequest, errors, loading } = useAPI({ - propagateErrors: true, - }); + const { makeRequest, makeLightRequest, createRequest, errors, loading } = + useAPI({ + propagateErrors: true, + }); const validateFeatureToggleName = async ( name: string | undefined, @@ -61,9 +62,9 @@ const useFeatureApi = () => { 'toggleFeatureEnvironmentOn', ); - return makeRequest(req.caller, req.id); + return makeLightRequest(req.caller, req.id); }, - [createRequest, makeRequest], + [createRequest, makeLightRequest], ); const bulkToggleFeaturesEnvironmentOn = useCallback( @@ -119,9 +120,9 @@ const useFeatureApi = () => { 'toggleFeatureEnvironmentOff', ); - return makeRequest(req.caller, req.id); + return makeLightRequest(req.caller, req.id); }, - [createRequest, makeRequest], + [createRequest, makeLightRequest], ); const changeFeatureProject = async (