diff --git a/frontend/package.json b/frontend/package.json index 1f14a8a9cb..7832cbe178 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/accessStoreFake.js b/frontend/src/accessStoreFake.js deleted file mode 100644 index fb39464765..0000000000 --- a/frontend/src/accessStoreFake.js +++ /dev/null @@ -1,11 +0,0 @@ -import { Map as $MAp } from 'immutable'; - -export const createFakeStore = permissions => { - return { - getState: () => ({ - user: new $MAp({ - permissions, - }), - }), - }; -}; diff --git a/frontend/src/component/App.tsx b/frontend/src/component/App.tsx index 62ff747de1..80dfd6e2d3 100644 --- a/frontend/src/component/App.tsx +++ b/frontend/src/component/App.tsx @@ -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); }; diff --git a/frontend/src/component/AppContainer.tsx b/frontend/src/component/AppContainer.tsx deleted file mode 100644 index 7a0958fa6e..0000000000 --- a/frontend/src/component/AppContainer.tsx +++ /dev/null @@ -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); diff --git a/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap b/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap new file mode 100644 index 0000000000..55a4015dff --- /dev/null +++ b/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly if no application 1`] = ` +
+

+ Loading... +

+
+
+
+
+
+`; + +exports[`renders correctly with permissions 1`] = ` +
+

+ Loading... +

+
+
+
+
+
+`; + +exports[`renders correctly without permission 1`] = ` +
+

+ Loading... +

