mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-27 15:27:41 +01:00
Implement redux.
This commit is contained in:
parent
9d8a2406e1
commit
5d4ff5cfa4
@ -1,14 +1,30 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useSelector } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import Link from 'components/Link';
|
||||
import styles from './Header.module.css';
|
||||
|
||||
export default function Header() {
|
||||
const user = useSelector(state => state.user);
|
||||
|
||||
return (
|
||||
<header className="container">
|
||||
<h1>
|
||||
<Link href="/">
|
||||
<a>umami</a>
|
||||
</Link>
|
||||
</h1>
|
||||
<header className={classNames(styles.header, 'container')}>
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<Link href="/" className={styles.title}>
|
||||
umami
|
||||
</Link>
|
||||
</div>
|
||||
{user && (
|
||||
<div className="col">
|
||||
<div className={styles.nav}>
|
||||
<Link href="/">Dashboard</Link>
|
||||
<Link href="/settings">Settings</Link>
|
||||
<Link href="/logout">Logout</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
25
components/Header.module.css
Normal file
25
components/Header.module.css
Normal file
@ -0,0 +1,25 @@
|
||||
.header {
|
||||
display: flex;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.header > div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav > * {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-left: 40px;
|
||||
}
|
@ -3,9 +3,9 @@ import classNames from 'classnames';
|
||||
import NextLink from 'next/link';
|
||||
import styles from './Link.module.css';
|
||||
|
||||
export default function Link({ href, className, children }) {
|
||||
export default function Link({ className, children, ...props }) {
|
||||
return (
|
||||
<NextLink href={href}>
|
||||
<NextLink {...props}>
|
||||
<a className={classNames(styles.link, className)}>{children}</a>
|
||||
</NextLink>
|
||||
);
|
||||
|
@ -20,7 +20,7 @@ export default function Login() {
|
||||
const [message, setMessage] = useState();
|
||||
|
||||
const handleSubmit = async ({ username, password }) => {
|
||||
const response = await post('/api/auth', { username, password });
|
||||
const response = await post('/api/auth/login', { username, password });
|
||||
|
||||
if (response?.token) {
|
||||
await Router.push('/');
|
||||
|
@ -3,10 +3,6 @@
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1000px) {
|
||||
.container {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.container > div:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
@ -3,6 +3,8 @@
|
||||
min-height: 430px;
|
||||
font-size: 14px;
|
||||
padding: 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
@ -76,12 +78,14 @@
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.body:empty:before {
|
||||
content: 'No data available';
|
||||
display: block;
|
||||
color: #b3b3b3;
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
9
components/Settings.js
Normal file
9
components/Settings.js
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Settings() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Settings</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,3 +1,8 @@
|
||||
.container {
|
||||
background: #fff;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
@ -18,23 +18,31 @@ export default function WebsiteList() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
{data &&
|
||||
data.websites.map(({ website_id, label }) => (
|
||||
<div key={website_id} className={styles.website}>
|
||||
<div className={styles.header}>
|
||||
<h2>
|
||||
<Link href={`/website/${website_id}/${label}`} className={styles.title}>
|
||||
<Link
|
||||
href="/website/[...id]"
|
||||
as={`/website/${website_id}/${label}`}
|
||||
className={styles.title}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
</h2>
|
||||
<Link href={`/website/${website_id}/${label}`} className={styles.details}>
|
||||
<Link
|
||||
href="/website/[...id]"
|
||||
as={`/website/${website_id}/${label}`}
|
||||
className={styles.details}
|
||||
>
|
||||
<Icon icon={<Arrow />} /> View details
|
||||
</Link>
|
||||
</div>
|
||||
<WebsiteChart key={website_id} title={label} websiteId={website_id} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,8 @@
|
||||
.container {
|
||||
background: #fff;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.website {
|
||||
padding-bottom: 30px;
|
||||
border-bottom: 1px solid #e1e1e1;
|
||||
|
42
hooks/useUser.js
Normal file
42
hooks/useUser.js
Normal file
@ -0,0 +1,42 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { updateUser } from 'redux/actions/user';
|
||||
|
||||
export async function fetchUser() {
|
||||
const res = await fetch('/api/auth/verify');
|
||||
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export default function useUser() {
|
||||
const dispatch = useDispatch();
|
||||
const storeUser = useSelector(state => state.user);
|
||||
const [loading, setLoading] = useState(!storeUser);
|
||||
const [user, setUser] = useState(storeUser || null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && user) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
fetchUser().then(async user => {
|
||||
if (!user) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
await dispatch(updateUser({ user: user }));
|
||||
|
||||
setUser(user);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { user, loading };
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import { parse } from 'cookie';
|
||||
import { verifySecureToken } from './crypto';
|
||||
import { AUTH_COOKIE_NAME } from './constants';
|
||||
|
||||
export default async req => {
|
||||
const token = parse(req.headers.cookie || '')['umami.auth'];
|
||||
export async function verifyAuthToken(req) {
|
||||
const token = parse(req.headers.cookie || '')[AUTH_COOKIE_NAME];
|
||||
|
||||
return verifySecureToken(token);
|
||||
};
|
||||
}
|
||||
|
1
lib/constants.js
Normal file
1
lib/constants.js
Normal file
@ -0,0 +1 @@
|
||||
export const AUTH_COOKIE_NAME = 'umami.auth';
|
@ -285,6 +285,8 @@ export const refFilter = data =>
|
||||
data.filter(({ x }) => x !== '' && !x.startsWith('/') && !x.startsWith('#'));
|
||||
|
||||
export const deviceFilter = data => {
|
||||
if (data.length === 0) return [];
|
||||
|
||||
const devices = data.reduce(
|
||||
(obj, { x, y }) => {
|
||||
const [width] = x.split('x');
|
||||
|
@ -1,6 +1,6 @@
|
||||
import cors from 'cors';
|
||||
import session from './session';
|
||||
import auth from './auth';
|
||||
import { verifySession } from './session';
|
||||
import { verifyAuthToken } from './auth';
|
||||
|
||||
export function use(middleware) {
|
||||
return (req, res) =>
|
||||
@ -18,7 +18,7 @@ export const useCors = use(cors());
|
||||
|
||||
export const useSession = use(async (req, res, next) => {
|
||||
try {
|
||||
req.session = await session(req);
|
||||
req.session = await verifySession(req);
|
||||
} catch {
|
||||
return res.status(400).end();
|
||||
}
|
||||
@ -27,7 +27,7 @@ export const useSession = use(async (req, res, next) => {
|
||||
|
||||
export const useAuth = use(async (req, res, next) => {
|
||||
try {
|
||||
req.auth = await auth(req);
|
||||
req.auth = await verifyAuthToken(req);
|
||||
} catch {
|
||||
return res.status(401).end();
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { getWebsite, getSession, createSession } from 'lib/db';
|
||||
import { getCountry, getDevice, getIpAddress } from 'lib/request';
|
||||
import { uuid, isValidId, verifyToken } from 'lib/crypto';
|
||||
|
||||
export default async req => {
|
||||
export async function verifySession(req) {
|
||||
const { payload } = req.body;
|
||||
const { website: website_uuid, hostname, screen, language, session } = payload;
|
||||
|
||||
@ -51,4 +51,4 @@ export default async req => {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -39,6 +39,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "2.3.0",
|
||||
"@reduxjs/toolkit": "^1.4.0",
|
||||
"bcrypt": "^5.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"chart.js": "^2.9.3",
|
||||
@ -60,9 +61,12 @@
|
||||
"promise-polyfill": "^8.1.3",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-redux": "^7.2.1",
|
||||
"react-simple-maps": "^2.1.2",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-tooltip": "^4.2.7",
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"request-ip": "^2.1.3",
|
||||
"tinycolor2": "^1.4.1",
|
||||
"unfetch": "^4.1.0",
|
||||
|
@ -1,7 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { useStore } from 'redux/store';
|
||||
import 'styles/bootstrap-grid.css';
|
||||
import 'styles/index.css';
|
||||
|
||||
export default function App({ Component, pageProps }) {
|
||||
return <Component {...pageProps} />;
|
||||
const store = useStore();
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Component {...pageProps} />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { serialize } from 'cookie';
|
||||
import { checkPassword, createSecureToken } from 'lib/crypto';
|
||||
import { getAccount } from 'lib/db';
|
||||
import { AUTH_COOKIE_NAME } from 'lib/constants';
|
||||
|
||||
export default async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
@ -10,7 +11,7 @@ export default async (req, res) => {
|
||||
if (account && (await checkPassword(password, account.password))) {
|
||||
const { user_id, username, is_admin } = account;
|
||||
const token = await createSecureToken({ user_id, username, is_admin });
|
||||
const cookie = serialize('umami.auth', token, {
|
||||
const cookie = serialize(AUTH_COOKIE_NAME, token, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 60 * 24 * 365,
|
16
pages/api/auth/logout.js
Normal file
16
pages/api/auth/logout.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { serialize } from 'cookie';
|
||||
import { AUTH_COOKIE_NAME } from 'lib/constants';
|
||||
|
||||
export default async (req, res) => {
|
||||
const cookie = serialize(AUTH_COOKIE_NAME, '', {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
maxAge: 0,
|
||||
});
|
||||
|
||||
res.statusCode = 303;
|
||||
res.setHeader('Set-Cookie', [cookie]);
|
||||
res.setHeader('Location', '/login');
|
||||
|
||||
return res.end();
|
||||
};
|
11
pages/api/auth/verify.js
Normal file
11
pages/api/auth/verify.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { useAuth } from 'lib/middleware';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
if (req.auth) {
|
||||
return res.status(200).json(req.auth);
|
||||
}
|
||||
|
||||
return res.status(401).end();
|
||||
};
|
@ -1,33 +1,18 @@
|
||||
import React from 'react';
|
||||
import { parse } from 'cookie';
|
||||
import Layout from 'components/Layout';
|
||||
import { verifySecureToken } from 'lib/crypto';
|
||||
import WebsiteList from '../components/WebsiteList';
|
||||
import WebsiteList from 'components/WebsiteList';
|
||||
import useUser from 'hooks/useUser';
|
||||
|
||||
export default function HomePage() {
|
||||
const { loading } = useUser();
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function HomePage({ username }) {
|
||||
return (
|
||||
<Layout>
|
||||
<WebsiteList />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps({ req, res }) {
|
||||
const token = parse(req.headers.cookie || '')['umami.auth'];
|
||||
|
||||
try {
|
||||
const payload = await verifySecureToken(token);
|
||||
|
||||
return {
|
||||
props: {
|
||||
...payload,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
res.statusCode = 303;
|
||||
res.setHeader('Location', '/login');
|
||||
res.end();
|
||||
}
|
||||
|
||||
return { props: {} };
|
||||
}
|
||||
|
@ -1,27 +1,9 @@
|
||||
import React from 'react';
|
||||
import { serialize } from 'cookie';
|
||||
import Layout from 'components/Layout';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function LogoutPage() {
|
||||
return (
|
||||
<Layout title="Logout">
|
||||
<h2>You've successfully logged out..</h2>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps({ res }) {
|
||||
const cookie = serialize('umami.auth', '', {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
maxAge: 0,
|
||||
});
|
||||
|
||||
res.statusCode = 303;
|
||||
res.setHeader('Set-Cookie', [cookie]);
|
||||
res.setHeader('Location', '/login');
|
||||
|
||||
res.end();
|
||||
|
||||
return { props: {} };
|
||||
useEffect(() => {
|
||||
fetch('/api/auth/logout').then(() => (window.location.href = '/login'));
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
18
pages/settings.js
Normal file
18
pages/settings.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import Layout from 'components/Layout';
|
||||
import Settings from 'components/Settings';
|
||||
import useUser from 'hooks/useUser';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { loading } = useUser();
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Settings />
|
||||
</Layout>
|
||||
);
|
||||
}
|
@ -1,13 +1,15 @@
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Layout from 'components/Layout';
|
||||
import WebsiteDetails from '../../components/WebsiteDetails';
|
||||
import WebsiteDetails from 'components/WebsiteDetails';
|
||||
import useUser from 'hooks/useUser';
|
||||
|
||||
export default function DetailsPage() {
|
||||
const { loading } = useUser();
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
if (!id) {
|
||||
if (!id || loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
16
redux/actions/user.js
Normal file
16
redux/actions/user.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
const user = createSlice({
|
||||
name: 'user',
|
||||
initialState: null,
|
||||
reducers: {
|
||||
updateUser(state, action) {
|
||||
state = action.payload;
|
||||
return state;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { updateUser } = user.actions;
|
||||
|
||||
export default user.reducer;
|
4
redux/reducers.js
Normal file
4
redux/reducers.js
Normal file
@ -0,0 +1,4 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import user from './actions/user';
|
||||
|
||||
export default combineReducers({ user });
|
40
redux/store.js
Normal file
40
redux/store.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { useMemo } from 'react';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import thunk from 'redux-thunk';
|
||||
import rootReducer from './reducers';
|
||||
|
||||
let store;
|
||||
|
||||
export function getStore(preloadedState) {
|
||||
return configureStore({
|
||||
reducer: rootReducer,
|
||||
middleware: [thunk],
|
||||
preloadedState,
|
||||
});
|
||||
}
|
||||
|
||||
export const initializeStore = preloadedState => {
|
||||
let _store = store ?? getStore(preloadedState);
|
||||
|
||||
// After navigating to a page with an initial Redux state, merge that state
|
||||
// with the current state in the store, and create a new store
|
||||
if (preloadedState && store) {
|
||||
_store = getStore({
|
||||
...store.getState(),
|
||||
...preloadedState,
|
||||
});
|
||||
// Reset the current store
|
||||
store = undefined;
|
||||
}
|
||||
|
||||
// For SSG and SSR always create a new store
|
||||
if (typeof window === 'undefined') return _store;
|
||||
// Create the store once in the client
|
||||
if (!store) store = _store;
|
||||
|
||||
return _store;
|
||||
};
|
||||
|
||||
export function useStore(initialState) {
|
||||
return useMemo(() => initializeStore(initialState), [initialState]);
|
||||
}
|
@ -60,11 +60,10 @@ select {
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 20px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
|
58
yarn.lock
58
yarn.lock
@ -1102,6 +1102,13 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.5.5":
|
||||
version "7.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.0.tgz#f10245877042a815e07f7e693faff0ae9d3a2aac"
|
||||
integrity sha512-qArkXsjJq7H+T86WrIFV0Fnu/tNOkZ4cgXmjkzAu3b/58D5mFIO8JH/y77t7C9q0OdDRdh9s7Ue5GasYssxtXw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.10.4", "@babel/template@^7.7.4":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
|
||||
@ -1217,6 +1224,16 @@
|
||||
dependencies:
|
||||
pkg-up "^3.1.0"
|
||||
|
||||
"@reduxjs/toolkit@^1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.4.0.tgz#ee2e2384cc3d1d76780d844b9c2da3580d32710d"
|
||||
integrity sha512-hkxQwVx4BNVRsYdxjNF6cAseRmtrkpSlcgJRr3kLUcHPIAMZAmMJkXmHh/eUEGTMqPzsYpJLM7NN2w9fxQDuGw==
|
||||
dependencies:
|
||||
immer "^7.0.3"
|
||||
redux "^4.0.0"
|
||||
redux-thunk "^2.3.0"
|
||||
reselect "^4.0.0"
|
||||
|
||||
"@rollup/plugin-buble@^0.21.3":
|
||||
version "0.21.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-buble/-/plugin-buble-0.21.3.tgz#1649a915b1d051a4f430d40e7734a7f67a69b33e"
|
||||
@ -4529,6 +4546,11 @@ image-size@~0.5.0:
|
||||
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
|
||||
integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=
|
||||
|
||||
immer@^7.0.3:
|
||||
version "7.0.7"
|
||||
resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.7.tgz#9dfe713d49bf871cc59aedfce59b1992fa37a977"
|
||||
integrity sha512-Q8yYwVADJXrNfp1ZUAh4XDHkcoE3wpdpb4mC5abDSajs2EbW8+cGdPyAnglMyLnm7EF6ojD2xBFX7L5i4TIytw==
|
||||
|
||||
import-fresh@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
|
||||
@ -7328,11 +7350,22 @@ react-fast-compare@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||
|
||||
react-is@16.13.1, react-is@^16.7.0, react-is@^16.8.1:
|
||||
react-is@16.13.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-redux@^7.2.1:
|
||||
version "7.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.1.tgz#8dedf784901014db2feca1ab633864dee68ad985"
|
||||
integrity sha512-T+VfD/bvgGTUA74iW9d2i5THrDQWbweXP0AVNI8tNd1Rk5ch1rnMiJkDD67ejw7YBKM4+REvcvqRuWJb7BLuEg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^16.9.0"
|
||||
|
||||
react-refresh@0.8.3:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||
@ -7445,6 +7478,19 @@ redent@^3.0.0:
|
||||
indent-string "^4.0.0"
|
||||
strip-indent "^3.0.0"
|
||||
|
||||
redux-thunk@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
||||
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
|
||||
|
||||
redux@^4.0.0, redux@^4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
|
||||
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
|
||||
dependencies:
|
||||
loose-envify "^1.4.0"
|
||||
symbol-observable "^1.2.0"
|
||||
|
||||
reflect.ownkeys@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
|
||||
@ -7634,6 +7680,11 @@ require-relative@^0.8.7:
|
||||
resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de"
|
||||
integrity sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=
|
||||
|
||||
reselect@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
|
||||
integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
|
||||
|
||||
resolve-from@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
|
||||
@ -8585,6 +8636,11 @@ svgo@^1.0.0, svgo@^1.2.2:
|
||||
unquote "~1.1.1"
|
||||
util.promisify "~1.0.0"
|
||||
|
||||
symbol-observable@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
|
||||
|
||||
table@^5.2.3, table@^5.4.6:
|
||||
version "5.4.6"
|
||||
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
|
||||
|
Loading…
Reference in New Issue
Block a user