Merge pull request #681 from SkynetLabs/unpin-from-dashboard
unpin skylink from dashboard
This commit is contained in:
commit
f42a3a9c20
|
@ -0,0 +1 @@
|
||||||
|
- added unpinning skylinks from account dashboard
|
|
@ -97,7 +97,7 @@
|
||||||
preserve_host: true
|
preserve_host: true
|
||||||
url: "http://accounts:3000"
|
url: "http://accounts:3000"
|
||||||
match:
|
match:
|
||||||
url: "http://oathkeeper<{,:4455}>/<{login,logout,user,user/uploads,user/downloads,user/stats}>"
|
url: "http://oathkeeper<{,:4455}>/<{login,logout,user,user/uploads,user/uploads/*,user/downloads,user/stats}>"
|
||||||
methods:
|
methods:
|
||||||
- GET
|
- GET
|
||||||
- POST
|
- POST
|
||||||
|
|
|
@ -10,6 +10,6 @@ RUN yarn --frozen-lockfile
|
||||||
COPY public ./public
|
COPY public ./public
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
COPY styles ./styles
|
COPY styles ./styles
|
||||||
COPY next.config.js postcss.config.js tailwind.config.js ./
|
COPY postcss.config.js tailwind.config.js ./
|
||||||
|
|
||||||
CMD ["sh", "-c", "env | grep -E 'NEXT_PUBLIC|KRATOS|STRIPE' > .env.local && yarn build && yarn start"]
|
CMD ["sh", "-c", "env | grep -E 'NEXT_PUBLIC|KRATOS|STRIPE' > .env.local && yarn build && yarn start"]
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
webpack5: true,
|
|
||||||
};
|
|
|
@ -23,11 +23,13 @@
|
||||||
"http-status-codes": "2.1.4",
|
"http-status-codes": "2.1.4",
|
||||||
"ky": "0.25.1",
|
"ky": "0.25.1",
|
||||||
"next": "11.1.2",
|
"next": "11.1.2",
|
||||||
|
"normalize.css": "8.0.1",
|
||||||
"postcss": "8.3.8",
|
"postcss": "8.3.8",
|
||||||
"prettier": "2.4.1",
|
"prettier": "2.4.1",
|
||||||
"pretty-bytes": "5.6.0",
|
"pretty-bytes": "5.6.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
|
"react-toastify": "8.0.2",
|
||||||
"skynet-js": "3.0.2",
|
"skynet-js": "3.0.2",
|
||||||
"stripe": "8.176.0",
|
"stripe": "8.176.0",
|
||||||
"superagent": "6.1.0",
|
"superagent": "6.1.0",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
|
|
||||||
function Button({ children, disabled, className, ...props }) {
|
function Button({ children, disabled, ...props }) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -10,8 +10,7 @@ function Button({ children, disabled, className, ...props }) {
|
||||||
{
|
{
|
||||||
"hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500": !disabled,
|
"hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500": !disabled,
|
||||||
"cursor-auto opacity-50": disabled,
|
"cursor-auto opacity-50": disabled,
|
||||||
},
|
}
|
||||||
className
|
|
||||||
)}
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -21,7 +20,26 @@ function Button({ children, disabled, className, ...props }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Table({ items, count, headers, actions, offset, setOffset, pageSize = 10 }) {
|
function ButtonAction({ children, disabled, ...props }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classnames(
|
||||||
|
"inline-flex items-center px-2.5 py-1.5 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white",
|
||||||
|
{
|
||||||
|
"hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500": !disabled,
|
||||||
|
"cursor-auto opacity-50": disabled,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
disabled={disabled}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Table({ items, count, headers, mutate, actions, offset, setOffset, pageSize = 10 }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (offset < 0) setOffset(0);
|
if (offset < 0) setOffset(0);
|
||||||
else if (offset >= count && count > 0) setOffset(Math.floor(count / pageSize - 1) * pageSize);
|
else if (offset >= count && count > 0) setOffset(Math.floor(count / pageSize - 1) * pageSize);
|
||||||
|
@ -45,8 +63,8 @@ export default function Table({ items, count, headers, actions, offset, setOffse
|
||||||
{name}
|
{name}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
{actions.map(({ key, name }) => (
|
{actions.map(({ name }, index) => (
|
||||||
<th key={key} scope="col" className="relative px-6 py-3">
|
<th key={index} scope="col" className="relative px-6 py-3">
|
||||||
<span className="sr-only">{name}</span>
|
<span className="sr-only">{name}</span>
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
|
@ -56,7 +74,7 @@ export default function Table({ items, count, headers, actions, offset, setOffse
|
||||||
{items && items.length ? (
|
{items && items.length ? (
|
||||||
items.map((row, index) => (
|
items.map((row, index) => (
|
||||||
<tr className={index % 2 ? "bg-gray-100" : "bg-white"} key={index}>
|
<tr className={index % 2 ? "bg-gray-100" : "bg-white"} key={index}>
|
||||||
{headers.map(({ key, formatter, href, nowrap = true }) => (
|
{headers.map(({ key, formatter, href, nowrap }) => (
|
||||||
<td
|
<td
|
||||||
key={key}
|
key={key}
|
||||||
className={`${nowrap ? "whitespace-nowrap" : ""} px-6 py-4 text-sm font-medium text-gray-900`}
|
className={`${nowrap ? "whitespace-nowrap" : ""} px-6 py-4 text-sm font-medium text-gray-900`}
|
||||||
|
@ -77,11 +95,9 @@ export default function Table({ items, count, headers, actions, offset, setOffse
|
||||||
)) || <>—</>}
|
)) || <>—</>}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
{actions.map(({ key, name, action }) => (
|
{actions.map(({ name, action }, index) => (
|
||||||
<td key={key} className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<td key={index} className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
<a href="#" className="text-green-600 hover:text-green-900" onClick={action}>
|
<ButtonAction onClick={() => action(row, mutate)}>{name}</ButtonAction>
|
||||||
{name}
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -107,15 +123,11 @@ export default function Table({ items, count, headers, actions, offset, setOffse
|
||||||
<span className="font-medium">{count}</span> results
|
<span className="font-medium">{count}</span> results
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex justify-between sm:justify-end">
|
<div className="flex-1 flex justify-between sm:justify-end space-x-3">
|
||||||
<Button disabled={offset - pageSize < 0} onClick={() => setOffset(offset - pageSize)}>
|
<Button disabled={offset - pageSize < 0} onClick={() => setOffset(offset - pageSize)}>
|
||||||
Previous
|
Previous
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button disabled={offset + pageSize >= count} onClick={() => setOffset(offset + pageSize)}>
|
||||||
className="ml-3"
|
|
||||||
disabled={offset + pageSize >= count}
|
|
||||||
onClick={() => setOffset(offset + pageSize)}
|
|
||||||
>
|
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { Elements } from "@stripe/react-stripe-js";
|
import { Elements } from "@stripe/react-stripe-js";
|
||||||
import { loadStripe } from "@stripe/stripe-js";
|
import { loadStripe } from "@stripe/stripe-js";
|
||||||
|
import { ToastContainer } from "react-toastify";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
import "normalize.css";
|
||||||
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import "tailwindcss/tailwind.css";
|
import "tailwindcss/tailwind.css";
|
||||||
import "@fontsource/metropolis/all.css";
|
import "@fontsource/metropolis/all.css";
|
||||||
|
|
||||||
|
@ -14,6 +17,7 @@ function MyApp({ Component, pageProps }) {
|
||||||
<title key="title">Skynet</title>
|
<title key="title">Skynet</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
|
<ToastContainer bodyClassName={() => "Toastify__toast-body text-sm font-medium text-palette-500"} />
|
||||||
</Elements>
|
</Elements>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ export default async (req, res) => {
|
||||||
res.setHeader("Set-Cookie", header["set-cookie"]);
|
res.setHeader("Set-Cookie", header["set-cookie"]);
|
||||||
res.redirect(req.query.return_to ?? "/");
|
res.redirect(req.query.return_to ?? "/");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(`Cookie is present but authentication failed: ${error.message}`);
|
||||||
|
|
||||||
// credentials were correct but accounts service failed
|
// credentials were correct but accounts service failed
|
||||||
res.redirect("/.ory/kratos/public/self-service/browser/flows/logout");
|
res.redirect("/.ory/kratos/public/self-service/browser/flows/logout");
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,8 @@ export async function getServerSideProps(context) {
|
||||||
|
|
||||||
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
|
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(`Unexpected error retrieving login flow: ${error.message}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
|
|
|
@ -35,6 +35,8 @@ export async function getServerSideProps(context) {
|
||||||
|
|
||||||
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
|
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(`Unexpected error retrieving registration flow: ${error.message}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
|
|
|
@ -12,8 +12,25 @@ const apiPrefix = process.env.NODE_ENV === "development" ? "/api/stubs" : "";
|
||||||
const getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink);
|
const getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink);
|
||||||
const getRelativeDate = ({ downloadedOn }) => dayjs(downloadedOn).format("YYYY-MM-DD HH:mm:ss");
|
const getRelativeDate = ({ downloadedOn }) => dayjs(downloadedOn).format("YYYY-MM-DD HH:mm:ss");
|
||||||
const headers = [
|
const headers = [
|
||||||
{ key: "name", name: "Name", nowrap: false, href: getSkylinkLink },
|
{
|
||||||
{ key: "skylink", name: "Skylink" },
|
key: "name",
|
||||||
|
name: "File",
|
||||||
|
formatter: ({ name, skylink }) => (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href={getSkylinkLink({ skylink })}
|
||||||
|
className="text-green-600 hover:text-green-900 break-all"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500 text-xs">{skylink}</p>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
{ key: "size", name: "Size", formatter: ({ size }) => prettyBytes(size) },
|
{ key: "size", name: "Size", formatter: ({ size }) => prettyBytes(size) },
|
||||||
{ key: "downloadedOn", name: "Accessed on", formatter: getRelativeDate },
|
{ key: "downloadedOn", name: "Accessed on", formatter: getRelativeDate },
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import ky from "ky/umd";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
import Layout from "../components/Layout";
|
import Layout from "../components/Layout";
|
||||||
import Table from "../components/Table";
|
import Table from "../components/Table";
|
||||||
import authServerSideProps from "../services/authServerSideProps";
|
import authServerSideProps from "../services/authServerSideProps";
|
||||||
|
@ -12,12 +14,42 @@ const apiPrefix = process.env.NODE_ENV === "development" ? "/api/stubs" : "";
|
||||||
const getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink);
|
const getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink);
|
||||||
const getRelativeDate = ({ uploadedOn }) => dayjs(uploadedOn).format("YYYY-MM-DD HH:mm:ss");
|
const getRelativeDate = ({ uploadedOn }) => dayjs(uploadedOn).format("YYYY-MM-DD HH:mm:ss");
|
||||||
const headers = [
|
const headers = [
|
||||||
{ key: "name", name: "Name", nowrap: false, href: getSkylinkLink },
|
{
|
||||||
{ key: "skylink", name: "Skylink" },
|
key: "name",
|
||||||
|
name: "File",
|
||||||
|
formatter: ({ name, skylink }) => (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href={getSkylinkLink({ skylink })}
|
||||||
|
className="text-green-600 hover:text-green-900 break-all"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500 text-xs">{skylink}</p>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
{ key: "size", name: "Size", formatter: ({ size }) => prettyBytes(size) },
|
{ key: "size", name: "Size", formatter: ({ size }) => prettyBytes(size) },
|
||||||
{ key: "uploadedOn", name: "Uploaded on", formatter: getRelativeDate },
|
{ key: "uploadedOn", name: "Uploaded on", formatter: getRelativeDate },
|
||||||
];
|
];
|
||||||
const actions = [];
|
const actions = [
|
||||||
|
{
|
||||||
|
name: "Unpin Skylink",
|
||||||
|
action: async ({ skylink }, mutate) => {
|
||||||
|
await toast.promise(ky.delete(`/user/uploads/${skylink}`), {
|
||||||
|
pending: "Unpinning Skylink",
|
||||||
|
success: "Skylink unpinned",
|
||||||
|
error: (error) => error.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
mutate();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const getServerSideProps = authServerSideProps(async (context, api) => {
|
export const getServerSideProps = authServerSideProps(async (context, api) => {
|
||||||
const initialData = await api.get("user/uploads?pageSize=10&offset=0").json();
|
const initialData = await api.get("user/uploads?pageSize=10&offset=0").json();
|
||||||
|
@ -27,7 +59,7 @@ export const getServerSideProps = authServerSideProps(async (context, api) => {
|
||||||
|
|
||||||
export default function Uploads({ initialData }) {
|
export default function Uploads({ initialData }) {
|
||||||
const [offset, setOffset] = useState(0);
|
const [offset, setOffset] = useState(0);
|
||||||
const { data } = useAccountsApi(`${apiPrefix}/user/uploads?pageSize=10&offset=${offset}`, {
|
const { data, mutate } = useAccountsApi(`${apiPrefix}/user/uploads?pageSize=10&offset=${offset}`, {
|
||||||
initialData: offset === 0 ? initialData : undefined,
|
initialData: offset === 0 ? initialData : undefined,
|
||||||
revalidateOnMount: true,
|
revalidateOnMount: true,
|
||||||
});
|
});
|
||||||
|
@ -38,7 +70,7 @@ export default function Uploads({ initialData }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout title="Your uploads">
|
<Layout title="Your uploads">
|
||||||
<Table {...data} headers={headers} actions={actions} setOffset={setOffset} />
|
<Table {...data} mutate={mutate} headers={headers} actions={actions} setOffset={setOffset} />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,17 @@ const isProduction = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
export default function authServerSideProps(getServerSideProps) {
|
export default function authServerSideProps(getServerSideProps) {
|
||||||
return function authenticate(context) {
|
return function authenticate(context) {
|
||||||
if (isProduction && (!("ory_kratos_session" in context.req.cookies) || !("skynet-jwt" in context.req.cookies))) {
|
const authCookies = ["ory_kratos_session", "skynet-jwt"];
|
||||||
|
|
||||||
|
if (isProduction && !authCookies.every((cookie) => context.req.cookies[cookie])) {
|
||||||
|
// it is higly unusual that some of the cookies would be set but other would not
|
||||||
|
if (authCookies.some((cookie) => context.req.cookies[cookie])) {
|
||||||
|
console.log(
|
||||||
|
"Unexpected auth cookies state!",
|
||||||
|
authCookies.map((cookie) => `[${cookie}] is ${context.req.cookies[cookie] ? "set" : "not set"}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
|
|
|
@ -640,6 +640,11 @@ clipboardy@2.3.0:
|
||||||
execa "^1.0.0"
|
execa "^1.0.0"
|
||||||
is-wsl "^2.1.1"
|
is-wsl "^2.1.1"
|
||||||
|
|
||||||
|
clsx@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
||||||
|
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
|
||||||
|
|
||||||
color-convert@^1.9.0:
|
color-convert@^1.9.0:
|
||||||
version "1.9.3"
|
version "1.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||||
|
@ -2090,6 +2095,11 @@ normalize-range@^0.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
|
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
|
||||||
integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
|
integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
|
||||||
|
|
||||||
|
normalize.css@8.0.1:
|
||||||
|
version "8.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
|
||||||
|
integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==
|
||||||
|
|
||||||
npm-run-path@^2.0.0:
|
npm-run-path@^2.0.0:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
|
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
|
||||||
|
@ -2523,6 +2533,13 @@ react-refresh@0.8.3:
|
||||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||||
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
|
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
|
||||||
|
|
||||||
|
react-toastify@8.0.2:
|
||||||
|
version "8.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-8.0.2.tgz#11f73b3a847fcffeb47b1823e8974e9895f98fae"
|
||||||
|
integrity sha512-0Nud2d0VD4LIevgkB4L8NYoQ5plTpfqgj2CRVxs58SGA/TTO+2Ojz4C1bLUdGUWsw0zuWqd4GJqxNuMIv0cXMw==
|
||||||
|
dependencies:
|
||||||
|
clsx "^1.1.1"
|
||||||
|
|
||||||
react@17.0.2:
|
react@17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||||
|
|
Reference in New Issue