1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-07 01:16:28 +02:00

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.
This commit is contained in:
Fredrik Strand Oseberg 2023-11-07 09:19:55 +01:00 committed by GitHub
parent 312999066b
commit 92e2b1890c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 171 additions and 53 deletions

View File

@ -38,6 +38,7 @@ import { ExportDialog } from './ExportDialog';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { focusable } from 'themes/themeStyles'; import { focusable } from 'themes/themeStyles';
import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell'; import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
import useToast from 'hooks/useToast';
export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({ export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
name: 'Name of the feature', name: 'Name of the feature',
@ -69,6 +70,7 @@ export const FeatureToggleListTable: VFC = () => {
const [showExportDialog, setShowExportDialog] = useState(false); const [showExportDialog, setShowExportDialog] = useState(false);
const { features = [], loading, refetchFeatures } = useFeatures(); const { features = [], loading, refetchFeatures } = useFeatures();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const { setToastApiError } = useToast();
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const showEnvironmentLastSeen = Boolean( const showEnvironmentLastSeen = Boolean(
uiConfig.flags.lastSeenByEnvironment, uiConfig.flags.lastSeenByEnvironment,
@ -97,14 +99,20 @@ export const FeatureToggleListTable: VFC = () => {
const { favorite, unfavorite } = useFavoriteFeaturesApi(); const { favorite, unfavorite } = useFavoriteFeaturesApi();
const onFavorite = useCallback( const onFavorite = useCallback(
async (feature: any) => { async (feature: any) => {
if (feature?.favorite) { try {
await unfavorite(feature.project, feature.name); if (feature?.favorite) {
} else { await unfavorite(feature.project, feature.name);
await favorite(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( const columns = useMemo(

View File

@ -144,7 +144,7 @@ export const FeatureView = () => {
const { favorite, unfavorite } = useFavoriteFeaturesApi(); const { favorite, unfavorite } = useFavoriteFeaturesApi();
const { refetchFeature } = useFeature(projectId, featureId); const { refetchFeature } = useFeature(projectId, featureId);
const dependentFeatures = useUiFlag('dependentFeatures'); const dependentFeatures = useUiFlag('dependentFeatures');
const { setToastData } = useToast(); const { setToastData, setToastApiError } = useToast();
const [openTagDialog, setOpenTagDialog] = useState(false); const [openTagDialog, setOpenTagDialog] = useState(false);
const [showDelDialog, setShowDelDialog] = useState(false); const [showDelDialog, setShowDelDialog] = useState(false);
@ -187,12 +187,16 @@ export const FeatureView = () => {
tabData.find((tab) => tab.path === pathname) ?? tabData[0]; tabData.find((tab) => tab.path === pathname) ?? tabData[0];
const onFavorite = async () => { const onFavorite = async () => {
if (feature?.favorite) { try {
await unfavorite(projectId, feature.name); if (feature?.favorite) {
} else { await unfavorite(projectId, feature.name);
await favorite(projectId, feature.name); } else {
await favorite(projectId, feature.name);
}
refetchFeature();
} catch (error) {
setToastApiError('Something went wrong, could not update favorite');
} }
refetchFeature();
}; };
if (status === 404) { if (status === 404) {

View File

@ -64,7 +64,7 @@ export const Project = () => {
const params = useQueryParams(); const params = useQueryParams();
const { project, loading, error, refetch } = useProject(projectId); const { project, loading, error, refetch } = useProject(projectId);
const ref = useLoading(loading); const ref = useLoading(loading);
const { setToastData } = useToast(); const { setToastData, setToastApiError } = useToast();
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const { pathname } = useLocation(); const { pathname } = useLocation();
@ -155,12 +155,16 @@ export const Project = () => {
} }
const onFavorite = async () => { const onFavorite = async () => {
if (project?.favorite) { try {
await unfavorite(projectId); if (project?.favorite) {
} else { await unfavorite(projectId);
await favorite(projectId); } else {
await favorite(projectId);
}
refetch();
} catch (error) {
setToastApiError('Something went wrong, could not update favorite');
} }
refetch();
}; };
const enterpriseIcon = ( const enterpriseIcon = (

View File

@ -65,6 +65,7 @@ import { RowSelectCell } from './RowSelectCell/RowSelectCell';
import { BatchSelectionActionsBar } from '../../../common/BatchSelectionActionsBar/BatchSelectionActionsBar'; import { BatchSelectionActionsBar } from '../../../common/BatchSelectionActionsBar/BatchSelectionActionsBar';
import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions'; import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
import { FeatureEnvironmentSeenCell } from '../../../common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell'; import { FeatureEnvironmentSeenCell } from '../../../common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
import useToast from 'hooks/useToast';
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({ const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
@ -135,7 +136,7 @@ export const ProjectFeatureToggles = ({
string | undefined string | undefined
>(); >();
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const { setToastApiError } = useToast();
const { value: storedParams, setValue: setStoredParams } = const { value: storedParams, setValue: setStoredParams } =
createLocalStorage( createLocalStorage(
`${projectId}:FeatureToggleListTable:v1`, `${projectId}:FeatureToggleListTable:v1`,
@ -171,14 +172,20 @@ export const ProjectFeatureToggles = ({
const onFavorite = useCallback( const onFavorite = useCallback(
async (feature: IFeatureToggleListItem) => { async (feature: IFeatureToggleListItem) => {
if (feature?.favorite) { try {
await unfavorite(projectId, feature.name); if (feature?.favorite) {
} else { await unfavorite(projectId, feature.name);
await favorite(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( const showTagsColumn = useMemo(

View File

@ -63,6 +63,7 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { ListItemType } from './ProjectFeatureToggles.types'; import { ListItemType } from './ProjectFeatureToggles.types';
import { createFeatureToggleCell } from './FeatureToggleSwitch/createFeatureToggleCell'; import { createFeatureToggleCell } from './FeatureToggleSwitch/createFeatureToggleCell';
import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch'; import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch';
import useToast from 'hooks/useToast';
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({ const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
@ -91,6 +92,7 @@ export const ProjectFeatureToggles = ({
}: IProjectFeatureTogglesProps) => { }: IProjectFeatureTogglesProps) => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const theme = useTheme(); const theme = useTheme();
const { setToastApiError } = useToast();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const [strategiesDialogState, setStrategiesDialogState] = useState({ const [strategiesDialogState, setStrategiesDialogState] = useState({
open: false, open: false,
@ -138,12 +140,18 @@ export const ProjectFeatureToggles = ({
const onFavorite = useCallback( const onFavorite = useCallback(
async (feature: IFeatureToggleListItem) => { async (feature: IFeatureToggleListItem) => {
if (feature?.favorite) { try {
await unfavorite(projectId, feature.name); if (feature?.favorite) {
} else { await unfavorite(projectId, feature.name);
await favorite(projectId, feature.name); } else {
await favorite(projectId, feature.name);
}
onChange();
} catch (error) {
setToastApiError(
'Something went wrong, could not update favorite',
);
} }
onChange();
}, },
[projectId, onChange], [projectId, onChange],
); );

View File

@ -27,6 +27,7 @@ import {
StyledDivInfoContainer, StyledDivInfoContainer,
StyledParagraphInfo, StyledParagraphInfo,
} from './ProjectCard.styles'; } from './ProjectCard.styles';
import useToast from 'hooks/useToast';
interface IProjectCardProps { interface IProjectCardProps {
name: string; name: string;
@ -48,6 +49,7 @@ export const ProjectCard = ({
isFavorite = false, isFavorite = false,
}: IProjectCardProps) => { }: IProjectCardProps) => {
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const { setToastApiError } = useToast();
const { isOss } = useUiConfig(); const { isOss } = useUiConfig();
const [anchorEl, setAnchorEl] = useState<Element | null>(null); const [anchorEl, setAnchorEl] = useState<Element | null>(null);
const [showDelDialog, setShowDelDialog] = useState(false); const [showDelDialog, setShowDelDialog] = useState(false);
@ -62,12 +64,16 @@ export const ProjectCard = ({
const onFavorite = async (e: React.SyntheticEvent) => { const onFavorite = async (e: React.SyntheticEvent) => {
e.preventDefault(); e.preventDefault();
if (isFavorite) { try {
await unfavorite(id); if (isFavorite) {
} else { await unfavorite(id);
await favorite(id); } else {
await favorite(id);
}
refetch();
} catch (error) {
setToastApiError('Something went wrong, could not update favorite');
} }
refetch();
}; };
return ( return (

View File

@ -24,6 +24,13 @@ type ApiErrorHandler = (
requestId: string, requestId: string,
) => void; ) => void;
type ApiCaller = () => Promise<Response>;
type RequestFunction = (
apiCaller: ApiCaller,
requestId: string,
loadingOn?: boolean,
) => Promise<Response>;
interface IUseAPI { interface IUseAPI {
handleBadRequest?: ApiErrorHandler; handleBadRequest?: ApiErrorHandler;
handleNotFound?: ApiErrorHandler; handleNotFound?: ApiErrorHandler;
@ -33,6 +40,29 @@ interface IUseAPI {
propagateErrors?: boolean; 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 = ({ const useAPI = ({
handleBadRequest, handleBadRequest,
handleNotFound, handleNotFound,
@ -157,6 +187,27 @@ const useAPI = ({
], ],
); );
const requestWithTimer = (requestFunction: RequestFunction) => {
return async (
apiCaller: () => Promise<Response>,
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( const makeRequest = useCallback(
async ( async (
apiCaller: () => Promise<Response>, apiCaller: () => Promise<Response>,
@ -187,6 +238,27 @@ const useAPI = ({
[handleResponses], [handleResponses],
); );
const makeLightRequest = useCallback(
async (
apiCaller: () => Promise<Response>,
requestId: string,
loadingOn: boolean = true,
): Promise<Response> => {
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( const createRequest = useCallback(
(path: string, options: any, requestId: string = '') => { (path: string, options: any, requestId: string = '') => {
const defaultOptions: RequestInit = { 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 { return {
loading, loading,
makeRequest, makeRequest: isDevelopment ? makeRequestWithTimer : makeRequest,
makeLightRequest: isDevelopment
? makeLightRequestWithTimer
: makeLightRequest,
createRequest, createRequest,
errors, errors,
}; };

View File

@ -5,7 +5,7 @@ import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import useAPI from '../useApi/useApi'; import useAPI from '../useApi/useApi';
export const useFavoriteFeaturesApi = () => { export const useFavoriteFeaturesApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({ const { makeLightRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true, propagateErrors: true,
}); });
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
@ -21,7 +21,7 @@ export const useFavoriteFeaturesApi = () => {
); );
try { try {
await makeRequest(req.caller, req.id); await makeLightRequest(req.caller, req.id);
setToastData({ setToastData({
title: 'Toggle added to favorites', title: 'Toggle added to favorites',
@ -36,7 +36,7 @@ export const useFavoriteFeaturesApi = () => {
setToastApiError(formatUnknownError(error)); setToastApiError(formatUnknownError(error));
} }
}, },
[createRequest, makeRequest], [createRequest, makeLightRequest],
); );
const unfavorite = useCallback( const unfavorite = useCallback(
@ -49,7 +49,7 @@ export const useFavoriteFeaturesApi = () => {
); );
try { try {
await makeRequest(req.caller, req.id); await makeLightRequest(req.caller, req.id);
setToastData({ setToastData({
title: 'Toggle removed from favorites', title: 'Toggle removed from favorites',
@ -64,7 +64,7 @@ export const useFavoriteFeaturesApi = () => {
setToastApiError(formatUnknownError(error)); setToastApiError(formatUnknownError(error));
} }
}, },
[createRequest, makeRequest], [createRequest, makeLightRequest],
); );
return { return {

View File

@ -5,7 +5,7 @@ import useAPI from '../useApi/useApi';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
export const useFavoriteProjectsApi = () => { export const useFavoriteProjectsApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({ const { makeLightRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true, propagateErrors: true,
}); });
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
@ -21,7 +21,7 @@ export const useFavoriteProjectsApi = () => {
); );
try { try {
await makeRequest(req.caller, req.id); await makeLightRequest(req.caller, req.id);
setToastData({ setToastData({
title: 'Project added to favorites', title: 'Project added to favorites',
@ -36,7 +36,7 @@ export const useFavoriteProjectsApi = () => {
setToastApiError(formatUnknownError(error)); setToastApiError(formatUnknownError(error));
} }
}, },
[createRequest, makeRequest], [createRequest, makeLightRequest],
); );
const unfavorite = useCallback( const unfavorite = useCallback(
@ -49,7 +49,7 @@ export const useFavoriteProjectsApi = () => {
); );
try { try {
await makeRequest(req.caller, req.id); await makeLightRequest(req.caller, req.id);
setToastData({ setToastData({
title: 'Project removed from favorites', title: 'Project removed from favorites',
@ -64,7 +64,7 @@ export const useFavoriteProjectsApi = () => {
setToastApiError(formatUnknownError(error)); setToastApiError(formatUnknownError(error));
} }
}, },
[createRequest, makeRequest], [createRequest, makeLightRequest],
); );
return { return {

View File

@ -7,9 +7,10 @@ import useAPI from '../useApi/useApi';
import { IFeatureVariant } from 'interfaces/featureToggle'; import { IFeatureVariant } from 'interfaces/featureToggle';
const useFeatureApi = () => { const useFeatureApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({ const { makeRequest, makeLightRequest, createRequest, errors, loading } =
propagateErrors: true, useAPI({
}); propagateErrors: true,
});
const validateFeatureToggleName = async ( const validateFeatureToggleName = async (
name: string | undefined, name: string | undefined,
@ -61,9 +62,9 @@ const useFeatureApi = () => {
'toggleFeatureEnvironmentOn', 'toggleFeatureEnvironmentOn',
); );
return makeRequest(req.caller, req.id); return makeLightRequest(req.caller, req.id);
}, },
[createRequest, makeRequest], [createRequest, makeLightRequest],
); );
const bulkToggleFeaturesEnvironmentOn = useCallback( const bulkToggleFeaturesEnvironmentOn = useCallback(
@ -119,9 +120,9 @@ const useFeatureApi = () => {
'toggleFeatureEnvironmentOff', 'toggleFeatureEnvironmentOff',
); );
return makeRequest(req.caller, req.id); return makeLightRequest(req.caller, req.id);
}, },
[createRequest, makeRequest], [createRequest, makeLightRequest],
); );
const changeFeatureProject = async ( const changeFeatureProject = async (