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 (