diff --git a/dashboard/package.json b/dashboard/package.json index 983d89820f14..07663492d925 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -37,6 +37,7 @@ "@paciolan/remote-component": "^2.13.0", "@tanstack/match-sorter-utils": "^8.7.6", "@tanstack/react-table": "^8.9.3", + "@testing-library/jest-dom": "^5.16.5", "@types/react": "^17.0.39", "ajv": "^8.12.0", "axios": "^1.3.5", @@ -81,6 +82,7 @@ "@bufbuild/protoc-gen-es": "^1.2.1", "@craco/craco": "^7.0.0", "@formatjs/cli": "^6.0.4", + "@reduxjs/toolkit": "^1.9.5", "@testing-library/react": "^12.1.5", "@types/enzyme": "^3.10.12", "@types/jest": "^29.5.2", diff --git a/dashboard/src/components/PrivateRoute/PrivateRoute.test.tsx b/dashboard/src/components/PrivateRoute/PrivateRoute.test.tsx deleted file mode 100644 index 7cfcf38e95d6..000000000000 --- a/dashboard/src/components/PrivateRoute/PrivateRoute.test.tsx +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2018-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import { CdsModal } from "@cds/react/modal"; -import { shallow } from "enzyme"; -import { createMemoryHistory } from "history"; -import React from "react"; -import { Redirect, RouteComponentProps } from "react-router-dom"; -import { defaultStore, mountWrapper } from "shared/specs/mountWrapper"; -import PrivateRoute from "./PrivateRoute"; - -const emptyRouteComponentProps: RouteComponentProps<{}> = { - history: createMemoryHistory(), - location: { - hash: "", - pathname: "", - search: "", - state: "", - key: "", - }, - match: { - isExact: false, - params: {}, - path: "", - url: "", - }, -}; - -class MockComponent extends React.Component {} - -it("redirects to the /login route if not authenticated", () => { - const wrapper = shallow( - , - ); - const RenderMethod = (wrapper.instance() as PrivateRoute).renderRouteIfAuthenticated; - const wrapper2 = shallow(); - expect(wrapper2.find(Redirect).exists()).toBe(true); - expect(wrapper2.find(Redirect).props()).toMatchObject({ - to: { pathname: "/login" }, - } as any); -}); - -it("renders the given component when authenticated", () => { - const wrapper = shallow( - , - ); - const RenderMethod = (wrapper.instance() as PrivateRoute).renderRouteIfAuthenticated; - const wrapper2 = shallow(); - expect(wrapper2.find(MockComponent).exists()).toBe(true); -}); - -it("renders modal to reload the page if the session is expired", () => { - const wrapper = mountWrapper( - defaultStore, - , - ); - expect(wrapper.find(CdsModal)).toExist(); -}); - -it("does not render modal to reload the page if the session isn't expired", () => { - const wrapper = mountWrapper( - defaultStore, - , - ); - expect(wrapper.find(CdsModal)).not.toExist(); -}); diff --git a/dashboard/src/components/PrivateRoute/PrivateRoute.tsx b/dashboard/src/components/PrivateRoute/PrivateRoute.tsx deleted file mode 100644 index af1505fa23e2..000000000000 --- a/dashboard/src/components/PrivateRoute/PrivateRoute.tsx +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2018-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import { CdsButton } from "@cds/react/button"; -import { CdsModal, CdsModalActions, CdsModalContent } from "@cds/react/modal"; -import React from "react"; -import { Redirect, Route, RouteComponentProps, RouteProps } from "react-router-dom"; -import "./PrivateRoute.css"; - -type IRouteComponentPropsAndRouteProps = RouteProps & RouteComponentProps; - -interface IPrivateRouteProps extends IRouteComponentPropsAndRouteProps { - authenticated: boolean; - sessionExpired: boolean; -} - -class PrivateRoute extends React.Component { - public render() { - const { authenticated, component: Component, ...rest } = this.props; - return ; - } - - public renderRouteIfAuthenticated = (props: RouteComponentProps) => { - const { sessionExpired, authenticated, component: Component } = this.props; - const refreshPage = () => { - window.location.reload(); - }; - if (authenticated && Component) { - return ; - } else { - return ( - <> - {" "} - {sessionExpired ? ( - - - {" "} -

- Your session has expired or the connection has been lost, please reload the page. -

-
- - - Reload - - -
- ) : ( - - )} - - ); - } - }; -} - -export default PrivateRoute; diff --git a/dashboard/src/components/PrivateRoute/index.tsx b/dashboard/src/components/PrivateRoute/index.tsx deleted file mode 100644 index dec73f165093..000000000000 --- a/dashboard/src/components/PrivateRoute/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright 2018-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import PrivateRoute from "./PrivateRoute"; - -export default PrivateRoute; diff --git a/dashboard/src/components/PrivateRoute/PrivateRoute.scss b/dashboard/src/components/RequireAuthentication/RequireAuthentication.scss similarity index 100% rename from dashboard/src/components/PrivateRoute/PrivateRoute.scss rename to dashboard/src/components/RequireAuthentication/RequireAuthentication.scss diff --git a/dashboard/src/components/RequireAuthentication/RequireAuthentication.test.tsx b/dashboard/src/components/RequireAuthentication/RequireAuthentication.test.tsx new file mode 100644 index 000000000000..b06daa87efb8 --- /dev/null +++ b/dashboard/src/components/RequireAuthentication/RequireAuthentication.test.tsx @@ -0,0 +1,75 @@ +// Copyright 2018-2022 the Kubeapps contributors. +// SPDX-License-Identifier: Apache-2.0 + +import { createMemoryHistory } from "history"; +import { Route, Router } from "react-router-dom"; +import { renderWithProviders } from "shared/specs/mountWrapper"; +import { RequireAuthentication } from "./RequireAuthentication"; +import { screen } from "@testing-library/react"; +import "@testing-library/jest-dom/extend-expect"; + +it("redirects to the /login route if not authenticated", () => { + renderWithProviders(( + + +

