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

Write a generic http thrower for status > 299 (#405)

* Write a generic http thrower for status > 299

* Perform location reload if user is no longer authorized, i.e if status === 401
This commit is contained in:
Christopher Kolstad 2021-10-15 09:21:38 +02:00 committed by GitHub
parent f132ce63da
commit 166c6fef0e
19 changed files with 99 additions and 55 deletions

View File

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { Route, Switch, Redirect } from 'react-router-dom';
import { Redirect, Route, Switch } from 'react-router-dom';
import { RouteComponentProps } from 'react-router';
import ProtectedRoute from './common/ProtectedRoute/ProtectedRoute';
@ -13,6 +13,9 @@ import IAuthStatus from '../interfaces/user';
import { useEffect } from 'react';
import NotFound from './common/NotFound/NotFound';
import Feedback from './common/Feedback';
import { SWRConfig } from 'swr';
import useToast from '../hooks/useToast';
interface IAppProps extends RouteComponentProps {
user: IAuthStatus;
fetchUiBootstrap: any;
@ -20,6 +23,7 @@ interface IAppProps extends RouteComponentProps {
}
const App = ({ location, user, fetchUiBootstrap, feedback }: IAppProps) => {
const { toast, setToastData } = useToast();
useEffect(() => {
fetchUiBootstrap();
/* eslint-disable-next-line */
@ -71,27 +75,46 @@ const App = ({ location, user, fetchUiBootstrap, feedback }: IAppProps) => {
};
return (
<div className={styles.container}>
<LayoutPicker location={location}>
<Switch>
<ProtectedRoute
exact
path="/"
unauthorized={isUnauthorized()}
component={Redirect}
renderProps={{ to: '/features' }}
<SWRConfig value={{
onError: (error) => {
if (!isUnauthorized()) {
if (error.status === 401) {
// If we've been in an authorized state,
// but cookie has been deleted (server or client side,
// perform a window reload to reload app
window.location.reload();
}
setToastData({
show: true,
type: 'error',
text: error.message,
});
}
},
}}>
<div className={styles.container}>
<LayoutPicker location={location}>
<Switch>
<ProtectedRoute
exact
path='/'
unauthorized={isUnauthorized()}
component={Redirect}
renderProps={{ to: '/features' }}
/>
{renderMainLayoutRoutes()}
{renderStandaloneRoutes()}
<Route path='/404' component={NotFound} />
<Redirect to='/404' />
</Switch>
<Feedback
feedbackId='pnps'
openUrl='http://feedback.unleash.run'
/>
{renderMainLayoutRoutes()}
{renderStandaloneRoutes()}
<Route path="/404" component={NotFound} />
<Redirect to="/404" />
</Switch>
<Feedback
feedbackId="pnps"
openUrl="http://feedback.unleash.run"
/>
</LayoutPicker>
</div>
</LayoutPicker>
{toast}
</div>
</SWRConfig>
);
};
// Set state to any for now, to avoid typing up entire state object while converting to tsx.

View File

@ -63,7 +63,7 @@ const FeatureOverviewMetrics = () => {
});
}
/* We display maxium three environments metrics */
/* We display maximum three environments metrics */
if (featureMetrics.length >= 3) {
return featureMetrics.slice(0, 3).map((metric, index) => {
if (index === 0) {

View File

@ -0,0 +1,21 @@
const handleErrorResponses = (target: string) => async (res: Response) => {
if (!res.ok) {
const error = new Error(`An error occurred while trying to get ${target}`);
// Try to resolve body, but don't rethrow res.json is not a function
try {
// @ts-ignore
error.info = await res.json();
} catch (e) {
// @ts-ignore
error.info = {};
}
// @ts-ignore
error.status = res.status;
// @ts-ignore
error.statusText = res.statusText;
throw error;
}
return res;
}
export default handleErrorResponses;

View File

@ -1,13 +1,14 @@
import useSWR, { mutate } from 'swr';
import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import handleErrorResponses from '../httpErrorResponseHandler';
const useApiTokens = () => {
const fetcher = async () => {
const path = formatApiPath(`api/admin/api-tokens`);
const res = await fetch(path, {
method: 'GET',
});
}).then(handleErrorResponses('Api tokens'));
return res.json();
};

View File

@ -2,6 +2,7 @@ import useSWR, { mutate } from 'swr';
import { useState, useEffect } from 'react';
import { IEnvironmentResponse } from '../../../../interfaces/environments';
import { formatApiPath } from '../../../../utils/format-path';
import handleErrorResponses from '../httpErrorResponseHandler';
export const ENVIRONMENT_CACHE_KEY = `api/admin/environments`;
@ -10,7 +11,7 @@ const useEnvironments = () => {
const path = formatApiPath(`api/admin/environments`);
return fetch(path, {
method: 'GET',
}).then(res => res.json());
}).then(handleErrorResponses('Environments')).then(res => res.json());
};
const { data, error } = useSWR<IEnvironmentResponse>(

View File

@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import { IFeatureToggle } from '../../../../interfaces/featureToggle';
import { defaultFeature } from './defaultFeature';
import handleErrorResponses from '../httpErrorResponseHandler';
interface IUseFeatureOptions {
refreshInterval?: number;
@ -21,25 +22,9 @@ const useFeature = (
const path = formatApiPath(
`api/admin/projects/${projectId}/features/${id}`
);
const res = await fetch(path, {
return fetch(path, {
method: 'GET',
});
// If the status code is not in the range 200-299,
// we still try to parse and throw it.
if (!res.ok) {
const error = new Error('An error occurred while fetching the data.')
// Attach extra info to the error object.
// @ts-ignore
error.info = await res.json();
// @ts-ignore
error.status = res.status;
throw error;
}
return res.json()
}).then(handleErrorResponses('Feature toggle data')).then(res => res.json());
};
const FEATURE_CACHE_KEY = `api/admin/projects/${projectId}/features/${id}`;

View File

@ -3,6 +3,7 @@ import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import { IFeatureStrategy } from '../../../../interfaces/strategy';
import handleErrorResponses from '../httpErrorResponseHandler';
interface IUseFeatureOptions {
refreshInterval?: number;
@ -25,7 +26,7 @@ const useFeatureStrategy = (
);
return fetch(path, {
method: 'GET',
}).then(res => res.json());
}).then(handleErrorResponses(`Strategies for ${featureId}`)).then(res => res.json());
};
const FEATURE_STRATEGY_CACHE_KEY = strategyId;

View File

@ -2,13 +2,14 @@ import useSWR, { mutate } from 'swr';
import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import { IFeatureType } from '../../../../interfaces/featureTypes';
import handleErrorResponses from '../httpErrorResponseHandler';
const useFeatureTypes = () => {
const fetcher = async () => {
const path = formatApiPath(`api/admin/feature-types`);
const res = await fetch(path, {
method: 'GET',
});
}).then(handleErrorResponses('Feature types'));
return res.json();
};

View File

@ -4,6 +4,7 @@ import { IProjectHealthReport } from '../../../../interfaces/project';
import { fallbackProject } from '../useProject/fallbackProject';
import useSort from '../../../useSort';
import { formatApiPath } from '../../../../utils/format-path';
import handleErrorResponses from '../httpErrorResponseHandler';
const useHealthReport = (id: string) => {
const KEY = `api/admin/projects/${id}/health-report`;
@ -12,7 +13,7 @@ const useHealthReport = (id: string) => {
const path = formatApiPath(`api/admin/projects/${id}/health-report`);
return fetch(path, {
method: 'GET',
}).then(res => res.json());
}).then(handleErrorResponses('Health report')).then(res => res.json());
};
const [sort] = useSort();

View File

@ -1,11 +1,12 @@
import { formatApiPath } from '../../../../utils/format-path';
import handleErrorResponses from '../httpErrorResponseHandler';
export const getProjectFetcher = (id: string) => {
const fetcher = () => {
const path = formatApiPath(`api/admin/projects/${id}`);
return fetch(path, {
method: 'GET',
}).then(res => res.json());
}).then(handleErrorResponses('Project overview')).then(res => res.json());
};
const KEY = `api/admin/projects/${id}`;

