1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-08 01:15:49 +02:00

Merge branch 'master' into fix/featureView-tab

This commit is contained in:
Youssef Khedher 2021-10-15 08:41:35 +01:00 committed by GitHub
commit 4d55aab98a
21 changed files with 104 additions and 60 deletions

View File

@ -45,7 +45,7 @@
"@types/enzyme": "3.10.9", "@types/enzyme": "3.10.9",
"@types/enzyme-adapter-react-16": "1.0.6", "@types/enzyme-adapter-react-16": "1.0.6",
"@types/jest": "27.0.2", "@types/jest": "27.0.2",
"@types/node": "14.17.26", "@types/node": "14.17.27",
"@types/react": "17.0.30", "@types/react": "17.0.30",
"@types/react-dom": "17.0.9", "@types/react-dom": "17.0.9",
"@types/react-router-dom": "5.3.1", "@types/react-router-dom": "5.3.1",

View File

@ -1,5 +1,5 @@
import { connect } from 'react-redux'; 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 { RouteComponentProps } from 'react-router';
import ProtectedRoute from './common/ProtectedRoute/ProtectedRoute'; import ProtectedRoute from './common/ProtectedRoute/ProtectedRoute';
@ -13,6 +13,9 @@ import IAuthStatus from '../interfaces/user';
import { useEffect } from 'react'; import { useEffect } from 'react';
import NotFound from './common/NotFound/NotFound'; import NotFound from './common/NotFound/NotFound';
import Feedback from './common/Feedback'; import Feedback from './common/Feedback';
import { SWRConfig } from 'swr';
import useToast from '../hooks/useToast';
interface IAppProps extends RouteComponentProps { interface IAppProps extends RouteComponentProps {
user: IAuthStatus; user: IAuthStatus;
fetchUiBootstrap: any; fetchUiBootstrap: any;
@ -20,6 +23,7 @@ interface IAppProps extends RouteComponentProps {
} }
const App = ({ location, user, fetchUiBootstrap, feedback }: IAppProps) => { const App = ({ location, user, fetchUiBootstrap, feedback }: IAppProps) => {
const { toast, setToastData } = useToast();
useEffect(() => { useEffect(() => {
fetchUiBootstrap(); fetchUiBootstrap();
/* eslint-disable-next-line */ /* eslint-disable-next-line */
@ -71,27 +75,46 @@ const App = ({ location, user, fetchUiBootstrap, feedback }: IAppProps) => {
}; };
return ( return (
<div className={styles.container}> <SWRConfig value={{
<LayoutPicker location={location}> onError: (error) => {
<Switch> if (!isUnauthorized()) {
<ProtectedRoute if (error.status === 401) {
exact // If we've been in an authorized state,
path="/" // but cookie has been deleted (server or client side,
unauthorized={isUnauthorized()} // perform a window reload to reload app
component={Redirect} window.location.reload();
renderProps={{ to: '/features' }} }
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()} </LayoutPicker>
{renderStandaloneRoutes()} {toast}
<Route path="/404" component={NotFound} /> </div>
<Redirect to="/404" /> </SWRConfig>
</Switch>
<Feedback
feedbackId="pnps"
openUrl="http://feedback.unleash.run"
/>
</LayoutPicker>
</div>
); );
}; };
// Set state to any for now, to avoid typing up entire state object while converting to tsx. // 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) { if (featureMetrics.length >= 3) {
return featureMetrics.slice(0, 3).map((metric, index) => { return featureMetrics.slice(0, 3).map((metric, index) => {
if (index === 0) { 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 useSWR, { mutate } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import handleErrorResponses from '../httpErrorResponseHandler';
const useApiTokens = () => { const useApiTokens = () => {
const fetcher = async () => { const fetcher = async () => {
const path = formatApiPath(`api/admin/api-tokens`); const path = formatApiPath(`api/admin/api-tokens`);
const res = await fetch(path, { const res = await fetch(path, {
method: 'GET', method: 'GET',
}); }).then(handleErrorResponses('Api tokens'));
return res.json(); return res.json();
}; };

View File

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

View File

@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import { IFeatureToggle } from '../../../../interfaces/featureToggle'; import { IFeatureToggle } from '../../../../interfaces/featureToggle';
import { defaultFeature } from './defaultFeature'; import { defaultFeature } from './defaultFeature';
import handleErrorResponses from '../httpErrorResponseHandler';
interface IUseFeatureOptions { interface IUseFeatureOptions {
refreshInterval?: number; refreshInterval?: number;
@ -21,25 +22,9 @@ const useFeature = (
const path = formatApiPath( const path = formatApiPath(
`api/admin/projects/${projectId}/features/${id}` `api/admin/projects/${projectId}/features/${id}`
); );
return fetch(path, {
const res = await fetch(path, {
method: 'GET', method: 'GET',
}); }).then(handleErrorResponses('Feature toggle data')).then(res => res.json());
// 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()
}; };
const FEATURE_CACHE_KEY = `api/admin/projects/${projectId}/features/${id}`; 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 { formatApiPath } from '../../../../utils/format-path';
import { IFeatureStrategy } from '../../../../interfaces/strategy'; import { IFeatureStrategy } from '../../../../interfaces/strategy';
import handleErrorResponses from '../httpErrorResponseHandler';
interface IUseFeatureOptions { interface IUseFeatureOptions {
refreshInterval?: number; refreshInterval?: number;
@ -25,7 +26,7 @@ const useFeatureStrategy = (
); );
return fetch(path, { return fetch(path, {
method: 'GET', method: 'GET',
}).then(res => res.json()); }).then(handleErrorResponses(`Strategies for ${featureId}`)).then(res => res.json());
}; };
const FEATURE_STRATEGY_CACHE_KEY = strategyId; const FEATURE_STRATEGY_CACHE_KEY = strategyId;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2107,10 +2107,10 @@
resolved "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz" resolved "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz"
integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==
"@types/node@14.17.26": "@types/node@14.17.27":
version "14.17.26" version "14.17.27"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.26.tgz#47a53c7e7804490155a4646d60c8e194816d073c" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.27.tgz#5054610d37bb5f6e21342d0e6d24c494231f3b85"
integrity sha512-eSTNkK/nfmnC7IKpOJZixDgG0W2/eHz1qyFN7o/rwwwIHsVRp+G9nbh4BrQ77kbQ2zPu286AQRxkuRLPcR3gXw== integrity sha512-94+Ahf9IcaDuJTle/2b+wzvjmutxXAEXU6O81JHblYXUg2BDG+dnBy7VxIPHKAyEEDHzCMQydTJuWvrE+Aanzw==
"@types/node@^14.14.31": "@types/node@^14.14.31":
version "14.17.19" version "14.17.19"