Authenticated

+
+ +

Login

+
+
+ ), { + preloadedState: { + auth: { + authenticated: false, + sessionExpired: false, + } + } + }); + + expect(screen.getByRole("heading")).toHaveTextContent("Login"); +}); + +it("renders the given component when authenticated", () => { + renderWithProviders(( + + +

Authenticated

+
+ +

Login

+
+
+ ), { + preloadedState: { + auth: { + authenticated: true, + sessionExpired: false, + } + } + }); + + expect(screen.getByRole("heading")).toHaveTextContent("Authenticated"); +}); + +it("renders modal to reload the page if the session is expired", () => { + renderWithProviders(( + + +

Authenticated

+
+ +

Login

+
+
+ ), { + preloadedState: { + auth: { + authenticated: false, + sessionExpired: true, + } + } + }); + + expect(screen.getByRole("dialog")).toHaveTextContent("Your session has expired"); +}); diff --git a/dashboard/src/components/RequireAuthentication/RequireAuthentication.tsx b/dashboard/src/components/RequireAuthentication/RequireAuthentication.tsx new file mode 100644 index 000000000000..a757c92a93cb --- /dev/null +++ b/dashboard/src/components/RequireAuthentication/RequireAuthentication.tsx @@ -0,0 +1,53 @@ +// Copyright 2018-2022 the Kubeapps contributors. +// SPDX-License-Identifier: Apache-2.0 + +import { CdsButton } from "@cds/react/button"; +import { CdsModal, CdsModalActions, CdsModalContent } from "@cds/react/modal"; +import { ReactElement } from "react"; +import { Redirect, useLocation } from "react-router-dom"; +import "./RequireAuthentication.css"; +import { useSelector } from "react-redux"; +import { IStoreState } from "shared/types"; + +interface IRequireAuthenticationProps { + children: ReactElement; +} + +export function RequireAuthentication({ + children, +}: IRequireAuthenticationProps): ReactElement { + const { authenticated, sessionExpired } = useSelector((state: IStoreState) => state.auth) + const refreshPage = () => { + window.location.reload(); + }; + const location = useLocation(); + if (authenticated && children) { + return children; + } else { + return ( + <> + {" "} + {sessionExpired ? ( + + + {" "} +

+ Your session has expired or the connection has been lost, please reload the page. +