View File

@ -3,13 +3,14 @@ import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import { IProjectCard } from '../../../../interfaces/project';
import handleErrorResponses from '../httpErrorResponseHandler';
const useProjects = () => {
const fetcher = () => {
const path = formatApiPath(`api/admin/projects`);
return fetch(path, {
method: 'GET',
}).then(res => res.json());
}).then(handleErrorResponses('Projects')).then(res => res.json());
};
const KEY = `api/admin/projects`;

View File

@ -2,12 +2,13 @@ import useSWR from 'swr';
import useQueryParams from '../../../useQueryParams';
import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import handleErrorResponses from '../httpErrorResponseHandler';
const getFetcher = (token: string) => () => {
const path = formatApiPath(`auth/reset/validate?token=${token}`);
return fetch(path, {
method: 'GET',
}).then(res => res.json());
}).then(handleErrorResponses('Password reset')).then(res => res.json());
};
const INVALID_TOKEN_ERROR = 'InvalidTokenError';

View File

@ -2,6 +2,7 @@ import useSWR, { mutate } from 'swr';
import { useEffect, useState } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import { IStrategy } from '../../../../interfaces/strategy';
import handleErrorResponses from '../httpErrorResponseHandler';
export const STRATEGIES_CACHE_KEY = 'api/admin/strategies';
@ -12,7 +13,7 @@ const useStrategies = () => {
return fetch(path, {
method: 'GET',
credentials: 'include',
}).then(res => res.json());
}).then(handleErrorResponses('Strategies')).then(res => res.json());
};
const { data, error } = useSWR<{ strategies: IStrategy[] }>(

View File

@ -2,13 +2,14 @@ import useSWR, { mutate } from 'swr';
import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import { ITagType } from '../../../../interfaces/tags';
import handleErrorResponses from '../httpErrorResponseHandler';
const useTagTypes = () => {
const fetcher = async () => {
const path = formatApiPath(`api/admin/tag-types`);
const res = await fetch(path, {
method: 'GET',
});
}).then(handleErrorResponses('Tag types'));
return res.json();
};

