diff --git a/frontend/src/component/App.tsx b/frontend/src/component/App.tsx
index 7e4b9d90ae..8180e4a393 100644
--- a/frontend/src/component/App.tsx
+++ b/frontend/src/component/App.tsx
@@ -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 (
-
-
-
- {
+ 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,
+ });
+ }
+ },
+ }}>
+
+
+
+
+ {renderMainLayoutRoutes()}
+ {renderStandaloneRoutes()}
+
+
+
+
- {renderMainLayoutRoutes()}
- {renderStandaloneRoutes()}
-
-
-
-
-
-
+
+ {toast}
+
+
);
};
// Set state to any for now, to avoid typing up entire state object while converting to tsx.
diff --git a/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewMetrics/FeatureOverviewMetrics.tsx b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewMetrics/FeatureOverviewMetrics.tsx
index 629f93e178..767ecd8741 100644
--- a/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewMetrics/FeatureOverviewMetrics.tsx
+++ b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewMetrics/FeatureOverviewMetrics.tsx
@@ -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) {
diff --git a/frontend/src/hooks/api/getters/httpErrorResponseHandler.ts b/frontend/src/hooks/api/getters/httpErrorResponseHandler.ts
new file mode 100644
index 0000000000..4222588146
--- /dev/null
+++ b/frontend/src/hooks/api/getters/httpErrorResponseHandler.ts
@@ -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;
diff --git a/frontend/src/hooks/api/getters/useApiTokens/useApiTokens.ts b/frontend/src/hooks/api/getters/useApiTokens/useApiTokens.ts
index 7c9b891a73..7192511d08 100644
--- a/frontend/src/hooks/api/getters/useApiTokens/useApiTokens.ts
+++ b/frontend/src/hooks/api/getters/useApiTokens/useApiTokens.ts
@@ -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();
};
diff --git a/frontend/src/hooks/api/getters/useEnvironments/useEnvironments.ts b/frontend/src/hooks/api/getters/useEnvironments/useEnvironments.ts
index 864abc33a5..4d71e95a00 100644
--- a/frontend/src/hooks/api/getters/useEnvironments/useEnvironments.ts
+++ b/frontend/src/hooks/api/getters/useEnvironments/useEnvironments.ts
@@ -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(
diff --git a/frontend/src/hooks/api/getters/useFeature/useFeature.ts b/frontend/src/hooks/api/getters/useFeature/useFeature.ts
index 8c96c83938..acad0a6260 100644
--- a/frontend/src/hooks/api/getters/useFeature/useFeature.ts
+++ b/frontend/src/hooks/api/getters/useFeature/useFeature.ts
@@ -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}`;
diff --git a/frontend/src/hooks/api/getters/useFeatureStrategy/useFeatureStrategy.ts b/frontend/src/hooks/api/getters/useFeatureStrategy/useFeatureStrategy.ts
index 60a870ec00..673143a389 100644
--- a/frontend/src/hooks/api/getters/useFeatureStrategy/useFeatureStrategy.ts
+++ b/frontend/src/hooks/api/getters/useFeatureStrategy/useFeatureStrategy.ts
@@ -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;
diff --git a/frontend/src/hooks/api/getters/useFeatureTypes/useFeatureTypes.ts b/frontend/src/hooks/api/getters/useFeatureTypes/useFeatureTypes.ts
index 2962450a8a..9ae20ce2bf 100644
--- a/frontend/src/hooks/api/getters/useFeatureTypes/useFeatureTypes.ts
+++ b/frontend/src/hooks/api/getters/useFeatureTypes/useFeatureTypes.ts
@@ -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();
};
diff --git a/frontend/src/hooks/api/getters/useHealthReport/useHealthReport.ts b/frontend/src/hooks/api/getters/useHealthReport/useHealthReport.ts
index 9c1a033bbd..4ee8247be0 100644
--- a/frontend/src/hooks/api/getters/useHealthReport/useHealthReport.ts
+++ b/frontend/src/hooks/api/getters/useHealthReport/useHealthReport.ts
@@ -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();
diff --git a/frontend/src/hooks/api/getters/useProject/getProjectFetcher.ts b/frontend/src/hooks/api/getters/useProject/getProjectFetcher.ts
index 1fe3e760ec..302c24ccd1 100644
--- a/frontend/src/hooks/api/getters/useProject/getProjectFetcher.ts
+++ b/frontend/src/hooks/api/getters/useProject/getProjectFetcher.ts
@@ -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}`;
diff --git a/frontend/src/hooks/api/getters/useProjects/useProjects.ts b/frontend/src/hooks/api/getters/useProjects/useProjects.ts
index a45f2b2655..2c3ab1c768 100644
--- a/frontend/src/hooks/api/getters/useProjects/useProjects.ts
+++ b/frontend/src/hooks/api/getters/useProjects/useProjects.ts
@@ -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`;
diff --git a/frontend/src/hooks/api/getters/useResetPassword/useResetPassword.ts b/frontend/src/hooks/api/getters/useResetPassword/useResetPassword.ts
index b1b5cab7c6..11b89afa12 100644
--- a/frontend/src/hooks/api/getters/useResetPassword/useResetPassword.ts
+++ b/frontend/src/hooks/api/getters/useResetPassword/useResetPassword.ts
@@ -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';
diff --git a/frontend/src/hooks/api/getters/useStrategies/useStrategies.ts b/frontend/src/hooks/api/getters/useStrategies/useStrategies.ts
index 9638c58409..c1d005e560 100644
--- a/frontend/src/hooks/api/getters/useStrategies/useStrategies.ts
+++ b/frontend/src/hooks/api/getters/useStrategies/useStrategies.ts
@@ -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[] }>(
diff --git a/frontend/src/hooks/api/getters/useTagTypes/useTagTypes.ts b/frontend/src/hooks/api/getters/useTagTypes/useTagTypes.ts
index 4dd0380807..8d6cae4b23 100644
--- a/frontend/src/hooks/api/getters/useTagTypes/useTagTypes.ts
+++ b/frontend/src/hooks/api/getters/useTagTypes/useTagTypes.ts
@@ -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();
};
diff --git a/frontend/src/hooks/api/getters/useTags/useTags.ts b/frontend/src/hooks/api/getters/useTags/useTags.ts
index 5e31443b3b..dcea601d79 100644
--- a/frontend/src/hooks/api/getters/useTags/useTags.ts
+++ b/frontend/src/hooks/api/getters/useTags/useTags.ts
@@ -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();
};
diff --git a/frontend/src/hooks/api/getters/useUiConfig/useUiConfig.ts b/frontend/src/hooks/api/getters/useUiConfig/useUiConfig.ts
index 2f50605943..e299155c6e 100644
--- a/frontend/src/hooks/api/getters/useUiConfig/useUiConfig.ts
+++ b/frontend/src/hooks/api/getters/useUiConfig/useUiConfig.ts
@@ -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(REQUEST_KEY, fetcher);
diff --git a/frontend/src/hooks/api/getters/useUnleashContext/useUnleashContext.ts b/frontend/src/hooks/api/getters/useUnleashContext/useUnleashContext.ts
index 35a99b661e..5701a7611f 100644
--- a/frontend/src/hooks/api/getters/useUnleashContext/useUnleashContext.ts
+++ b/frontend/src/hooks/api/getters/useUnleashContext/useUnleashContext.ts
@@ -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';
diff --git a/frontend/src/hooks/api/getters/useUser/useUser.ts b/frontend/src/hooks/api/getters/useUser/useUser.ts
index 997354537b..69e855ca18 100644
--- a/frontend/src/hooks/api/getters/useUser/useUser.ts
+++ b/frontend/src/hooks/api/getters/useUser/useUser.ts
@@ -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);
diff --git a/frontend/src/hooks/api/getters/useUsers/useUsers.ts b/frontend/src/hooks/api/getters/useUsers/useUsers.ts
index 87c0c9a4fc..45387c400a 100644
--- a/frontend/src/hooks/api/getters/useUsers/useUsers.ts
+++ b/frontend/src/hooks/api/getters/useUsers/useUsers.ts
@@ -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);