+
+ + + Reload + + +
+ ) : ( + + ) + } + + ); + } +}; + +export default RequireAuthentication; diff --git a/dashboard/src/components/RequireAuthentication/index.tsx b/dashboard/src/components/RequireAuthentication/index.tsx new file mode 100644 index 000000000000..ebd870a79179 --- /dev/null +++ b/dashboard/src/components/RequireAuthentication/index.tsx @@ -0,0 +1,6 @@ +// Copyright 2018-2022 the Kubeapps contributors. +// SPDX-License-Identifier: Apache-2.0 + +import RequireAuthentication from "./RequireAuthentication"; + +export default RequireAuthentication; diff --git a/dashboard/src/containers/PrivateRouteContainer/PrivateRouteContainer.ts b/dashboard/src/containers/PrivateRouteContainer/PrivateRouteContainer.ts deleted file mode 100644 index afbab3aa1e5a..000000000000 --- a/dashboard/src/containers/PrivateRouteContainer/PrivateRouteContainer.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2018-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import { connect } from "react-redux"; -import { withRouter } from "react-router-dom"; -import { IStoreState } from "shared/types"; -import PrivateRoute from "../../components/PrivateRoute"; - -function mapStateToProps({ - auth: { authenticated, oidcAuthenticated, sessionExpired }, -}: IStoreState) { - return { - sessionExpired, - authenticated, - oidcAuthenticated, - }; -} - -export default withRouter(connect(mapStateToProps)(PrivateRoute)); diff --git a/dashboard/src/containers/PrivateRouteContainer/index.ts b/dashboard/src/containers/PrivateRouteContainer/index.ts deleted file mode 100644 index 6c62486d24f9..000000000000 --- a/dashboard/src/containers/PrivateRouteContainer/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright 2018-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import PrivateRouteContainer from "./PrivateRouteContainer"; - -export default PrivateRouteContainer; diff --git a/dashboard/src/containers/RoutesContainer/Routes.tsx b/dashboard/src/containers/RoutesContainer/Routes.tsx index b943e303c6aa..555bc05ff768 100644 --- a/dashboard/src/containers/RoutesContainer/Routes.tsx +++ b/dashboard/src/containers/RoutesContainer/Routes.tsx @@ -15,8 +15,6 @@ import { RouteComponentProps, RouteProps, Switch, - useLocation, - useParams, } from "react-router-dom"; import { app } from "shared/url"; import ApiDocs from "../../components/ApiDocs"; @@ -32,7 +30,7 @@ import OperatorInstanceViewContainer from "../../containers/OperatorInstanceView import OperatorNewContainer from "../../containers/OperatorNewContainer"; import OperatorsListContainer from "../../containers/OperatorsListContainer"; import OperatorViewContainer from "../../containers/OperatorViewContainer"; -import PrivateRouteContainer from "../../containers/PrivateRouteContainer"; +import RequireAuthentication from "components/RequireAuthentication"; type IRouteComponentPropsAndRouteProps = RouteProps & RouteComponentProps; @@ -90,9 +88,14 @@ class Routes extends React.Component { {Object.entries(privateRoutes).map(([route, component]) => { const Component = component; return ( - - - + { + return ( + + + + ) + } + } /> ) } )} @@ -100,9 +103,14 @@ class Routes extends React.Component { Object.entries(operatorsRoutes).map(([route, component]) => { const Component = component; return ( - - - + { + return ( + + + + ) + } + } /> ) } )} diff --git a/dashboard/src/reducers/index.ts b/dashboard/src/reducers/index.ts index 3b4de0fa165e..8a2d822d59ff 100644 --- a/dashboard/src/reducers/index.ts +++ b/dashboard/src/reducers/index.ts @@ -14,17 +14,21 @@ import kubeReducer from "./kube"; import operatorReducer from "./operators"; import reposReducer from "./repos"; +export const reducers = { + apps: installedPackagesReducer, + auth: authReducer, + packages: packageReducer, + config: configReducer, + kube: kubeReducer, + clusters: clusterReducer, + repos: reposReducer, + operators: operatorReducer, +}; + const rootReducer = (history: History) => combineReducers({ router: connectRouter(history), - apps: installedPackagesReducer, - auth: authReducer, - packages: packageReducer, - config: configReducer, - kube: kubeReducer, - clusters: clusterReducer, - repos: reposReducer, - operators: operatorReducer, + ...reducers, }); export default rootReducer; diff --git a/dashboard/src/shared/specs/mountWrapper.tsx b/dashboard/src/shared/specs/mountWrapper.tsx index a9571d178457..018c4ddeefb6 100644 --- a/dashboard/src/shared/specs/mountWrapper.tsx +++ b/dashboard/src/shared/specs/mountWrapper.tsx @@ -5,8 +5,8 @@ import { RouterState } from "connected-react-router"; import { mount } from "enzyme"; import { cloneDeep, merge } from "lodash"; import { IntlProvider } from "react-intl"; -import { Provider } from "react-redux"; -import { BrowserRouter as Router } from "react-router-dom"; +import { DefaultRootState, Provider } from "react-redux"; +import { MemoryRouter, BrowserRouter as Router } from "react-router-dom"; import { initialState as installedPackagesInitialState } from "reducers/installedpackages"; import { initialState as authInitialState } from "reducers/auth"; import { initialState as availablePackagesInitialState } from "reducers/availablepackages"; @@ -19,6 +19,14 @@ import configureMockStore, { MockStore } from "redux-mock-store"; import thunk from "redux-thunk"; import I18n from "shared/I18n"; import { IStoreState } from "shared/types"; +import React, { PropsWithChildren } from 'react' +import { render } from '@testing-library/react' +import type { RenderOptions } from '@testing-library/react' +import { configureStore } from '@reduxjs/toolkit' +import type { PreloadedState } from '@reduxjs/toolkit' +import { reducers } from "reducers"; +import { AppStore } from "store"; + const mockStore = configureMockStore([thunk]); @@ -77,3 +85,36 @@ export const mountWrapper = (store: MockStore, children: React.ReactElement) => , , ); + +// Things have moved on for testing to utilise the React Testing Library (RTL) +// so that the redux documentation now recommends the following setup, which +// we should use for new code and gradually move old code over. +// https://redux.js.org/usage/writing-tests + +// This type interface extends the default options for render from RTL, as well +// as allows the user to specify other things such as initialState, store. +interface ExtendedRenderOptions extends Omit { + preloadedState?: PreloadedState + store?: AppStore, +} + +export function renderWithProviders( + ui: React.ReactElement, + { + preloadedState = {}, + // Automatically create a store instance if no store was passed in + store = configureStore({ reducer: reducers, preloadedState }), + ...renderOptions + }: ExtendedRenderOptions = {} +) { + function Wrapper({ children }: PropsWithChildren<{}>): JSX.Element { + return ( + + {children} + + ); + } + + // Return an object with the store and all of RTL's query functions + return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) } +} diff --git a/dashboard/src/store/index.ts b/dashboard/src/store/index.ts index 36078f6e892e..f7d41a120c3b 100644 --- a/dashboard/src/store/index.ts +++ b/dashboard/src/store/index.ts @@ -20,3 +20,5 @@ export default createStore( ), ), ); + +export type AppStore = ReturnType; diff --git a/dashboard/yarn.lock b/dashboard/yarn.lock index 225d5c7fc741..383f07ce084b 100644 --- a/dashboard/yarn.lock +++ b/dashboard/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== +"@adobe/css-tools@^4.0.1": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.2.0.tgz#e1a84fca468f4b337816fcb7f0964beb620ba855" + integrity sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA== + "@alloc/quick-lru@^5.2.0": version "5.2.0" resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" @@ -2133,6 +2138,16 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@reduxjs/toolkit@^1.9.5": + version "1.9.5" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.5.tgz#d3987849c24189ca483baa7aa59386c8e52077c4" + integrity sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ== + dependencies: + immer "^9.0.21" + redux "^4.2.1" + redux-thunk "^2.4.2" + reselect "^4.1.8" + "@rollup/plugin-babel@^5.2.0": version "5.3.1" resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" @@ -2640,6 +2655,21 @@ lz-string "^1.5.0" pretty-format "^27.0.2" +"@testing-library/jest-dom@^5.16.5": + version "5.16.5" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz#3912846af19a29b2dbf32a6ae9c31ef52580074e" + integrity sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA== + dependencies: + "@adobe/css-tools" "^4.0.1" + "@babel/runtime" "^7.9.2" + "@types/testing-library__jest-dom" "^5.9.1" + aria-query "^5.0.0" + chalk "^3.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.5.6" + lodash "^4.17.15" + redent "^3.0.0" + "@testing-library/react@^12.1.5": version "12.1.5" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.5.tgz#bb248f72f02a5ac9d949dea07279095fa577963b" @@ -2886,6 +2916,14 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/jest@*": + version "29.5.3" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.3.tgz#7a35dc0044ffb8b56325c6802a4781a626b05777" + integrity sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + "@types/jest@^29.5.2": version "29.5.2" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.2.tgz#86b4afc86e3a8f3005b297ed8a72494f89e6395b" @@ -3162,6 +3200,13 @@ dependencies: "@types/react" "*" +"@types/testing-library__jest-dom@^5.9.1": + version "5.14.8" + resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.8.tgz#b32090a01c29040461fb7fa10a82400f216a4b93" + integrity sha512-NRfJE9Cgpmu4fx716q9SYmU4jxxhYRU1BQo239Txt/9N3EC745XZX1Yl7h/SBIDlo1ANVOCRB4YDXjaQdoKCHQ== + dependencies: + "@types/jest" "*" + "@types/trusted-types@^2.0.2": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311" @@ -3697,7 +3742,7 @@ aria-query@5.1.3: dependencies: deep-equal "^2.0.5" -aria-query@^5.1.3: +aria-query@^5.0.0, aria-query@^5.1.3: version "5.3.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== @@ -4550,6 +4595,14 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -5191,7 +5244,7 @@ css-what@^6.0.1, css-what@^6.1.0: resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== -css.escape@1.5.1: +css.escape@1.5.1, css.escape@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== @@ -5638,7 +5691,7 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.5.9: +dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: version "0.5.16" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== @@ -7598,7 +7651,7 @@ ignore@^5.2.0, ignore@^5.2.1: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== -immer@^9.0.7: +immer@^9.0.21, immer@^9.0.7: version "9.0.21" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== @@ -9378,7 +9431,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lodash@^4.15.0, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: +lodash@^4.15.0, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -12407,7 +12460,7 @@ redux-thunk@^2.4.2: resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b" integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== -redux@^4.0.0, redux@^4.0.5, redux@^4.1.2, redux@^4.2.0: +redux@^4.0.0, redux@^4.0.5, redux@^4.1.2, redux@^4.2.0, redux@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==