diff --git a/packages/dashboard-v2/package.json b/packages/dashboard-v2/package.json index c94cfa3f..7b620128 100644 --- a/packages/dashboard-v2/package.json +++ b/packages/dashboard-v2/package.json @@ -21,15 +21,21 @@ "dependencies": { "@fontsource/sora": "^4.5.3", "@fontsource/source-sans-pro": "^4.5.3", + "classnames": "^2.3.1", + "copy-text-to-clipboard": "^3.0.1", "gatsby": "^4.6.2", "gatsby-plugin-postcss": "^5.7.0", "http-status-codes": "^2.2.0", + "nanoid": "^3.3.1", + "path-browserify": "^1.0.1", "postcss": "^8.4.6", "pretty-bytes": "^6.0.0", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-dropzone": "^12.0.4", "react-helmet": "^6.1.0", "react-use": "^17.3.2", + "skynet-js": "^3.0.2", "swr": "^1.2.2", "tailwindcss": "^3.0.23" }, diff --git a/packages/dashboard-v2/src/components/Button/Button.js b/packages/dashboard-v2/src/components/Button/Button.js index 230a5a93..165935a2 100644 --- a/packages/dashboard-v2/src/components/Button/Button.js +++ b/packages/dashboard-v2/src/components/Button/Button.js @@ -1,41 +1,21 @@ import PropTypes from "prop-types"; +import styled from "styled-components"; /** * Primary UI component for user interaction */ -export const Button = ({ primary, label, ...props }) => { - return ( - - ); -}; - +export const Button = styled.button.attrs(({ $primary }) => ({ + type: "button", + className: `px-6 py-3 rounded-full font-sans uppercase text-xs tracking-wide text-palette-600 + ${$primary ? "bg-primary" : "bg-white border-2 border-black"}`, +}))``; Button.propTypes = { /** * Is this the principal call to action on the page? */ - primary: PropTypes.bool, - /** - * What background color to use - */ - backgroundColor: PropTypes.string, - /** - * Button contents - */ - label: PropTypes.string.isRequired, - /** - * Optional click handler - */ - onClick: PropTypes.func, + $primary: PropTypes.bool, }; Button.defaultProps = { - primary: false, - onClick: undefined, + $primary: false, }; diff --git a/packages/dashboard-v2/src/components/Icons/icons/CircledArrowUpIcon.js b/packages/dashboard-v2/src/components/Icons/icons/CircledArrowUpIcon.js new file mode 100644 index 00000000..f27742d4 --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/icons/CircledArrowUpIcon.js @@ -0,0 +1,18 @@ +import { withIconProps } from "../withIconProps"; + +export const CircledArrowUpIcon = withIconProps(({ size, ...props }) => ( + + + + +)); diff --git a/packages/dashboard-v2/src/components/Icons/icons/CircledCheckmarkIcon.js b/packages/dashboard-v2/src/components/Icons/icons/CircledCheckmarkIcon.js new file mode 100644 index 00000000..5f0cfc31 --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/icons/CircledCheckmarkIcon.js @@ -0,0 +1,18 @@ +import { withIconProps } from "../withIconProps"; + +export const CircledCheckmarkIcon = withIconProps(({ size, ...props }) => ( + + + + +)); diff --git a/packages/dashboard-v2/src/components/Icons/icons/CircledErrorIcon.js b/packages/dashboard-v2/src/components/Icons/icons/CircledErrorIcon.js new file mode 100644 index 00000000..388ae1b4 --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/icons/CircledErrorIcon.js @@ -0,0 +1,19 @@ +import { withIconProps } from "../withIconProps"; + +export const CircledErrorIcon = withIconProps(({ size, ...props }) => ( + + + + +)); diff --git a/packages/dashboard-v2/src/components/Icons/icons/CircledProgressIcon.js b/packages/dashboard-v2/src/components/Icons/icons/CircledProgressIcon.js new file mode 100644 index 00000000..95c6fbcd --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/icons/CircledProgressIcon.js @@ -0,0 +1,22 @@ +import { withIconProps } from "../withIconProps"; + +export const CircledProgressIcon = withIconProps(({ size, ...props }) => ( + + + + + + + + + + + +)); diff --git a/packages/dashboard-v2/src/components/Icons/icons/FolderUploadIcon.js b/packages/dashboard-v2/src/components/Icons/icons/FolderUploadIcon.js new file mode 100644 index 00000000..b54e3e4f --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/icons/FolderUploadIcon.js @@ -0,0 +1,24 @@ +import { withIconProps } from "../withIconProps"; + +export const FolderUploadIcon = withIconProps((props) => ( + + + + + + + + + +)); diff --git a/packages/dashboard-v2/src/components/Icons/icons/PlusIcon.js b/packages/dashboard-v2/src/components/Icons/icons/PlusIcon.js new file mode 100644 index 00000000..48dc64f0 --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/icons/PlusIcon.js @@ -0,0 +1,14 @@ +import { withIconProps } from "../withIconProps"; + +export const PlusIcon = withIconProps(({ size, ...props }) => ( + + + +)); diff --git a/packages/dashboard-v2/src/components/Icons/icons/UploadIcon.js b/packages/dashboard-v2/src/components/Icons/icons/UploadIcon.js new file mode 100644 index 00000000..8f7d1ee6 --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/icons/UploadIcon.js @@ -0,0 +1,40 @@ +import { withIconProps } from "../withIconProps"; + +export const UploadIcon = withIconProps((props) => ( + + + + + + + + + + +)); diff --git a/packages/dashboard-v2/src/components/Icons/index.js b/packages/dashboard-v2/src/components/Icons/index.js index 28f87b15..41552e34 100644 --- a/packages/dashboard-v2/src/components/Icons/index.js +++ b/packages/dashboard-v2/src/components/Icons/index.js @@ -4,3 +4,8 @@ export * from "./icons/LockClosedIcon"; export * from "./icons/SkynetLogoIcon"; export * from "./icons/ArrowRightIcon"; export * from "./icons/InfoIcon"; +export * from "./icons/CircledCheckmarkIcon"; +export * from "./icons/CircledErrorIcon"; +export * from "./icons/CircledProgressIcon"; +export * from "./icons/CircledArrowUpIcon"; +export * from "./icons/PlusIcon"; diff --git a/packages/dashboard-v2/src/components/Slider/Bullets.js b/packages/dashboard-v2/src/components/Slider/Bullets.js index b43c66b5..d7dff453 100644 --- a/packages/dashboard-v2/src/components/Slider/Bullets.js +++ b/packages/dashboard-v2/src/components/Slider/Bullets.js @@ -14,7 +14,7 @@ export default function Bullets({ visibleSlides, activeIndex, allSlides, changeS key={index} type="button" className={`rounded-full w-3 h-3 ${activeIndex === index ? "bg-primary" : "border-2 cursor-pointer"}`} - onClick={() => changeSlide(index)} + onClick={(event) => changeSlide(event, index)} /> ))} diff --git a/packages/dashboard-v2/src/components/Slider/Slider.js b/packages/dashboard-v2/src/components/Slider/Slider.js index 1a04485d..ae311242 100644 --- a/packages/dashboard-v2/src/components/Slider/Slider.js +++ b/packages/dashboard-v2/src/components/Slider/Slider.js @@ -1,7 +1,6 @@ import * as React from "react"; import PropTypes from "prop-types"; import styled, { css } from "styled-components"; -import theme from "../../lib/theme"; import useActiveBreakpoint from "./useActiveBreakpoint"; import Bullets from "./Bullets"; @@ -32,7 +31,9 @@ const Slider = ({ slides, breakpoints }) => { const { visibleSlides, scrollable } = useActiveBreakpoint(breakpoints); const [activeIndex, setActiveIndex] = React.useState(0); const changeSlide = React.useCallback( - (index) => { + (event, index) => { + event.preventDefault(); + event.stopPropagation(); setActiveIndex(Math.min(index, slides.length - visibleSlides)); // Don't let it scroll too far }, [slides, visibleSlides, setActiveIndex] @@ -62,7 +63,7 @@ const Slider = ({ slides, breakpoints }) => {
changeSlide(index) : null} + onClickCapture={scrollable && !isVisible ? (event) => changeSlide(event, index) : null} > {slide} diff --git a/packages/dashboard-v2/src/components/Tabs/TabPanel.js b/packages/dashboard-v2/src/components/Tabs/TabPanel.js index a22becc6..f9907074 100644 --- a/packages/dashboard-v2/src/components/Tabs/TabPanel.js +++ b/packages/dashboard-v2/src/components/Tabs/TabPanel.js @@ -1,15 +1,33 @@ import PropTypes from "prop-types"; +import { useEffect, useState } from "react"; /** * Besides documented props, it accepts all HMTL attributes a `
` element does. */ export const TabPanel = ({ children, active, tabId, ...props }) => { - if (!active) { + const [wasActivated, setWasActivated] = useState(false); + + useEffect(() => { + if (active) { + setWasActivated(true); + } + }, [active]); + + // If the TabPanel was never activated, let's not render its children at all. + // We'll only render them after the first activation and then won't unmount them + // unless the entire TabPanel is unmounted, too. + if (!active && !wasActivated) { return null; } return ( -
+
{children}
); diff --git a/packages/dashboard-v2/src/components/Tabs/Tabs.js b/packages/dashboard-v2/src/components/Tabs/Tabs.js index 14356466..4bf20ccf 100644 --- a/packages/dashboard-v2/src/components/Tabs/Tabs.js +++ b/packages/dashboard-v2/src/components/Tabs/Tabs.js @@ -6,11 +6,11 @@ import { ActiveTabIndicator } from "./ActiveTabIndicator"; import { usePrefixedTabIds, useTabsChildren } from "./hooks"; const Container = styled.div.attrs({ - className: "tabs-container", + className: "tabs-container flex flex-col h-full", })``; const Header = styled.div.attrs({ - className: "relative flex justify-start overflow-hidden", + className: "relative flex justify-start overflow-hidden grow-0 shrink-0", })``; const TabList = styled.div.attrs(({ variant }) => ({ @@ -26,7 +26,7 @@ const Divider = styled.div.attrs({ right: calc(-100vw - 2px); `; -const Body = styled.div``; +const Body = styled.div.attrs({ className: "grow min-h-0" })``; /** * Besides documented props, it accepts all HMTL attributes a `
` element does. diff --git a/packages/dashboard-v2/src/components/Uploader/ProgressBar.js b/packages/dashboard-v2/src/components/Uploader/ProgressBar.js new file mode 100644 index 00000000..362c60a4 --- /dev/null +++ b/packages/dashboard-v2/src/components/Uploader/ProgressBar.js @@ -0,0 +1,71 @@ +import cn from "classnames"; +import PropTypes from "prop-types"; +import styled, { css, keyframes } from "styled-components"; + +const moveAnimation = keyframes` + 0% { + background-position: 0 0; + } + 100% { + background-position: 15px 0; + } +`; +const Container = styled.div.attrs(({ $status }) => ({ + className: cn("flex relative rounded-sm h-1", { "bg-palette-200": $status === "uploading" }), +}))``; + +const Indicator = styled.div.attrs(({ $status }) => ({ + className: cn( + ` + rounded-sm bg-[length:15px_10px] + `, + { + "bg-primary": $status === "uploading" || $status === "complete", + "text-primary": $status !== "error", + "text-error": $status === "error", + "bg-dashed": $status === "error" || $status === "enqueued" || $status === "processing", + } + ), +}))` + width: ${({ $status, $percentage }) => ($status === "uploading" ? $percentage : 100)}%; + &.bg-dashed { + opacity: 0.4; + background-image: linear-gradient( + -60deg, + transparent, + transparent 30%, + currentColor 30%, + currentColor 70%, + transparent 70%, + transparent + ); + animation: ${css` + ${moveAnimation} 1s linear infinite + `}; + } +`; + +/** + * Primary UI component for indicating progress of a given task + */ +export const ProgressBar = ({ status, percentage, ...props }) => ( + + + +); + +ProgressBar.propTypes = { + /** + * Status of the task + */ + status: PropTypes.oneOf(["complete", "enqueued", "error", "uploading", "processing"]), + /** + * Progress of the task (in case status is "uploading") + */ + percentage: PropTypes.number, +}; + +ProgressBar.defaultProps = { + status: "enqueued", + percentage: 0, +}; diff --git a/packages/dashboard-v2/src/components/Uploader/ProgressBar.stories.js b/packages/dashboard-v2/src/components/Uploader/ProgressBar.stories.js new file mode 100644 index 00000000..da4b6c19 --- /dev/null +++ b/packages/dashboard-v2/src/components/Uploader/ProgressBar.stories.js @@ -0,0 +1,37 @@ +import React from "react"; +import { ProgressBar } from "./ProgressBar"; + +// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +export default { + title: "SkynetLibrary/ProgressBar", + component: ProgressBar, + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes +}; + +// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +const Template = (args) => ; + +export const Uploading = Template.bind({}); +// More on args: https://storybook.js.org/docs/react/writing-stories/args +Uploading.args = { + status: "uploading", + percentage: 65, +}; + +export const Complete = Template.bind({}); +// More on args: https://storybook.js.org/docs/react/writing-stories/args +Complete.args = { + status: "complete", +}; + +export const Enqueued = Template.bind({}); +// More on args: https://storybook.js.org/docs/react/writing-stories/args +Enqueued.args = { + status: "enqueued", +}; + +export const Error = Template.bind({}); +// More on args: https://storybook.js.org/docs/react/writing-stories/args +Error.args = { + status: "error", +}; diff --git a/packages/dashboard-v2/src/components/Uploader/Uploader.js b/packages/dashboard-v2/src/components/Uploader/Uploader.js new file mode 100644 index 00000000..269d9fbe --- /dev/null +++ b/packages/dashboard-v2/src/components/Uploader/Uploader.js @@ -0,0 +1,135 @@ +import { useCallback, useEffect, useState } from "react"; +import PropTypes from "prop-types"; +import cn from "classnames"; +import path from "path-browserify"; +import { useDropzone } from "react-dropzone"; +import { nanoid } from "nanoid"; + +import { Button } from "../Button"; +import { UploadIcon } from "../Icons/icons/UploadIcon"; +import { FolderUploadIcon } from "../Icons/icons/FolderUploadIcon"; + +import UploaderItem from "./UploaderItem"; +import { PlusIcon } from "../Icons"; + +const MAX_PARALLEL_UPLOADS = 1; + +const getFilePath = (file) => file.webkitRelativePath || file.path || file.name; + +const getRootDirectory = (file) => { + const filePath = getFilePath(file); + const { root, dir } = path.parse(filePath); + + return path.normalize(dir).slice(root.length).split(path.sep)[0]; +}; + +const Uploader = ({ mode }) => { + const [uploads, setUploads] = useState([]); + + const onUploadStateChange = useCallback((id, state) => { + setUploads((uploads) => { + const index = uploads.findIndex((upload) => upload.id === id); + + return [...uploads.slice(0, index), { ...uploads[index], ...state }, ...uploads.slice(index + 1)]; + }); + }, []); + + const handleDrop = async (files) => { + if (mode === "directory" && files.length) { + const name = getRootDirectory(files[0]); // get the file path from the first file + const size = files.reduce((acc, file) => acc + file.size, 0); + + files = [{ name, size, files }]; + } + + setUploads((uploads) => [ + ...files.map((file) => ({ id: nanoid(), file, progress: 0, mode, status: "enqueued" })), + ...uploads, + ]); + }; + + useEffect(() => { + const enqueued = uploads.filter(({ status }) => status === "enqueued"); + const uploading = uploads.filter(({ status }) => ["uploading", "processing", "retrying"].includes(status)); + const queue = enqueued.slice(0, MAX_PARALLEL_UPLOADS - uploading.length).map(({ id }) => id); + + if (queue.length && uploading.length < MAX_PARALLEL_UPLOADS) { + setUploads((uploads) => { + return uploads.map((upload) => { + if (queue.includes(upload.id)) return { ...upload, status: "uploading" }; + return upload; + }); + }); + } + }, [uploads]); + + const { getRootProps, getInputProps, isDragActive, inputRef } = useDropzone({ + onDrop: handleDrop, + useFsAccessApi: false, + }); + const inputElement = inputRef.current; + + useEffect(() => { + if (!inputElement) return; + if (mode === "directory") inputElement.setAttribute("webkitdirectory", "true"); + if (mode === "file") inputElement.removeAttribute("webkitdirectory"); + }, [inputElement, mode]); + + return ( +
+
+ + {uploads.length === 0 ? ( +
+ {mode === "file" ? ( + <> + +

Add, or drop your files here to pin to Skynet

+ + ) : ( + <> + +

Drop any folder with an index.html file to deploy to Skynet

+ + )} + +
+ ) : ( +
+ + Add, or drop your files here +
+ )} +
+ + {uploads.length > 0 && ( +
+ {uploads.map((upload) => ( + + ))} +
+ )} +
+ ); +}; + +Uploader.propTypes = { + mode: PropTypes.oneOf(["file", "directory"]), +}; + +Uploader.defaultProps = { + mode: "file", +}; + +export default Uploader; diff --git a/packages/dashboard-v2/src/components/Uploader/UploaderItem.js b/packages/dashboard-v2/src/components/Uploader/UploaderItem.js new file mode 100644 index 00000000..4f47809c --- /dev/null +++ b/packages/dashboard-v2/src/components/Uploader/UploaderItem.js @@ -0,0 +1,133 @@ +import * as React from "react"; +import cn from "classnames"; +import bytes from "pretty-bytes"; +import { StatusCodes } from "http-status-codes"; +import copy from "copy-text-to-clipboard"; +import path from "path-browserify"; +import { useTimeoutFn } from "react-use"; +import { SkynetClient } from "skynet-js"; +import { ProgressBar } from "./ProgressBar"; +import UploaderItemIcon from "./UploaderItemIcon"; +import buildUploadErrorMessage from "./buildUploadErrorMessage"; + +const skynetClient = new SkynetClient("https://siasky.net"); //TODO: proper API url + +const getFilePath = (file) => file.webkitRelativePath || file.path || file.name; + +const getRelativeFilePath = (file) => { + const filePath = getFilePath(file); + const { root, dir, base } = path.parse(filePath); + const relative = path.normalize(dir).slice(root.length).split(path.sep).slice(1); + + return path.join(...relative, base); +}; + +export default function UploaderItem({ onUploadStateChange, upload }) { + const [copied, setCopied] = React.useState(false); + const [, , reset] = useTimeoutFn(() => setCopied(false), 3000); + const [retryTimeout, setRetryTimeout] = React.useState(3000); // retry delay after "429: TOO_MANY_REQUESTS" + + const handleCopy = (url) => { + copy(url); + setCopied(true); + reset(); + }; + + React.useEffect(() => { + if (upload.status === "uploading" && !upload.startedTime) { + onUploadStateChange(upload.id, { startedTime: Date.now() }); + + (async () => { + const onUploadProgress = (progress) => { + const status = progress === 1 ? "processing" : "uploading"; + + onUploadStateChange(upload.id, { status, progress }); + }; + + try { + let response; + + if (upload.mode === "directory") { + const files = upload.file.files; + const directory = files.reduce((acc, file) => ({ ...acc, [getRelativeFilePath(file)]: file }), {}); + const name = encodeURIComponent(upload.file.name); + + response = await skynetClient.uploadDirectory(directory, name, { onUploadProgress }); + } else { + response = await skynetClient.uploadFile(upload.file, { onUploadProgress }); + } + + const url = await skynetClient.getSkylinkUrl(response.skylink, { subdomain: upload.mode === "directory" }); + + onUploadStateChange(upload.id, { status: "complete", url }); + } catch (error) { + if (error?.response?.status === StatusCodes.TOO_MANY_REQUESTS) { + onUploadStateChange(upload.id, { status: "retrying", progress: 0 }); + + setTimeout(() => { + onUploadStateChange(upload.id, { status: "enqueued", startedTime: null }); + setRetryTimeout((timeout) => timeout * 2); // increase timeout on next retry + }, retryTimeout); + } else { + onUploadStateChange(upload.id, { status: "error", error: buildUploadErrorMessage(error) }); + } + } + })(); + } + }, [onUploadStateChange, upload, retryTimeout]); + + return ( +
+
+
+ +
+
+
+
{upload.file.name}
+
+
+
+ {upload.status === "uploading" && ( + + Uploading {bytes(upload.file.size * upload.progress)} of {bytes(upload.file.size)} + + )} + {upload.status === "enqueued" && Upload in queue, please wait} + {upload.status === "processing" && Processing...} + {upload.status === "complete" && ( + + {upload.url} + + )} + {upload.status === "error" && upload.error && {upload.error}} + {upload.status === "retrying" && ( + Too many parallel requests, retrying in {retryTimeout / 1000} seconds + )} +
+
+
+
+ {upload.status === "uploading" && ( + {Math.floor(upload.progress * 100)}% + )} + {upload.status === "processing" && Wait} + {upload.status === "complete" && ( + + )} +
+
+ + +
+ ); +} diff --git a/packages/dashboard-v2/src/components/Uploader/UploaderItemIcon.js b/packages/dashboard-v2/src/components/Uploader/UploaderItemIcon.js new file mode 100644 index 00000000..c0d6a766 --- /dev/null +++ b/packages/dashboard-v2/src/components/Uploader/UploaderItemIcon.js @@ -0,0 +1,20 @@ +import cn from "classnames"; + +import { CircledCheckmarkIcon, CircledErrorIcon, CircledProgressIcon, CircledArrowUpIcon } from "../Icons"; + +export default function UploaderItemIcon({ status }) { + switch (status) { + case "enqueued": + case "retrying": + case "uploading": + return ; + case "processing": + return ; + case "complete": + return ; + case "error": + return ; + default: + return null; + } +} diff --git a/packages/dashboard-v2/src/components/Uploader/buildUploadErrorMessage.js b/packages/dashboard-v2/src/components/Uploader/buildUploadErrorMessage.js new file mode 100644 index 00000000..c41cd717 --- /dev/null +++ b/packages/dashboard-v2/src/components/Uploader/buildUploadErrorMessage.js @@ -0,0 +1,37 @@ +import { getReasonPhrase } from "http-status-codes"; +import bytes from "pretty-bytes"; + +export default function buildUploadErrorMessage(error) { + // The request was made and the server responded with a status code that falls out of the range of 2xx + if (error.response) { + if (error.response.data.message) { + return `Upload failed with error: ${error.response.data.message}`; + } + + const statusCode = error.response.status; + const statusText = getReasonPhrase(error.response.status); + + return `Upload failed, our server received your request but failed with status code: ${statusCode} ${statusText}`; + } + + // The request was made but no response was received. The best we can do is detect whether browser is online. + // This will be triggered mostly if the server is offline or misconfigured and doesn't respond to valid request. + if (error.request) { + if (!navigator.onLine) { + return "You are offline, please connect to the internet and try again"; + } + + // TODO: We should add a note "our team has been notified" and have some kind of notification with this error. + return "Server failed to respond to your request, please try again later."; + } + + // Match the error message to a message returned by TUS when upload exceeds max file size + const matchTusMaxFileSizeError = error.message.match(/upload exceeds maximum size: \d+ > (?\d+)/); + + if (matchTusMaxFileSizeError) { + return `File exceeds size limit of ${bytes(parseInt(matchTusMaxFileSizeError.groups.limit, 10))}`; + } + + // TODO: We should add a note "our team has been notified" and have some kind of notification with this error. + return `Critical error, please refresh the application and try again. ${error.message}`; +} diff --git a/packages/dashboard-v2/src/pages/index.js b/packages/dashboard-v2/src/pages/index.js index a8d48ae2..593db528 100644 --- a/packages/dashboard-v2/src/pages/index.js +++ b/packages/dashboard-v2/src/pages/index.js @@ -9,6 +9,7 @@ import LatestActivity from "../components/LatestActivity/LatestActivity"; import DashboardLayout from "../layouts/DashboardLayout"; import Slider from "../components/Slider/Slider"; import CurrentUsage from "../components/CurrentUsage"; +import Uploader from "../components/Uploader/Uploader"; const IndexPage = () => { const showRecentActivity = useMedia(`(min-width: ${theme.screens.md})`); @@ -18,15 +19,15 @@ const IndexPage = () => {
+ - -
Upload files...
+ + - -
Upload a directory...
+ +
, @@ -36,6 +37,7 @@ const IndexPage = () => { Usage } + className="h-[330px]" > , @@ -45,6 +47,7 @@ const IndexPage = () => { Current plan } + className="h-[330px]" >
  • Current
  • diff --git a/packages/dashboard-v2/yarn.lock b/packages/dashboard-v2/yarn.lock index c03ae52a..e860c61c 100644 --- a/packages/dashboard-v2/yarn.lock +++ b/packages/dashboard-v2/yarn.lock @@ -1107,7 +1107,7 @@ core-js-pure "^3.20.2" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.17.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== @@ -4518,6 +4518,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +attr-accept@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" + integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== + autoprefixer@^10.4.0, autoprefixer@^10.4.2: version "10.4.2" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.2.tgz#25e1df09a31a9fba5c40b578936b90d35c9d4d3b" @@ -4548,7 +4553,7 @@ axe-core@^4.3.5: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413" integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw== -axios@^0.21.1: +axios@^0.21.0, axios@^0.21.1: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== @@ -4817,6 +4822,13 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base32-encode@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/base32-encode/-/base32-encode-1.2.0.tgz#e150573a5e431af0a998e32bdfde7045725ca453" + integrity sha512-cHFU8XeRyx0GgmoWi5qHMCVRiqU6J3MHWxVgun7jggCBUpVzm1Ir7M9dYr2whjSNc3tFeXfQ/oZjQu/4u55h9A== + dependencies: + to-data-view "^1.1.0" + base64-arraybuffer@0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812" @@ -4888,6 +4900,11 @@ bl@^4.0.0, bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" +blakejs@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.1.1.tgz#bf313053978b2cd4c444a48795710be05c785702" + integrity sha512-bLG6PHOCZJKNshTjGRBvET0vTciwQE6zFKOKKXPDJfwFBd4Ac0yBfPZqcGvGJap50l7ktvlpFqc2jGVaUgbJgg== + bluebird@^3.3.5, bluebird@^3.5.5, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -5114,6 +5131,14 @@ buffer@^5.2.0, buffer@^5.5.0, buffer@^5.7.0: base64-js "^1.3.1" ieee754 "^1.1.13" +buffer@^6.0.1: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -5455,6 +5480,11 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +classnames@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== + clean-css@^4.2.3: version "4.2.4" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178" @@ -5820,6 +5850,11 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= +copy-text-to-clipboard@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.0.1.tgz#8cbf8f90e0a47f12e4a24743736265d157bce69c" + integrity sha512-rvVsHrpFcL4F2P8ihsoLdFHmd404+CMg71S756oRSeQgqk51U3kicGdnvfkrxva0xXH92SjGS62B0XIJsbh+9Q== + copy-to-clipboard@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae" @@ -7675,6 +7710,13 @@ file-loader@^6.2.0: loader-utils "^2.0.0" schema-utils "^3.0.0" +file-selector@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.4.0.tgz#59ec4f27aa5baf0841e9c6385c8386bef4d18b17" + integrity sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg== + dependencies: + tslib "^2.0.3" + file-system-cache@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/file-system-cache/-/file-system-cache-1.0.5.tgz#84259b36a2bbb8d3d6eb1021d3132ffe64cfff4f" @@ -11136,7 +11178,7 @@ nano-css@^5.3.1: stacktrace-js "^2.0.2" stylis "^4.0.6" -nanoid@^3.1.23, nanoid@^3.2.0: +nanoid@^3.1.23, nanoid@^3.2.0, nanoid@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== @@ -12582,7 +12624,7 @@ prompts@^2.4.0, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.7.2: +prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -12731,6 +12773,11 @@ querystring@^0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -12867,6 +12914,15 @@ react-draggable@^4.4.3: clsx "^1.1.1" prop-types "^15.6.0" +react-dropzone@^12.0.4: + version "12.0.4" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-12.0.4.tgz#b88eeaa2c7118f7fd042404682b17a1d466f2fcf" + integrity sha512-fcqHEYe1MzAghU6/Hz86lHDlBNsA+lO48nAcm7/wA+kIzwS6uuJbUG33tBZjksj7GAZ1iUQ6NHwjUURPmSGang== + dependencies: + attr-accept "^2.2.2" + file-selector "^0.4.0" + prop-types "^15.8.1" + react-element-to-jsx-string@^14.3.4: version "14.3.4" resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.4.tgz#709125bc72f06800b68f9f4db485f2c7d31218a8" @@ -13843,6 +13899,30 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== +sjcl@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/sjcl/-/sjcl-1.0.8.tgz#f2ec8d7dc1f0f21b069b8914a41a8f236b0e252a" + integrity sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ== + +skynet-js@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/skynet-js/-/skynet-js-3.0.2.tgz#d08a33066ee85b86e4ffc7c31591239a88da6fbe" + integrity sha512-rbmpOGbDwg2FcsZ7HkmGhVaUwWO6kaysRFKTBC3yGiV+b6fbnpPPNCskvh8kWwbTsj+koWkSRUFYqG7cc+eTuA== + dependencies: + "@babel/runtime" "^7.11.2" + axios "^0.21.0" + base32-encode "^1.1.1" + base64-js "^1.3.1" + blakejs "^1.1.0" + buffer "^6.0.1" + mime "^2.5.2" + path-browserify "^1.0.1" + randombytes "^2.1.0" + sjcl "^1.0.8" + tweetnacl "^1.0.3" + url-join "^4.0.1" + url-parse "^1.4.7" + slash@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" @@ -14774,6 +14854,11 @@ to-arraybuffer@^1.0.0: resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= +to-data-view@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/to-data-view/-/to-data-view-1.1.0.tgz#08d6492b0b8deb9b29bdf1f61c23eadfa8994d00" + integrity sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ== + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -14952,6 +15037,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tweetnacl@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -15245,6 +15335,11 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +url-join@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" + integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== + url-loader@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2" @@ -15261,6 +15356,14 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" +url-parse@^1.4.7: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"