1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-29 01:15:48 +02:00

Merge branch 'main' into fix/impression-data-label

This commit is contained in:
Youssef Khedher 2022-02-11 12:22:43 +01:00 committed by GitHub
commit b345ea45d7
36 changed files with 562 additions and 1103 deletions

View File

@ -1,7 +1,7 @@
{
"name": "unleash-frontend",
"description": "unleash your features",
"version": "4.7.2",
"version": "4.8.0-beta.1",
"keywords": [
"unleash",
"feature toggle",
@ -54,7 +54,6 @@
"@types/react-test-renderer": "17.0.1",
"@types/react-timeago": "4.1.3",
"@welldone-software/why-did-you-render": "6.2.3",
"array-move": "3.0.1",
"classnames": "2.3.1",
"copy-to-clipboard": "3.3.1",
"craco": "0.0.3",
@ -64,13 +63,10 @@
"debounce": "1.2.1",
"deep-diff": "1.0.2",
"fast-json-patch": "3.1.0",
"fetch-mock": "9.11.0",
"http-proxy-middleware": "2.0.2",
"immutable": "4.0.0",
"@types/lodash.clonedeep": "4.5.6",
"lodash.clonedeep": "4.5.0",
"lodash.flow": "3.5.0",
"node-fetch": "2.6.7",
"prettier": "2.5.1",
"prop-types": "15.8.1",
"react": "17.0.2",
@ -79,15 +75,10 @@
"react-dom": "17.0.2",
"react-hooks-global-state": "1.0.2",
"react-outside-click-handler": "1.3.0",
"react-redux": "7.2.6",
"react-router-dom": "5.3.0",
"react-scripts": "4.0.3",
"react-test-renderer": "16.14.0",
"react-timeago": "6.2.1",
"redux": "4.1.2",
"redux-devtools-extension": "2.13.9",
"redux-mock-store": "1.5.4",
"redux-thunk": "2.4.1",
"sass": "1.49.7",
"swr": "1.2.1",
"typescript": "4.5.5",

View File

@ -1,11 +0,0 @@
import { Map as $MAp } from 'immutable';
export const createFakeStore = permissions => {
return {
getState: () => ({
user: new $MAp({
permissions,
}),
}),
};
};

View File

@ -9,18 +9,12 @@ import SWRProvider from './providers/SWRProvider/SWRProvider';
import ToastRenderer from './common/ToastRenderer/ToastRenderer';
import styles from './styles.module.scss';
import { Redirect, Route, Switch } from 'react-router-dom';
import { RouteComponentProps } from 'react-router';
import { routes } from './menu/routes';
import { useEffect } from 'react';
import { useAuthDetails } from '../hooks/api/getters/useAuth/useAuthDetails';
import { useAuthUser } from '../hooks/api/getters/useAuth/useAuthUser';
import { useAuthSplash } from '../hooks/api/getters/useAuth/useAuthSplash';
interface IAppProps extends RouteComponentProps {
fetchUiBootstrap: () => void;
}
export const App = ({ fetchUiBootstrap }: IAppProps) => {
export const App = () => {
const { splash, refetchSplash } = useAuthSplash();
const { authDetails } = useAuthDetails();
const { user } = useAuthUser();
@ -29,10 +23,6 @@ export const App = ({ fetchUiBootstrap }: IAppProps) => {
const hasFetchedAuth = Boolean(authDetails || user);
const showEnvSplash = isLoggedIn && splash?.environment === false;
useEffect(() => {
fetchUiBootstrap();
}, [fetchUiBootstrap, authDetails?.type]);
const renderMainLayoutRoutes = () => {
return routes.filter(route => route.layout === 'main').map(renderRoute);
};

View File

@ -1,5 +0,0 @@
import { connect } from 'react-redux';
import { App } from './App';
import { fetchUiBootstrap } from '../store/ui-bootstrap/actions';
export default connect(null, { fetchUiBootstrap })(App);

View File

@ -0,0 +1,64 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly if no application 1`] = `
<div>
<p>
Loading...
</p>
<div
className="MuiLinearProgress-root MuiLinearProgress-colorPrimary MuiLinearProgress-indeterminate"
role="progressbar"
>
<div
className="MuiLinearProgress-bar MuiLinearProgress-barColorPrimary MuiLinearProgress-bar1Indeterminate"
style={Object {}}
/>
<div
className="MuiLinearProgress-bar MuiLinearProgress-bar2Indeterminate MuiLinearProgress-barColorPrimary"
style={Object {}}
/>
</div>
</div>
`;
exports[`renders correctly with permissions 1`] = `
<div>
<p>
Loading...
</p>
<div
className="MuiLinearProgress-root MuiLinearProgress-colorPrimary MuiLinearProgress-indeterminate"
role="progressbar"
>
<div
className="MuiLinearProgress-bar MuiLinearProgress-barColorPrimary MuiLinearProgress-bar1Indeterminate"
style={Object {}}
/>
<div
className="MuiLinearProgress-bar MuiLinearProgress-bar2Indeterminate MuiLinearProgress-barColorPrimary"
style={Object {}}
/>
</div>
</div>
`;
exports[`renders correctly without permission 1`] = `
<div>
<p>
Loading...
</p>
<div
className="MuiLinearProgress-root MuiLinearProgress-colorPrimary MuiLinearProgress-indeterminate"
role="progressbar"
>
<div
className="MuiLinearProgress-bar MuiLinearProgress-barColorPrimary MuiLinearProgress-bar1Indeterminate"
style={Object {}}
/>
<div
className="MuiLinearProgress-bar MuiLinearProgress-bar2Indeterminate MuiLinearProgress-barColorPrimary"
style={Object {}}
/>
</div>
</div>
`;

View File

@ -0,0 +1,158 @@
import { ThemeProvider } from '@material-ui/core';
import { ApplicationEdit } from '../ApplicationEdit/ApplicationEdit';
import renderer from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom';
import { ADMIN } from '../../providers/AccessProvider/permissions';
import theme from '../../../themes/main-theme';
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
import UIProvider from '../../providers/UIProvider/UIProvider';
test('renders correctly if no application', () => {
const tree = renderer
.create(
<AccessProvider permissions={[{ permission: ADMIN }]}>
<ThemeProvider theme={theme}>
<UIProvider>
<MemoryRouter initialEntries={['/test']}>
<ApplicationEdit
fetchApplication={() => Promise.resolve({})}
storeApplicationMetaData={jest.fn()}
deleteApplication={jest.fn()}
history={{}}
locationSettings={{ locale: 'en-GB' }}
/>
</MemoryRouter>
</UIProvider>
</ThemeProvider>
</AccessProvider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
test('renders correctly without permission', () => {
const tree = renderer
.create(
<MemoryRouter>
<ThemeProvider theme={theme}>
<UIProvider>
<AccessProvider permissions={[{ permission: ADMIN }]}>
<ApplicationEdit
fetchApplication={() => Promise.resolve({})}
storeApplicationMetaData={jest.fn()}
deleteApplication={jest.fn()}
history={{}}
application={{
appName: 'test-app',
instances: [
{
instanceId: 'instance-1',
clientIp: '123.123.123.123',
lastSeen: '2017-02-23T15:56:49',
sdkVersion: '4.0',
},
],
strategies: [
{
name: 'StrategyA',
description: 'A description',
},
{
name: 'StrategyB',
description: 'B description',
notFound: true,
},
],
seenToggles: [
{
name: 'ToggleA',
description: 'this is A toggle',
enabled: true,
project: 'default',
},
{
name: 'ToggleB',
description: 'this is B toggle',
enabled: false,
notFound: true,
project: 'default',
},
],
url: 'http://example.org',
description: 'app description',
}}
locationSettings={{ locale: 'en-GB' }}
/>
</AccessProvider>
</UIProvider>
</ThemeProvider>
</MemoryRouter>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
test('renders correctly with permissions', () => {
const tree = renderer
.create(
<MemoryRouter>
<ThemeProvider theme={theme}>
<UIProvider>
<AccessProvider permissions={[{ permission: ADMIN }]}>
<ApplicationEdit
fetchApplication={() => Promise.resolve({})}
storeApplicationMetaData={jest.fn()}
history={{}}
deleteApplication={jest.fn()}
application={{
appName: 'test-app',
instances: [
{
instanceId: 'instance-1',
clientIp: '123.123.123.123',
lastSeen: '2017-02-23T15:56:49',
sdkVersion: '4.0',
},
],
strategies: [
{
name: 'StrategyA',
description: 'A description',
},
{
name: 'StrategyB',
description: 'B description',
notFound: true,
},
],
seenToggles: [
{
name: 'ToggleA',
description: 'this is A toggle',
enabled: true,
project: 'default',
},
{
name: 'ToggleB',
description: 'this is B toggle',
enabled: false,
notFound: true,
project: 'default',
},
],
url: 'http://example.org',
description: 'app description',
}}
locationSettings={{ locale: 'en-GB' }}
/>
</AccessProvider>
</UIProvider>
</ThemeProvider>
</MemoryRouter>
)
.toJSON();
expect(tree).toMatchSnapshot();
});

View File

@ -4,14 +4,13 @@ import renderer from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom';
import { ADMIN } from '../../providers/AccessProvider/permissions';
import theme from '../../../themes/main-theme';
import { createFakeStore } from '../../../accessStoreFake';
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
import UIProvider from '../../providers/UIProvider/UIProvider';
test('renders correctly if no application', () => {
const tree = renderer
.create(
<AccessProvider store={createFakeStore([{ permission: ADMIN }])}>
<AccessProvider permissions={[{ permission: ADMIN }]}>
<ThemeProvider theme={theme}>
<UIProvider>
<MemoryRouter initialEntries={['/test']}>
@ -38,7 +37,7 @@ test('renders correctly without permission', () => {
<MemoryRouter>
<ThemeProvider theme={theme}>
<UIProvider>
<AccessProvider store={createFakeStore([])}>
<AccessProvider permissions={[{ permission: ADMIN }]}>
<ApplicationEdit
fetchApplication={() => Promise.resolve({})}
storeApplicationMetaData={jest.fn()}
@ -101,9 +100,7 @@ test('renders correctly with permissions', () => {
<MemoryRouter>
<ThemeProvider theme={theme}>
<UIProvider>
<AccessProvider
store={createFakeStore([{ permission: ADMIN }])}
>
<AccessProvider permissions={[{ permission: ADMIN }]}>
<ApplicationEdit
fetchApplication={() => Promise.resolve({})}
storeApplicationMetaData={jest.fn()}

View File

@ -1,37 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Snackbar, IconButton } from '@material-ui/core';
import { Close, QuestionAnswer } from '@material-ui/icons';
const ErrorComponent = ({ errors, muteError }) => {
const showError = errors.length > 0;
const error = showError ? errors[0] : undefined;
return (
<Snackbar
action={
<React.Fragment>
<IconButton size="small" aria-label="close" color="inherit">
<Close />
</IconButton>
</React.Fragment>
}
open={showError}
onClose={() => muteError(error)}
autoHideDuration={10000}
message={
<div key={error}>
<QuestionAnswer />
{error}
</div>
}
/>
);
};
ErrorComponent.propTypes = {
errors: PropTypes.array.isRequired,
muteError: PropTypes.func.isRequired,
};
export default ErrorComponent;

View File

@ -1,19 +0,0 @@
import { connect } from 'react-redux';
import ErrorComponent from './error-component';
import { muteError } from '../../store/error/actions';
const mapDispatchToProps = {
muteError,
};
const mapStateToProps = state => {
return {
errors: state.error
.get('list')
.toArray()
.reverse()
}
};
export default connect(mapStateToProps, mapDispatchToProps)(ErrorComponent);

View File

@ -1,7 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useState, useRef, useEffect } from 'react';
import { Link, useParams } from 'react-router-dom';
import { Link, useHistory, useParams } from 'react-router-dom';
import {
Button,
@ -23,17 +22,18 @@ import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureAp
import useFeature from '../../../hooks/api/getters/useFeature/useFeature';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
const CopyFeature = props => {
export const CopyFeatureToggle = () => {
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
const [replaceGroupId, setReplaceGroupId] = useState(true);
const [apiError, setApiError] = useState('');
const [nameError, setNameError] = useState(undefined);
const [newToggleName, setNewToggleName] = useState();
const { cloneFeatureToggle } = useFeatureApi();
const { cloneFeatureToggle, validateFeatureToggleName } = useFeatureApi();
const inputRef = useRef();
const { name: copyToggleName, id: projectId } = useParams();
const { feature } = useFeature(projectId, copyToggleName);
const { uiConfig } = useUiConfig();
const history = useHistory();
useEffect(() => {
inputRef.current?.focus();
@ -50,7 +50,7 @@ const CopyFeature = props => {
const onValidateName = async () => {
try {
await props.validateName(newToggleName);
await validateFeatureToggleName(newToggleName);
setNameError(undefined);
} catch (err) {
@ -70,7 +70,7 @@ const CopyFeature = props => {
name: newToggleName,
replaceGroupId,
});
props.history.push(
history.push(
getTogglePath(projectId, newToggleName, uiConfig.flags.E)
);
} catch (e) {
@ -137,10 +137,3 @@ const CopyFeature = props => {
</Paper>
);
};
CopyFeature.propTypes = {
history: PropTypes.object.isRequired,
validateName: PropTypes.func.isRequired,
};
export default CopyFeature;

View File

@ -1,18 +0,0 @@
import { connect } from 'react-redux';
import CopyFeatureComponent from './CopyFeature';
import { validateName } from '../../../store/feature-toggle/actions';
const mapStateToProps = (state, props) => ({
history: props.history,
});
const mapDispatchToProps = dispatch => ({
validateName,
});
const FormAddContainer = connect(
mapStateToProps,
mapDispatchToProps
)(CopyFeatureComponent);
export default FormAddContainer;

View File

@ -3,7 +3,6 @@ import classnames from 'classnames';
import { makeStyles } from '@material-ui/core/styles';
import { Grid } from '@material-ui/core';
import styles from '../../styles.module.scss';
import ErrorContainer from '../../error/error-container';
import Header from '../../menu/Header/Header';
import Footer from '../../menu/Footer/Footer';
import Proclamation from '../../common/Proclamation/Proclamation';
@ -27,7 +26,7 @@ const useStyles = makeStyles(theme => ({
}));
interface IMainLayoutProps {
children: ReactNode
children: ReactNode;
}
export const MainLayout = ({ children }: IMainLayoutProps) => {
@ -48,7 +47,6 @@ export const MainLayout = ({ children }: IMainLayoutProps) => {
<Proclamation toast={uiConfig.toast} />
{children}
</div>
<ErrorContainer />
</Grid>
<div style={{ overflow: 'hidden' }}>
<div

View File

@ -1,4 +1,3 @@
import CopyFeatureToggle from '../../page/features/copy';
import { FeatureToggleListContainer } from '../feature/FeatureToggleList/FeatureToggleListContainer';
import { StrategyForm } from '../strategies/StrategyForm/StrategyForm';
import { StrategyView } from '../../component/strategies/StrategyView/StrategyView';
@ -45,6 +44,7 @@ import ContextList from '../context/ContextList/ContextList';
import RedirectFeatureView from '../feature/RedirectFeatureView/RedirectFeatureView';
import { CreateAddon } from '../addons/CreateAddon/CreateAddon';
import { EditAddon } from '../addons/EditAddon/EditAddon';
import { CopyFeatureToggle } from '../feature/CopyFeature/CopyFeature';
export const routes = [
// Project

View File

@ -6,7 +6,7 @@ import { useAuthPermissions } from '../../../hooks/api/getters/useAuth/useAuthPe
interface IAccessProviderProps {
children: ReactNode;
permissions: IPermission[];
permissions?: IPermission[];
}
// TODO(olav): Mock useAuth instead of using props.permissions in tests.
@ -77,8 +77,8 @@ const checkPermission = (
environment?: string
): boolean => {
if (!permission) {
console.warn(`Missing permission for AccessProvider: ${permission}`)
return false
console.warn(`Missing permission for AccessProvider: ${permission}`);
return false;
}
if (p.permission === ADMIN) {

View File

@ -28,7 +28,51 @@ exports[`renders correctly with one strategy 1`] = `
</div>
<div
className="makeStyles-headerActions-9"
/>
>
<span
aria-describedby={null}
className=""
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="Add new strategy"
>
<button
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
data-test="ADD_NEW_STRATEGY_ID"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={0}
type="button"
>
<span
className="MuiButton-label"
>
Add new strategy
<span
className="MuiButton-endIcon MuiButton-iconSizeMedium"
/>
</span>
<span
className="MuiTouchRipple-root"
/>
</button>
</span>
</div>
</div>
</div>
<div
@ -91,11 +135,11 @@ exports[`renders correctly with one strategy 1`] = `
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="You don't have access to perform this operation"
title="Deprecate activation strategy"
>
<button
className="MuiButtonBase-root MuiIconButton-root Mui-disabled Mui-disabled"
disabled={true}
className="MuiButtonBase-root MuiIconButton-root"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onDragLeave={[Function]}
@ -108,7 +152,7 @@ exports[`renders correctly with one strategy 1`] = `
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={-1}
tabIndex={0}
type="button"
>
<span
@ -125,9 +169,56 @@ exports[`renders correctly with one strategy 1`] = `
/>
</svg>
</span>
<span
className="MuiTouchRipple-root"
/>
</button>
</span>
</div>
<div
aria-describedby={null}
className=""
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="You cannot delete a built-in strategy"
>
<button
className="MuiButtonBase-root MuiIconButton-root Mui-disabled Mui-disabled"
disabled={true}
onBlur={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={-1}
type="button"
>
<span
className="MuiIconButton-label"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</span>
</button>
</div>
</li>
</ul>
</div>
@ -162,7 +253,51 @@ exports[`renders correctly with one strategy without permissions 1`] = `
</div>
<div
className="makeStyles-headerActions-9"
/>
>
<span
aria-describedby={null}
className=""
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="Add new strategy"
>
<button
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
data-test="ADD_NEW_STRATEGY_ID"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={0}
type="button"
>
<span
className="MuiButton-label"
>
Add new strategy
<span
className="MuiButton-endIcon MuiButton-iconSizeMedium"
/>
</span>
<span
className="MuiTouchRipple-root"
/>
</button>
</span>
</div>
</div>
</div>
<div
@ -225,11 +360,11 @@ exports[`renders correctly with one strategy without permissions 1`] = `
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="You don't have access to perform this operation"
title="Deprecate activation strategy"
>
<button
className="MuiButtonBase-root MuiIconButton-root Mui-disabled Mui-disabled"
disabled={true}
className="MuiButtonBase-root MuiIconButton-root"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onDragLeave={[Function]}
@ -242,7 +377,7 @@ exports[`renders correctly with one strategy without permissions 1`] = `
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={-1}
tabIndex={0}
type="button"
>
<span
@ -259,9 +394,56 @@ exports[`renders correctly with one strategy without permissions 1`] = `
/>
</svg>
</span>
<span
className="MuiTouchRipple-root"
/>
</button>
</span>
</div>
<div
aria-describedby={null}
className=""
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="You cannot delete a built-in strategy"
>
<button
className="MuiButtonBase-root MuiIconButton-root Mui-disabled Mui-disabled"
disabled={true}
onBlur={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={-1}
type="button"
>
<span
className="MuiIconButton-label"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</span>
</button>
</div>
</li>
</ul>
</div>

View File

@ -4,7 +4,6 @@ import { StrategiesList } from '../StrategiesList/StrategiesList';
import renderer from 'react-test-renderer';
import theme from '../../../themes/main-theme';
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
import { createFakeStore } from '../../../accessStoreFake';
import { ADMIN } from '../../providers/AccessProvider/permissions';
import UIProvider from '../../providers/UIProvider/UIProvider';
@ -17,7 +16,7 @@ test('renders correctly with one strategy', () => {
<MemoryRouter>
<ThemeProvider theme={theme}>
<UIProvider>
<AccessProvider store={createFakeStore()}>
<AccessProvider permissions={[{ permission: ADMIN }]}>
<StrategiesList
strategies={[strategy]}
fetchStrategies={jest.fn()}
@ -44,9 +43,7 @@ test('renders correctly with one strategy without permissions', () => {
<MemoryRouter>
<ThemeProvider theme={theme}>
<UIProvider>
<AccessProvider
store={createFakeStore([{ permission: ADMIN }])}
>
<AccessProvider permissions={[{ permission: ADMIN }]}>
<StrategiesList
strategies={[strategy]}
fetchStrategies={jest.fn()}

View File

@ -5,7 +5,6 @@ import { useStyles } from './EditProfile.styles';
import { useCommonStyles } from '../../../../common.styles';
import PasswordChecker from '../../common/ResetPasswordForm/PasswordChecker/PasswordChecker';
import PasswordMatcher from '../../common/ResetPasswordForm/PasswordMatcher/PasswordMatcher';
import { headers } from '../../../../store/api-helper';
import { Alert } from '@material-ui/lab';
import ConditionallyRender from '../../../common/ConditionallyRender';
import useLoading from '../../../../hooks/useLoading';
@ -17,6 +16,7 @@ import {
} from '../../../../constants/statusCodes';
import { formatApiPath } from '../../../../utils/format-path';
import PasswordField from '../../../common/PasswordField/PasswordField';
import { headers } from '../../../../utils/api-utils';
interface IEditProfileProps {
setEditingProfile: React.Dispatch<React.SetStateAction<boolean>>;

View File

@ -4,7 +4,7 @@ import {
AuthenticationError,
ForbiddenError,
NotFoundError,
} from '../../../../store/api-helper';
} from '../../../../utils/api-utils';
export const handleBadRequest = async (
setErrors?: Dispatch<SetStateAction<{}>>,

View File

@ -12,7 +12,7 @@ import {
ForbiddenError,
headers,
NotFoundError,
} from '../../../../store/api-helper';
} from '../../../../utils/api-utils';
import { formatApiPath } from '../../../../utils/format-path';
interface IUseAPI {

View File

@ -1,3 +1,4 @@
import { headers } from '../../../../utils/api-utils';
import useAPI from '../useApi/useApi';
type PasswordLogin = (
@ -16,24 +17,36 @@ interface IUseAuthApiOutput {
}
export const useAuthApi = (): IUseAuthApiOutput => {
const { makeRequest, createRequest, errors, loading } = useAPI({
const { makeRequest, errors, loading } = useAPI({
propagateErrors: true,
});
const passwordAuth = (path: string, username: string, password: string) => {
const req = createRequest(ensureRelativePath(path), {
method: 'POST',
body: JSON.stringify({ username, password }),
});
const req = {
caller: () => {
return fetch(path, {
headers,
method: 'POST',
body: JSON.stringify({ username, password }),
});
},
id: 'passwordAuth',
};
return makeRequest(req.caller, req.id);
};
const emailAuth = (path: string, email: string) => {
const req = createRequest(ensureRelativePath(path), {
method: 'POST',
body: JSON.stringify({ email }),
});
const req = {
caller: () => {
return fetch(path, {
headers,
method: 'POST',
body: JSON.stringify({ email }),
});
},
id: 'emailAuth',
};
return makeRequest(req.caller, req.id);
};

View File

@ -4,6 +4,8 @@ import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
const useUiBootstrap = (options: SWRConfiguration = {}) => {
// The point of the bootstrap is to get multiple datasets in one call. Therefore,
// this needs to be refactored to seed other hooks with the correct data.
const BOOTSTRAP_CACHE_KEY = `api/admin/ui-bootstrap`;
const fetcher = () => {

View File

@ -5,58 +5,34 @@ import './app.css';
import ReactDOM from 'react-dom';
import { Route, BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import { ThemeProvider, CssBaseline } from '@material-ui/core';
import thunkMiddleware from 'redux-thunk';
import { createStore, applyMiddleware, compose } from 'redux';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { StylesProvider } from '@material-ui/core/styles';
import mainTheme from './themes/main-theme';
import store from './store';
import App from './component/AppContainer';
import { App } from './component/App';
import ScrollToTop from './component/scroll-to-top';
import { writeWarning } from './security-logger';
import AccessProvider from './component/providers/AccessProvider/AccessProvider';
import { getBasePath } from './utils/format-path';
import UIProvider from './component/providers/UIProvider/UIProvider';
let composeEnhancers;
if (
process.env.NODE_ENV !== 'production' &&
(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
) {
composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
} else {
composeEnhancers = compose;
writeWarning();
}
const unleashStore = createStore(
store,
composeEnhancers(applyMiddleware(thunkMiddleware))
);
ReactDOM.render(
<Provider store={unleashStore}>
<DndProvider backend={HTML5Backend}>
<UIProvider>
<AccessProvider>
<Router basename={`${getBasePath()}`}>
<ThemeProvider theme={mainTheme}>
<StylesProvider injectFirst>
<CssBaseline />
<ScrollToTop>
<Route path="/" component={App} />
</ScrollToTop>
</StylesProvider>
</ThemeProvider>
</Router>
</AccessProvider>
</UIProvider>
</DndProvider>
</Provider>,
<DndProvider backend={HTML5Backend}>
<UIProvider>
<AccessProvider>
<Router basename={`${getBasePath()}`}>
<ThemeProvider theme={mainTheme}>
<StylesProvider injectFirst>
<CssBaseline />
<ScrollToTop>
<Route path="/" component={App} />
</ScrollToTop>
</StylesProvider>
</ThemeProvider>
</Router>
</AccessProvider>
</UIProvider>
</DndProvider>,
document.getElementById('app')
);

View File

@ -1,18 +0,0 @@
import React from 'react';
import CopyFeatureToggleForm from '../../component/feature/CopyFeature';
import PropTypes from 'prop-types';
const render = ({ history, match: { params } }) => (
<CopyFeatureToggleForm
title="Copy feature toggle"
history={history}
copyToggleName={params.copyToggle}
/>
);
render.propTypes = {
history: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
};
export default render;

View File

@ -1,62 +0,0 @@
import {
START_FETCH_FEATURE_TOGGLES,
FETCH_FEATURE_TOGGLES_SUCCESS,
FETCH_FEATURE_TOGGLE_ERROR,
RESET_LOADING,
} from '../feature-toggle/actions';
const apiCalls = (
state = {
fetchTogglesState: {
loading: false,
success: false,
error: null,
count: 0,
},
},
action
) => {
switch (action.type) {
case START_FETCH_FEATURE_TOGGLES:
if (state.fetchTogglesState.count > 0) return state;
return {
...state,
fetchTogglesState: {
...state.fetchTogglesState,
loading: true,
success: false,
error: null,
},
};
case FETCH_FEATURE_TOGGLES_SUCCESS:
return {
...state,
fetchTogglesState: {
...state.fetchTogglesState,
loading: false,
success: true,
error: null,
count: (state.fetchTogglesState.count += 1),
},
};
case FETCH_FEATURE_TOGGLE_ERROR:
return {
...state,
fetchTogglesState: {
...state.fetchTogglesState,
loading: false,
success: false,
error: true,
},
};
case RESET_LOADING:
return {
...state,
fetchTogglesState: { ...state.fetchTogglesState, count: 0 },
};
default:
return state;
}
};
export default apiCalls;

View File

@ -1,110 +0,0 @@
const defaultErrorMessage = 'Unexpected exception when talking to unleash-api';
function extractJoiMsg(body) {
return body.details.length > 0
? body.details[0].message
: defaultErrorMessage;
}
function extractLegacyMsg(body) {
return body && body.length > 0 ? body[0].msg : defaultErrorMessage;
}
class ServiceError extends Error {
constructor(statusCode = 500) {
super(defaultErrorMessage);
this.name = 'ServiceError';
this.statusCode = statusCode;
}
}
export class AuthenticationError extends Error {
constructor(statusCode, body) {
super('Authentication required');
this.name = 'AuthenticationError';
this.statusCode = statusCode;
this.body = body;
}
}
export class ForbiddenError extends Error {
constructor(statusCode, body = {}) {
super(
body.details?.length > 0
? body.details[0].message
: 'You cannot perform this action'
);
this.name = 'ForbiddenError';
this.statusCode = statusCode;
this.body = body;
}
}
export class BadRequestError extends Error {
constructor(statusCode, body = {}) {
super(
body.details?.length > 0 ? body.details[0].message : 'Bad request'
);
this.name = 'BadRequestError';
this.statusCode = statusCode;
this.body = body;
}
}
export class NotFoundError extends Error {
constructor(statusCode) {
super(
'The requested resource could not be found but may be available in the future'
);
this.name = 'NotFoundError';
this.statusCode = statusCode;
}
}
export function throwIfNotSuccess(response) {
if (!response.ok) {
if (response.status === 401) {
return new Promise((resolve, reject) => {
response
.json()
.then(body =>
reject(new AuthenticationError(response.status, body))
);
});
} else if (response.status === 403) {
return new Promise((resolve, reject) => {
response
.json()
.then(body =>
reject(new ForbiddenError(response.status, body))
);
});
} else if (response.status === 404) {
return new Promise((resolve, reject) => {
reject(new NotFoundError(response.status));
});
} else if (response.status > 399 && response.status < 501) {
return new Promise((resolve, reject) => {
response
.json()
.then(body => {
const errorMsg =
body && body.isJoi
? extractJoiMsg(body)
: extractLegacyMsg(body);
let error = new Error(errorMsg);
error.statusCode = response.status;
reject(error);
})
.catch(() => reject(new Error(defaultErrorMessage)));
});
} else {
return Promise.reject(new ServiceError(response.status));
}
}
return Promise.resolve(response);
}
export const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
};

View File

@ -1,6 +0,0 @@
export const MUTE_ERRORS = 'MUTE_ERRORS';
export const MUTE_ERROR = 'MUTE_ERROR';
export const muteErrors = () => ({ type: MUTE_ERRORS });
export const muteError = error => ({ type: MUTE_ERROR, error });

View File

@ -1,57 +0,0 @@
import { List, Map as $Map } from 'immutable';
import { MUTE_ERROR } from './actions';
import {
ERROR_FETCH_FEATURE_TOGGLES,
ERROR_CREATING_FEATURE_TOGGLE,
ERROR_REMOVE_FEATURE_TOGGLE,
ERROR_UPDATE_FEATURE_TOGGLE,
UPDATE_FEATURE_TOGGLE_STRATEGIES,
UPDATE_FEATURE_TOGGLE,
} from '../feature-toggle/actions';
import { FORBIDDEN } from '../util';
const debug = require('debug')('unleash:error-store');
function getInitState() {
return new $Map({
list: new List(),
});
}
function addErrorIfNotAlreadyInList(state, error) {
debug('Got error', error);
if (state.get('list').indexOf(error) < 0) {
return state.update('list', list => list.push(error));
}
return state;
}
const strategies = (state = getInitState(), action) => {
switch (action.type) {
case ERROR_CREATING_FEATURE_TOGGLE:
case ERROR_REMOVE_FEATURE_TOGGLE:
case ERROR_FETCH_FEATURE_TOGGLES:
case ERROR_UPDATE_FEATURE_TOGGLE:
return addErrorIfNotAlreadyInList(state, action.error.message);
case FORBIDDEN:
return addErrorIfNotAlreadyInList(
state,
action.error.message || '403 Forbidden'
);
case MUTE_ERROR:
return state.update('list', list =>
list.remove(list.indexOf(action.error))
);
// This reducer controls not only errors, but general information and success
// messages. This can be a little misleading, given it's naming. We should
// revise how this works in a future update.
case UPDATE_FEATURE_TOGGLE:
case UPDATE_FEATURE_TOGGLE_STRATEGIES:
return addErrorIfNotAlreadyInList(state, action.info);
default:
return state;
}
};
export default strategies;

View File

@ -1,235 +0,0 @@
import api from './api';
import { dispatchError } from '../util';
import { MUTE_ERROR } from '../error/actions';
const debug = require('debug')('unleash:feature-actions');
export const ADD_FEATURE_TOGGLE = 'ADD_FEATURE_TOGGLE';
export const COPY_FEATURE_TOGGLE = 'COPY_FEATURE_TOGGLE';
export const REMOVE_FEATURE_TOGGLE = 'REMOVE_FEATURE_TOGGLE';
export const UPDATE_FEATURE_TOGGLE = 'UPDATE_FEATURE_TOGGLE';
export const TOGGLE_FEATURE_TOGGLE = 'TOGGLE_FEATURE_TOGGLE';
export const START_FETCH_FEATURE_TOGGLES = 'START_FETCH_FEATURE_TOGGLES';
export const START_UPDATE_FEATURE_TOGGLE = 'START_UPDATE_FEATURE_TOGGLE';
export const START_CREATE_FEATURE_TOGGLE = 'START_CREATE_FEATURE_TOGGLE';
export const FETCH_FEATURE_TOGGLES_SUCCESS = 'FETCH_FEATURE_TOGGLES_SUCCESS';
export const START_REMOVE_FEATURE_TOGGLE = 'START_REMOVE_FEATURE_TOGGLE';
export const RECEIVE_FEATURE_TOGGLES = 'RECEIVE_FEATURE_TOGGLES';
export const ERROR_FETCH_FEATURE_TOGGLES = 'ERROR_FETCH_FEATURE_TOGGLES';
export const ERROR_CREATING_FEATURE_TOGGLE = 'ERROR_CREATING_FEATURE_TOGGLE';
export const ERROR_UPDATE_FEATURE_TOGGLE = 'ERROR_UPDATE_FEATURE_TOGGLE';
export const ERROR_REMOVE_FEATURE_TOGGLE = 'ERROR_REMOVE_FEATURE_TOGGLE';
export const UPDATE_FEATURE_TOGGLE_STRATEGIES =
'UPDATE_FEATURE_TOGGLE_STRATEGIES';
export const FETCH_FEATURE_TOGGLE_ERROR = 'FETCH_FEATURE_TOGGLE_ERROR';
export const RESET_LOADING = 'RESET_LOADING';
export const RECEIVE_FEATURE_TOGGLE = 'RECEIVE_FEATURE_TOGGLE';
export const START_FETCH_FEATURE_TOGGLE = 'START_FETCH_FEATURE_TOGGLE';
export const ERROR_FETCH_FEATURE_TOGGLE = 'START_FETCH_FEATURE_TOGGLE';
export function toggleFeature(enable, name) {
debug('Toggle feature toggle ', name);
return dispatch => {
dispatch(requestToggleFeatureToggle(enable, name));
};
}
export function setStale(stale, name) {
debug('Set stale property on feature toggle ', name);
return dispatch => {
dispatch(requestSetStaleFeatureToggle(stale, name));
};
}
export function editFeatureToggle(featureToggle) {
debug('Update feature toggle ', featureToggle);
return dispatch => {
dispatch(requestUpdateFeatureToggle(featureToggle));
};
}
function receiveFeatureToggles(json) {
debug('reviced feature toggles', json);
return {
type: RECEIVE_FEATURE_TOGGLES,
featureToggles: json.features.map(features => features),
receivedAt: Date.now(),
};
}
function receiveFeatureToggle(featureToggle) {
debug('reviced feature toggle', featureToggle);
return {
type: RECEIVE_FEATURE_TOGGLE,
featureToggle,
receivedAt: Date.now(),
};
}
export function fetchFeatureToggles() {
debug('Start fetching feature toggles');
return dispatch => {
dispatch({ type: START_FETCH_FEATURE_TOGGLES });
return api
.fetchAll()
.then(json => {
dispatch({ type: FETCH_FEATURE_TOGGLES_SUCCESS });
dispatch(receiveFeatureToggles(json));
})
.catch(() => {
dispatch({ type: FETCH_FEATURE_TOGGLE_ERROR });
dispatchError(dispatch, ERROR_FETCH_FEATURE_TOGGLES);
});
};
}
export function fetchFeatureToggle(name) {
debug('Start fetching feature toggles');
return dispatch => {
dispatch({ type: START_FETCH_FEATURE_TOGGLE });
return api
.fetchFeatureToggle(name)
.then(json => dispatch(receiveFeatureToggle(json)))
.catch(dispatchError(dispatch, ERROR_FETCH_FEATURE_TOGGLE));
};
}
export function createFeatureToggles(featureToggle) {
return dispatch => {
dispatch({ type: START_CREATE_FEATURE_TOGGLE });
return api
.create(featureToggle)
.then(res => res.json())
.then(createdFeature => {
dispatch({
type: ADD_FEATURE_TOGGLE,
featureToggle: createdFeature,
});
})
.catch(e => {
dispatchError(dispatch, ERROR_CREATING_FEATURE_TOGGLE);
throw e;
});
};
}
export function requestToggleFeatureToggle(enable, name) {
return dispatch => {
dispatch({ type: START_UPDATE_FEATURE_TOGGLE });
return api
.toggle(enable, name)
.then(() => dispatch({ type: TOGGLE_FEATURE_TOGGLE, name }))
.catch(dispatchError(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
};
}
export function requestSetStaleFeatureToggle(stale, name) {
return dispatch => {
dispatch({ type: START_UPDATE_FEATURE_TOGGLE });
return api
.setStale(stale, name)
.then(featureToggle => {
const info = `${name} marked as ${stale ? 'Stale' : 'Active'}.`;
setTimeout(
() => dispatch({ type: MUTE_ERROR, error: info }),
1000
);
dispatch({ type: UPDATE_FEATURE_TOGGLE, featureToggle, info });
})
.catch(dispatchError(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
};
}
export function requestUpdateFeatureToggle(featureToggle) {
return dispatch => {
dispatch({ type: START_UPDATE_FEATURE_TOGGLE });
return api
.update(featureToggle)
.then(() => {
const info = `${featureToggle.name} successfully updated!`;
setTimeout(
() => dispatch({ type: MUTE_ERROR, error: info }),
1000
);
dispatch({ type: UPDATE_FEATURE_TOGGLE, featureToggle, info });
})
.catch(dispatchError(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
};
}
export function requestUpdateFeatureToggleStrategies(
featureToggle,
newStrategies
) {
return dispatch => {
featureToggle.strategies = newStrategies;
dispatch({ type: START_UPDATE_FEATURE_TOGGLE });
return api
.update(featureToggle)
.then(() => {
const info = `${featureToggle.name} successfully updated!`;
setTimeout(
() => dispatch({ type: MUTE_ERROR, error: info }),
1000
);
return dispatch({
type: UPDATE_FEATURE_TOGGLE_STRATEGIES,
featureToggle,
info,
});
})
.catch(dispatchError(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
};
}
export function requestUpdateFeatureToggleVariants(featureToggle, newVariants) {
return dispatch => {
const newFeature = { ...featureToggle };
newFeature.variants = newVariants;
dispatch({ type: START_UPDATE_FEATURE_TOGGLE });
return api
.update(newFeature)
.then(() => {
const info = `${newFeature.name} successfully updated!`;
setTimeout(
() => dispatch({ type: MUTE_ERROR, error: info }),
1000
);
return dispatch({
type: UPDATE_FEATURE_TOGGLE_STRATEGIES,
featureToggle: newFeature,
info,
});
})
.catch(e => {
dispatchError(dispatch, ERROR_UPDATE_FEATURE_TOGGLE);
throw e;
});
};
}
export function removeFeatureToggle(featureToggleName) {
return dispatch => {
dispatch({ type: START_REMOVE_FEATURE_TOGGLE });
return api
.remove(featureToggleName)
.then(() =>
dispatch({ type: REMOVE_FEATURE_TOGGLE, featureToggleName })
)
.catch(dispatchError(dispatch, ERROR_REMOVE_FEATURE_TOGGLE));
};
}
export function validateName(featureToggleName) {
return api.validate({ name: featureToggleName });
}

View File

@ -1,100 +0,0 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess, headers } from '../api-helper';
const URI = formatApiPath('api/admin/features');
function validateToggle(featureToggle) {
return new Promise((resolve, reject) => {
if (
!featureToggle.strategies ||
featureToggle.strategies.length === 0
) {
reject(new Error('You must add at least one activation strategy'));
} else {
resolve(featureToggle);
}
});
}
function fetchAll() {
return fetch(URI, { credentials: 'include' })
.then(throwIfNotSuccess)
.then(response => response.json());
}
function fetchFeatureToggle(name) {
return fetch(`${URI}/${name}`, { credentials: 'include' })
.then(throwIfNotSuccess)
.then(response => response.json());
}
async function create(featureToggle) {
await validateToggle(featureToggle);
return fetch(URI, {
method: 'POST',
headers,
credentials: 'include',
body: JSON.stringify(featureToggle),
}).then(throwIfNotSuccess);
}
function validate(featureToggle) {
return fetch(`${URI}/validate`, {
method: 'POST',
headers,
credentials: 'include',
body: JSON.stringify(featureToggle),
}).then(throwIfNotSuccess);
}
function update(featureToggle) {
return validateToggle(featureToggle)
.then(() => {
return fetch(`${URI}/${featureToggle.name}`, {
method: 'PUT',
headers,
credentials: 'include',
body: JSON.stringify(featureToggle),
});
})
.then(throwIfNotSuccess);
}
function toggle(enable, name) {
const action = enable ? 'on' : 'off';
return fetch(`${URI}/${name}/toggle/${action}`, {
method: 'POST',
headers,
credentials: 'include',
}).then(throwIfNotSuccess);
}
function setStale(isStale, name) {
const action = isStale ? 'on' : 'off';
return fetch(`${URI}/${name}/stale/${action}`, {
method: 'POST',
headers,
credentials: 'include',
})
.then(throwIfNotSuccess)
.then(response => response.json());
}
function remove(featureToggleName) {
return fetch(`${URI}/${featureToggleName}`, {
method: 'DELETE',
credentials: 'include',
}).then(throwIfNotSuccess);
}
export default {
fetchAll,
fetchFeatureToggle,
create,
validate,
update,
toggle,
setStale,
remove,
};

View File

@ -1,68 +0,0 @@
import { List, Map as $Map } from 'immutable';
import {
ADD_FEATURE_TOGGLE,
RECEIVE_FEATURE_TOGGLES,
RECEIVE_FEATURE_TOGGLE,
UPDATE_FEATURE_TOGGLE,
UPDATE_FEATURE_TOGGLE_STRATEGIES,
REMOVE_FEATURE_TOGGLE,
TOGGLE_FEATURE_TOGGLE,
} from './actions';
const debug = require('debug')('unleash:feature-store');
const features = (state = new List([]), action) => {
switch (action.type) {
case ADD_FEATURE_TOGGLE:
debug(ADD_FEATURE_TOGGLE, action);
return state.push(new $Map(action.featureToggle));
case REMOVE_FEATURE_TOGGLE:
debug(REMOVE_FEATURE_TOGGLE, action);
return state.filter(
toggle => toggle.get('name') !== action.featureToggleName
);
case TOGGLE_FEATURE_TOGGLE:
debug(TOGGLE_FEATURE_TOGGLE, action);
return state.map(toggle => {
if (toggle.get('name') === action.name) {
return toggle.set('enabled', !toggle.get('enabled'));
} else {
return toggle;
}
});
case UPDATE_FEATURE_TOGGLE_STRATEGIES:
debug(UPDATE_FEATURE_TOGGLE_STRATEGIES, action);
return state.map(toggle => {
if (toggle.get('name') === action.featureToggle.name) {
return new $Map(action.featureToggle);
} else {
return toggle;
}
});
case UPDATE_FEATURE_TOGGLE:
debug(UPDATE_FEATURE_TOGGLE, action);
return state.map(toggle => {
if (toggle.get('name') === action.featureToggle.name) {
return new $Map(action.featureToggle);
} else {
return toggle;
}
});
case RECEIVE_FEATURE_TOGGLE:
debug(RECEIVE_FEATURE_TOGGLE, action);
return state.map(toggle => {
if (toggle.get('name') === action.featureToggle.name) {
return new $Map(action.featureToggle);
} else {
return toggle;
}
});
case RECEIVE_FEATURE_TOGGLES:
debug(RECEIVE_FEATURE_TOGGLES, action);
return new List(action.featureToggles.map($Map));
default:
return state;
}
};
export default features;

View File

@ -1,12 +0,0 @@
import { combineReducers } from 'redux';
import features from './feature-toggle';
import error from './error';
import apiCalls from './api-calls';
const unleashStore = combineReducers({
features,
error,
apiCalls,
});
export default unleashStore;

View File

@ -1,13 +0,0 @@
import api from './api';
import { dispatchError } from '../util';
export const RECEIVE_BOOTSTRAP = 'RECEIVE_CONFIG';
export const ERROR_RECEIVE_BOOTSTRAP = 'ERROR_RECEIVE_CONFIG';
export function fetchUiBootstrap() {
return dispatch =>
api
.fetchUIBootstrap()
.then(json => {})
.catch(dispatchError(dispatch, ERROR_RECEIVE_BOOTSTRAP));
}

View File

@ -1,14 +0,0 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess } from '../api-helper';
const URI = formatApiPath('api/admin/ui-bootstrap');
function fetchUIBootstrap() {
return fetch(URI, { credentials: 'include' })
.then(throwIfNotSuccess)
.then(response => response.json());
}
export default {
fetchUIBootstrap,
};

View File

@ -1,20 +0,0 @@
export const AUTH_REQUIRED = 'AUTH_REQUIRED';
export const FORBIDDEN = 'FORBIDDEN';
export function dispatchError(dispatch, type) {
return error => {
switch (error.statusCode) {
case 401:
dispatch({ type: AUTH_REQUIRED, error, receivedAt: Date.now() });
break;
case 403:
dispatch({ type: FORBIDDEN, error, receivedAt: Date.now() });
break;
default:
dispatch({ type, error, receivedAt: Date.now() });
break;
}
};
}
export const success = (dispatch, type, val) => value => dispatch({ type, value: val ? val : value });

View File

@ -0,0 +1,67 @@
const defaultErrorMessage = 'Unexpected exception when talking to unleash-api';
export const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
};
export const extractJoiMsg = (body: any) => {
return body.details.length > 0
? body.details[0].message
: defaultErrorMessage;
};
export const extractLegacyMsg = (body: any[]) => {
return body && body.length > 0 ? body[0].msg : defaultErrorMessage;
};
export class ServiceError extends Error {
constructor(statusCode = 500) {
super(defaultErrorMessage);
this.name = 'ServiceError';
this.statusCode = statusCode;
}
}
export class AuthenticationError extends Error {
constructor(statusCode, body) {
super('Authentication required');
this.name = 'AuthenticationError';
this.statusCode = statusCode;
this.body = body;
}
}
export class ForbiddenError extends Error {
constructor(statusCode, body = {}) {
super(
body.details?.length > 0
? body.details[0].message
: 'You cannot perform this action'
);
this.name = 'ForbiddenError';
this.statusCode = statusCode;
this.body = body;
}
}
export class BadRequestError extends Error {
constructor(statusCode, body = {}) {
super(
body.details?.length > 0 ? body.details[0].message : 'Bad request'
);
this.name = 'BadRequestError';
this.statusCode = statusCode;
this.body = body;
}
}
export class NotFoundError extends Error {
constructor(statusCode) {
super(
'The requested resource could not be found but may be available in the future'
);
this.name = 'NotFoundError';
this.statusCode = statusCode;
}
}

View File

@ -55,7 +55,7 @@
semver "^5.4.1"
source-map "^0.5.0"
"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.7.5", "@babel/core@^7.8.4":
"@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.7.5", "@babel/core@^7.8.4":
version "7.13.14"
resolved "https://registry.npmjs.org/@babel/core/-/core-7.13.14.tgz"
integrity sha512-wZso/vyF4ki0l0znlgM4inxbdrUvCb+cVz8grxDq+6C9k6qbqoIJteQOKicaKjCipU3ISV+XedCqpL2RJJVehA==
@ -1198,14 +1198,14 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.13.10"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz"
integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.12.13", "@babel/runtime@^7.15.4":
"@babel/runtime@^7.12.13":
version "7.15.4"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz"
integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==
@ -2012,14 +2012,6 @@
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64"
integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==
"@types/hoist-non-react-statics@^3.3.0":
version "3.3.1"
resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/html-minifier-terser@^5.0.0":
version "5.1.1"
resolved "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz"
@ -2148,16 +2140,6 @@
dependencies:
"@types/react" "*"
"@types/react-redux@^7.1.20":
version "7.1.20"
resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.20.tgz"
integrity sha512-q42es4c8iIeTgcnB+yJgRTTzftv3eYYvCZOh1Ckn2eX/3o5TdsQYKUWpLoLuGlcY/p+VAhV9IOEZJcWk/vfkXw==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-router-dom@5.3.3":
version "5.3.3"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83"
@ -2789,7 +2771,7 @@ anymatch@^3.0.3, anymatch@~3.1.1:
anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
dependencies:
normalize-path "^3.0.0"
@ -2866,11 +2848,6 @@ array-includes@^3.1.1, array-includes@^3.1.2, array-includes@^3.1.3:
get-intrinsic "^1.1.1"
is-string "^1.0.5"
array-move@3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/array-move/-/array-move-3.0.1.tgz"
integrity sha512-H3Of6NIn2nNU1gsVDqDnYKY/LCdWvCMMOWifNGhKcVQgiZ6nOek39aESOvro6zmueP07exSl93YLvkN4fZOkSg==
array-union@^1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz"
@ -3721,9 +3698,9 @@ check-types@^11.1.1:
integrity sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==
"chokidar@>=3.0.0 <4.0.0":
version "3.5.2"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz"
integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
@ -4151,7 +4128,7 @@ core-js@^2.4.0:
resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-js@^3.0.0, core-js@^3.6.5:
core-js@^3.6.5:
version "3.10.0"
resolved "https://registry.npmjs.org/core-js/-/core-js-3.10.0.tgz"
integrity sha512-MQx/7TLgmmDVamSyfE+O+5BHvG1aUGj/gHhLn1wVtm2B5u1eVIPvh7vkfjwWKNCjrTJB8+He99IntSQ1qP+vYQ==
@ -5702,22 +5679,6 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"
fetch-mock@9.11.0:
version "9.11.0"
resolved "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz"
integrity sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==
dependencies:
"@babel/core" "^7.0.0"
"@babel/runtime" "^7.0.0"
core-js "^3.0.0"
debug "^4.1.1"
glob-to-regexp "^0.4.0"
is-subset "^0.1.1"
lodash.isequal "^4.5.0"
path-to-regexp "^2.2.1"
querystring "^0.2.0"
whatwg-url "^6.5.0"
figgy-pudding@^3.5.1:
version "3.5.2"
resolved "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz"
@ -6078,11 +6039,6 @@ glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0, glob-parent@~5.1.2:
dependencies:
is-glob "^4.0.1"
glob-to-regexp@^0.4.0:
version "0.4.1"
resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
version "7.1.6"
resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz"
@ -6319,7 +6275,7 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@ -6565,9 +6521,9 @@ immer@8.0.1:
resolved "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz"
integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
immutable@4.0.0, immutable@^4.0.0:
immutable@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
import-cwd@^2.0.0:
@ -7045,11 +7001,6 @@ is-string@^1.0.5:
resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz"
integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
is-subset@^0.1.1:
version "0.1.1"
resolved "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz"
integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=
is-svg@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz"
@ -8026,16 +7977,6 @@ lodash.flow@3.5.0:
resolved "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz"
integrity sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz"
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz"
@ -8046,11 +7987,6 @@ lodash.once@^4.1.1:
resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
lodash.template@^4.5.0:
version "4.5.0"
resolved "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz"
@ -8549,13 +8485,6 @@ no-case@^3.0.4:
lower-case "^2.0.2"
tslib "^2.0.3"
node-fetch@2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
node-forge@^0.10.0:
version "0.10.0"
resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz"
@ -9111,11 +9040,6 @@ path-to-regexp@^1.7.0:
dependencies:
isarray "0.0.1"
path-to-regexp@^2.2.1:
version "2.4.0"
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz"
integrity sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==
path-type@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz"
@ -10327,7 +10251,7 @@ react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-i
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
"react-is@^16.8.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2:
"react-is@^16.8.0 || ^17.0.0", react-is@^17.0.1:
version "17.0.2"
resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
@ -10343,18 +10267,6 @@ react-outside-click-handler@1.3.0:
object.values "^1.1.0"
prop-types "^15.7.2"
react-redux@7.2.6:
version "7.2.6"
resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz"
integrity sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==
dependencies:
"@babel/runtime" "^7.15.4"
"@types/react-redux" "^7.1.20"
hoist-non-react-statics "^3.3.2"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^17.0.2"
react-refresh@^0.8.3:
version "0.8.3"
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz"
@ -10564,7 +10476,7 @@ readdirp@~3.5.0:
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
@ -10584,38 +10496,6 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"
redux-devtools-extension@2.13.9:
version "2.13.9"
resolved "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz"
integrity sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A==
redux-mock-store@1.5.4:
version "1.5.4"
resolved "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz"
integrity sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==
dependencies:
lodash.isplainobject "^4.0.6"
redux-thunk@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.1.tgz#0dd8042cf47868f4b29699941de03c9301a75714"
integrity sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==
redux@4.1.2:
version "4.1.2"
resolved "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz"
integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==
dependencies:
"@babel/runtime" "^7.9.2"
redux@^4.0.0:
version "4.0.5"
resolved "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
dependencies:
loose-envify "^1.4.0"
symbol-observable "^1.2.0"
redux@^4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz"
@ -11411,21 +11291,16 @@ source-list-map@^2.0.0:
resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz"
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
"source-map-js@>=0.6.2 <2.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf"
integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
source-map-js@^0.6.2:
version "0.6.2"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz"
integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
version "0.5.3"
resolved "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz"
@ -11866,7 +11741,7 @@ swr@1.2.1:
resolved "https://registry.yarnpkg.com/swr/-/swr-1.2.1.tgz#c21a4fe2139cb1c4630450589b5b5add947a9d41"
integrity sha512-1cuWXqJqXcFwbgONGCY4PHZ8v05009JdHsC3CIC6u7d00kgbMswNr1sHnnhseOBxtzVqcCNpOHEgVDciRer45w==
symbol-observable@1.2.0, symbol-observable@^1.2.0:
symbol-observable@1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
@ -12123,13 +11998,6 @@ tough-cookie@^4.0.0:
punycode "^2.1.1"
universalify "^0.1.2"
tr46@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz"
integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=
dependencies:
punycode "^2.1.0"
tr46@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz"
@ -12137,11 +12005,6 @@ tr46@^2.0.2:
dependencies:
punycode "^2.1.1"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
tryer@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz"
@ -12588,16 +12451,6 @@ web-vitals@2.1.4:
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.4.tgz#76563175a475a5e835264d373704f9dde718290c"
integrity sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz"
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
webidl-conversions@^4.0.2:
version "4.0.2"
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz"
integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
webidl-conversions@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz"
@ -12780,23 +12633,6 @@ whatwg-mimetype@^2.3.0:
resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz"
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
whatwg-url@^6.5.0:
version "6.5.0"
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz"
integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
dependencies:
lodash.sortby "^4.7.0"
tr46 "^1.0.1"
webidl-conversions "^4.0.2"
whatwg-url@^8.0.0, whatwg-url@^8.5.0:
version "8.5.0"
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.5.0.tgz"