+
+
+
+
+
+`; diff --git a/frontend/src/component/application/__tests__/application-edit-component-test.js b/frontend/src/component/application/__tests__/application-edit-component-test.js new file mode 100644 index 0000000000..3cc15544be --- /dev/null +++ b/frontend/src/component/application/__tests__/application-edit-component-test.js @@ -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( + + + + + Promise.resolve({})} + storeApplicationMetaData={jest.fn()} + deleteApplication={jest.fn()} + history={{}} + locationSettings={{ locale: 'en-GB' }} + /> + + + + + ) + .toJSON(); + + expect(tree).toMatchSnapshot(); +}); + +test('renders correctly without permission', () => { + const tree = renderer + .create( + + + + + 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' }} + /> + + + + + ) + .toJSON(); + + expect(tree).toMatchSnapshot(); +}); + +test('renders correctly with permissions', () => { + const tree = renderer + .create( + + + + + 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' }} + /> + + + + + ) + .toJSON(); + + expect(tree).toMatchSnapshot(); +}); diff --git a/frontend/src/component/application/__tests__/application-edit-component-test.jsx b/frontend/src/component/application/__tests__/application-edit-component-test.jsx index 7a48f3e00a..3cc15544be 100644 --- a/frontend/src/component/application/__tests__/application-edit-component-test.jsx +++ b/frontend/src/component/application/__tests__/application-edit-component-test.jsx @@ -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( - + @@ -38,7 +37,7 @@ test('renders correctly without permission', () => { - + Promise.resolve({})} storeApplicationMetaData={jest.fn()} @@ -101,9 +100,7 @@ test('renders correctly with permissions', () => { - + Promise.resolve({})} storeApplicationMetaData={jest.fn()} diff --git a/frontend/src/component/error/error-component.jsx b/frontend/src/component/error/error-component.jsx deleted file mode 100644 index 45e0d62e8b..0000000000 --- a/frontend/src/component/error/error-component.jsx +++ /dev/null @@ -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 ( - - - - - - } - open={showError} - onClose={() => muteError(error)} - autoHideDuration={10000} - message={ -
- - {error} -
- } - /> - ); -}; - -ErrorComponent.propTypes = { - errors: PropTypes.array.isRequired, - muteError: PropTypes.func.isRequired, -}; - -export default ErrorComponent; diff --git a/frontend/src/component/error/error-container.jsx b/frontend/src/component/error/error-container.jsx deleted file mode 100644 index 6c8fca1ab8..0000000000 --- a/frontend/src/component/error/error-container.jsx +++ /dev/null @@ -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); diff --git a/frontend/src/component/feature/CopyFeature/CopyFeature.jsx b/frontend/src/component/feature/CopyFeature/CopyFeature.jsx index 88a119219e..208e4db37a 100644 --- a/frontend/src/component/feature/CopyFeature/CopyFeature.jsx +++ b/frontend/src/component/feature/CopyFeature/CopyFeature.jsx @@ -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 => { ); }; - -CopyFeature.propTypes = { - history: PropTypes.object.isRequired, - validateName: PropTypes.func.isRequired, -}; - -export default CopyFeature; diff --git a/frontend/src/component/feature/CopyFeature/index.jsx b/frontend/src/component/feature/CopyFeature/index.jsx deleted file mode 100644 index d54fc2b9c6..0000000000 --- a/frontend/src/component/feature/CopyFeature/index.jsx +++ /dev/null @@ -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; diff --git a/frontend/src/component/layout/MainLayout/MainLayout.tsx b/frontend/src/component/layout/MainLayout/MainLayout.tsx index 525c9e225a..19a7b0e087 100644 --- a/frontend/src/component/layout/MainLayout/MainLayout.tsx +++ b/frontend/src/component/layout/MainLayout/MainLayout.tsx @@ -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) => { {children}
-
{ if (!permission) { - console.warn(`Missing permission for AccessProvider: ${permission}`) - return false + console.warn(`Missing permission for AccessProvider: ${permission}`); + return false; } if (p.permission === ADMIN) { diff --git a/frontend/src/component/strategies/__tests__/__snapshots__/list-component-test.jsx.snap b/frontend/src/component/strategies/__tests__/__snapshots__/list-component-test.jsx.snap index af59195c0d..c3cceed402 100644 --- a/frontend/src/component/strategies/__tests__/__snapshots__/list-component-test.jsx.snap +++ b/frontend/src/component/strategies/__tests__/__snapshots__/list-component-test.jsx.snap @@ -28,7 +28,51 @@ exports[`renders correctly with one strategy 1`] = `
+ > + + + +
+
+ +
@@ -162,7 +253,51 @@ exports[`renders correctly with one strategy without permissions 1`] = `
+ > + + + +
+
+ +
diff --git a/frontend/src/component/strategies/__tests__/list-component-test.jsx b/frontend/src/component/strategies/__tests__/list-component-test.jsx index 801eb528b3..3258a44810 100644 --- a/frontend/src/component/strategies/__tests__/list-component-test.jsx +++ b/frontend/src/component/strategies/__tests__/list-component-test.jsx @@ -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', () => { - + { - + >; diff --git a/frontend/src/hooks/api/actions/useAdminUsersApi/errorHandlers.ts b/frontend/src/hooks/api/actions/useAdminUsersApi/errorHandlers.ts index f7b157eb14..522bab536e 100644 --- a/frontend/src/hooks/api/actions/useAdminUsersApi/errorHandlers.ts +++ b/frontend/src/hooks/api/actions/useAdminUsersApi/errorHandlers.ts @@ -4,7 +4,7 @@ import { AuthenticationError, ForbiddenError, NotFoundError, -} from '../../../../store/api-helper'; +} from '../../../../utils/api-utils'; export const handleBadRequest = async ( setErrors?: Dispatch>, diff --git a/frontend/src/hooks/api/actions/useApi/useApi.ts b/frontend/src/hooks/api/actions/useApi/useApi.ts index eb2157a278..653315aaec 100644 --- a/frontend/src/hooks/api/actions/useApi/useApi.ts +++ b/frontend/src/hooks/api/actions/useApi/useApi.ts @@ -12,7 +12,7 @@ import { ForbiddenError, headers, NotFoundError, -} from '../../../../store/api-helper'; +} from '../../../../utils/api-utils'; import { formatApiPath } from '../../../../utils/format-path'; interface IUseAPI { diff --git a/frontend/src/hooks/api/actions/useAuthApi/useAuthApi.tsx b/frontend/src/hooks/api/actions/useAuthApi/useAuthApi.tsx index 9e5e2bb907..555b6ddc42 100644 --- a/frontend/src/hooks/api/actions/useAuthApi/useAuthApi.tsx +++ b/frontend/src/hooks/api/actions/useAuthApi/useAuthApi.tsx @@ -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); }; diff --git a/frontend/src/hooks/api/getters/useUiBootstrap/useUiBootstrap.ts b/frontend/src/hooks/api/getters/useUiBootstrap/useUiBootstrap.ts index 9531cbdb9c..2f3e6cd1b9 100644 --- a/frontend/src/hooks/api/getters/useUiBootstrap/useUiBootstrap.ts +++ b/frontend/src/hooks/api/getters/useUiBootstrap/useUiBootstrap.ts @@ -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 = () => { diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 02ae50648c..4731d0294b 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -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( - - - - - - - - - - - - - - - - - - , + + + + + + + + + + + + + + + + , document.getElementById('app') ); diff --git a/frontend/src/page/features/copy.js b/frontend/src/page/features/copy.js deleted file mode 100644 index 7afe17a487..0000000000 --- a/frontend/src/page/features/copy.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import CopyFeatureToggleForm from '../../component/feature/CopyFeature'; -import PropTypes from 'prop-types'; - -const render = ({ history, match: { params } }) => ( - -); - -render.propTypes = { - history: PropTypes.object.isRequired, - match: PropTypes.object.isRequired, -}; - -export default render; diff --git a/frontend/src/store/api-calls/index.js b/frontend/src/store/api-calls/index.js deleted file mode 100644 index d748d4232a..0000000000 --- a/frontend/src/store/api-calls/index.js +++ /dev/null @@ -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; diff --git a/frontend/src/store/api-helper.js b/frontend/src/store/api-helper.js deleted file mode 100644 index e528135295..0000000000 --- a/frontend/src/store/api-helper.js +++ /dev/null @@ -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', -}; diff --git a/frontend/src/store/error/actions.js b/frontend/src/store/error/actions.js deleted file mode 100644 index 3d378e690e..0000000000 --- a/frontend/src/store/error/actions.js +++ /dev/null @@ -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 }); diff --git a/frontend/src/store/error/index.js b/frontend/src/store/error/index.js deleted file mode 100644 index ec0cc63798..0000000000 --- a/frontend/src/store/error/index.js +++ /dev/null @@ -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; diff --git a/frontend/src/store/feature-toggle/actions.js b/frontend/src/store/feature-toggle/actions.js deleted file mode 100644 index 05f4f82637..0000000000 --- a/frontend/src/store/feature-toggle/actions.js +++ /dev/null @@ -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 }); -} diff --git a/frontend/src/store/feature-toggle/api.js b/frontend/src/store/feature-toggle/api.js deleted file mode 100644 index d354d99702..0000000000 --- a/frontend/src/store/feature-toggle/api.js +++ /dev/null @@ -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, -}; diff --git a/frontend/src/store/feature-toggle/index.js b/frontend/src/store/feature-toggle/index.js deleted file mode 100644 index 2ae1dbb69a..0000000000 --- a/frontend/src/store/feature-toggle/index.js +++ /dev/null @@ -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; diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js deleted file mode 100644 index 812a7762cd..0000000000 --- a/frontend/src/store/index.js +++ /dev/null @@ -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; diff --git a/frontend/src/store/ui-bootstrap/actions.js b/frontend/src/store/ui-bootstrap/actions.js deleted file mode 100644 index 317536c4e9..0000000000 --- a/frontend/src/store/ui-bootstrap/actions.js +++ /dev/null @@ -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)); -} diff --git a/frontend/src/store/ui-bootstrap/api.js b/frontend/src/store/ui-bootstrap/api.js deleted file mode 100644 index 98e0207798..0000000000 --- a/frontend/src/store/ui-bootstrap/api.js +++ /dev/null @@ -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, -}; diff --git a/frontend/src/store/util.js b/frontend/src/store/util.js deleted file mode 100644 index 95208f0918..0000000000 --- a/frontend/src/store/util.js +++ /dev/null @@ -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 }); diff --git a/frontend/src/utils/api-utils.ts b/frontend/src/utils/api-utils.ts new file mode 100644 index 0000000000..270d6818cc --- /dev/null +++ b/frontend/src/utils/api-utils.ts @@ -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; + } +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 2d58650bc8..3f9f53024f 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -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"