View File

@ -2,13 +2,14 @@ import useSWR, { mutate } from 'swr';
import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import { ITag } from '../../../../interfaces/tags';
import handleErrorResponses from '../httpErrorResponseHandler';
const useTags = (featureId: string) => {
const fetcher = async () => {
const path = formatApiPath(`api/admin/features/${featureId}/tags`);
const res = await fetch(path, {
method: 'GET',
});
}).then(handleErrorResponses('Tags'));
return res.json();
};

View File

@ -3,6 +3,7 @@ import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import { defaultValue } from './defaultValue';
import { IUiConfig } from '../../../../interfaces/uiConfig';
import handleErrorResponses from '../httpErrorResponseHandler';
const REQUEST_KEY = 'api/admin/ui-config';
@ -13,7 +14,7 @@ const useUiConfig = () => {
return fetch(path, {
method: 'GET',
credentials: 'include',
}).then(res => res.json());
}).then(handleErrorResponses('configuration')).then(res => res.json());
};
const { data, error } = useSWR<IUiConfig>(REQUEST_KEY, fetcher);

View File

@ -1,13 +1,14 @@
import useSWR, { mutate } from 'swr';
import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import handleErrorResponses from '../httpErrorResponseHandler';
const useUnleashContext = (revalidate = true) => {
const fetcher = () => {
const path = formatApiPath(`api/admin/context`);
return fetch(path, {
method: 'GET',
}).then(res => res.json());
}).then(handleErrorResponses('Context variables')).then(res => res.json());
};
const CONTEXT_CACHE_KEY = 'api/admin/context';

View File

@ -2,6 +2,7 @@ import useSWR, { mutate } from 'swr';
import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import { IPermission } from '../../../../interfaces/user';
import handleErrorResponses from '../httpErrorResponseHandler';
const useUser = () => {
const KEY = `api/admin/user`;
@ -9,7 +10,7 @@ const useUser = () => {
const path = formatApiPath(`api/admin/user`);
return fetch(path, {
method: 'GET',
}).then(res => res.json());
}).then(handleErrorResponses('User info')).then(res => res.json());
};
const { data, error } = useSWR(KEY, fetcher);

View File

@ -1,13 +1,14 @@
import useSWR, { mutate } from 'swr';
import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import handleErrorResponses from '../httpErrorResponseHandler';
const useUsers = () => {
const fetcher = () => {
const path = formatApiPath(`api/admin/user-admin`);
return fetch(path, {
method: 'GET',
}).then(res => res.json());
}).then(handleErrorResponses('Users')).then(res => res.json());
};
const { data, error } = useSWR(`api/admin/user-admin`, fetcher);