1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01: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 { 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(

View File

@ -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) {

View File

@ -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 = (

View File

@ -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(

View File

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

View File

@ -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<Element | null>(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 (

View File

@ -24,6 +24,13 @@ type ApiErrorHandler = (
requestId: string,
) => void;
type ApiCaller = () => Promise<Response>;
type RequestFunction = (
apiCaller: ApiCaller,
requestId: string,
loadingOn?: boolean,
) => Promise<Response>;
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<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(
async (
apiCaller: () => Promise<Response>,
@ -187,6 +238,27 @@ const useAPI = ({
[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(
(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,
};

View File

@ -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 {

View File

@ -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 {

View File

@ -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 (