Merge pull request #1996 from SkynetLabs/dashboard-v2-copy-changes

Dashboard v2 - addressing ops team feedback + DX improvements
This commit is contained in:
Karol Wypchło 2022-04-14 13:14:13 +02:00 committed by GitHub
commit 162e6de77a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 242 additions and 212 deletions

View File

@ -11,15 +11,20 @@ This is a Gatsby application. To run it locally, all you need is:
## Accessing remote APIs
To be able to log in on a local environment with your production credentials, you'll need to make the browser believe you're actually on the same domain, otherwise the browser will block the session cookie.
To have a fully functioning local environment, you'll need to make the browser believe you're actually on the same domain as a working API (i.e. a remote dev or production server) -- otherwise the browser will block the session cookie.
To do the trick, configure proper environment variables in the `.env.development` file.
This file allows to easily control which domain name you want to use locally and which API you'd like to access.
To do the trick, edit your `/etc/hosts` file and add a record like this:
Example:
```
127.0.0.1 local.skynetpro.net
```env
GATSBY_PORTAL_DOMAIN=skynetfree.net # Use skynetfree.net APIs
GATSBY_HOST=local.skynetfree.net # Address of your local build
```
then run `yarn develop:secure` -- it will run `gatsby develop` with `--https --host=local.skynetpro.net -p=443` options.
If you're on macOS, you may need to `sudo` the command to successfully bind to port `443`.
> It's recommended to keep the 2LD the same, so any cookies dispatched by the API work without issues.
> **NOTE:** This should become easier once we have a docker image for the new dashboard.
With the file configured, run `yarn develop:secure` -- it will run `gatsby develop` with `--https -p=443` options.
If you're on macOS, you may need to `sudo` the command to successfully bind to port `443` (https).
Gatsby will automatically add a proper entry to your `/etc/hosts` file and clean it up when process exits.

View File

@ -1,9 +1,15 @@
require("dotenv").config({
path: `.env.${process.env.NODE_ENV}`,
});
const { createProxyMiddleware } = require("http-proxy-middleware");
const { GATSBY_PORTAL_DOMAIN } = process.env;
module.exports = {
siteMetadata: {
title: "Skynet Account",
siteUrl: `https://account.${process.env.GATSBY_PORTAL_DOMAIN}/`,
title: `Account Dashboard`,
siteUrl: `https://account.${GATSBY_PORTAL_DOMAIN}`,
},
trailingSlash: "never",
plugins: [
@ -24,13 +30,27 @@ module.exports = {
},
],
developMiddleware: (app) => {
// Proxy Accounts service API requests:
app.use(
"/api/",
createProxyMiddleware({
target: "https://account.skynetpro.net",
target: `https://account.${GATSBY_PORTAL_DOMAIN}`,
secure: false, // Do not reject self-signed certificates.
changeOrigin: true,
})
);
// Proxy /skynet requests (e.g. uploads)
app.use(
["/skynet", "/__internal/"],
createProxyMiddleware({
target: `https://${GATSBY_PORTAL_DOMAIN}`,
secure: false, // Do not reject self-signed certificates.
changeOrigin: true,
pathRewrite: {
"^/skynet": "",
},
})
);
},
};

View File

@ -9,7 +9,7 @@
],
"scripts": {
"develop": "gatsby develop",
"develop:secure": "gatsby develop --https --host=local.skynetpro.net -p=443",
"develop:secure": "dotenv -e .env.development -- gatsby develop --https -p=443",
"start": "gatsby develop",
"build": "gatsby build",
"serve": "gatsby serve",
@ -60,6 +60,8 @@
"babel-loader": "^8.2.3",
"babel-plugin-preval": "^5.1.0",
"babel-plugin-styled-components": "^2.0.2",
"dotenv": "^16.0.0",
"dotenv-cli": "^5.1.0",
"eslint": "^8.9.0",
"eslint-config-react-app": "^7.0.0",
"eslint-plugin-storybook": "^0.5.6",

View File

@ -4,7 +4,7 @@ import { useCallback, useState } from "react";
import { Alert } from "../Alert";
import { Button } from "../Button";
import { AddSkylinkToAPIKeyForm } from "../forms/AddSkylinkToAPIKeyForm";
import { AddSkylinkToSponsorKeyForm } from "../forms/AddSkylinkToSponsorKeyForm";
import { CogIcon, TrashIcon } from "../Icons";
import { Modal } from "../Modal";
@ -13,7 +13,7 @@ import { useAPIKeyRemoval } from "./useAPIKeyRemoval";
export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
const { id, name, createdAt, skylinks } = apiKey;
const isPublic = apiKey.public === "true";
const isSponsorKey = apiKey.public === "true";
const [error, setError] = useState(null);
const onSkylinkListEdited = useCallback(() => {
@ -53,9 +53,9 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
}, [abortEdit]);
const skylinksNumber = skylinks?.length ?? 0;
const isNotConfigured = isPublic && skylinksNumber === 0;
const isNotConfigured = isSponsorKey && skylinksNumber === 0;
const skylinksPhrasePrefix = skylinksNumber === 0 ? "No" : skylinksNumber;
const skylinksPhrase = `${skylinksPhrasePrefix} ${skylinksNumber === 1 ? "skylink" : "skylinks"} configured`;
const skylinksPhrase = `${skylinksPhrasePrefix} ${skylinksNumber === 1 ? "skylink" : "skylinks"} sponsored`;
return (
<li
@ -66,21 +66,23 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
<span className="col-span-2 sm:col-span-1 flex items-center">
<span className="flex flex-col">
<span className={cn("truncate", { "text-palette-300": !name })}>{name || "unnamed key"}</span>
<button
onClick={promptEdit}
className={cn("text-xs hover:underline decoration-dotted", {
"text-error": isNotConfigured,
"text-palette-400": !isNotConfigured,
})}
>
{skylinksPhrase}
</button>
{isSponsorKey && (
<button
onClick={promptEdit}
className={cn("text-xs hover:underline decoration-dotted", {
"text-error": isNotConfigured,
"text-palette-400": !isNotConfigured,
})}
>
{skylinksPhrase}
</button>
)}
</span>
</span>
<span className="col-span-2 my-4 border-t border-t-palette-200/50 sm:hidden" />
<span className="text-palette-400">{dayjs(createdAt).format("MMM DD, YYYY")}</span>
<span className="flex items-center justify-end">
{isPublic && (
{isSponsorKey && (
<button
title="Add or remove skylinks"
aria-label="Add or remove skylinks"
@ -119,7 +121,7 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
)}
{editInitiated && (
<Modal onClose={closeEditModal} className="flex flex-col gap-4 text-center sm:px-8 sm:py-6">
<h4>Covered skylinks</h4>
<h4>Sponsored skylinks</h4>
{skylinks?.length > 0 ? (
<ul className="text-xs flex flex-col gap-2">
{skylinks.map((skylink) => (
@ -143,7 +145,7 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
<div className="flex flex-col gap-4">
{error && <Alert $variant="error">{error}</Alert>}
<AddSkylinkToAPIKeyForm addSkylink={addSkylink} />
<AddSkylinkToSponsorKeyForm addSkylink={addSkylink} />
</div>
<div className="flex gap-4 justify-center mt-4">
<Button onClick={closeEditModal}>Close</Button>

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { useUser } from "../../contexts/user";
import { SimpleUploadIcon } from "../Icons";
// import { SimpleUploadIcon } from "../Icons";
const AVATAR_PLACEHOLDER = "/images/avatar-placeholder.svg";
@ -20,6 +20,7 @@ export const AvatarUploader = (props) => {
>
<img src={imageUrl} className="w-[160px]" alt="" />
</div>
{/* TODO: uncomment when avatar uploads work
<div className="flex justify-center">
<button
className="flex items-center gap-4 hover:underline decoration-1 decoration-dashed underline-offset-2 decoration-gray-400"
@ -28,8 +29,8 @@ export const AvatarUploader = (props) => {
>
<SimpleUploadIcon size={20} className="shrink-0" /> Upload profile picture
</button>
{/* TODO: actual uploading */}
</div>
*/}
</div>
);
};

View File

@ -1,5 +1,6 @@
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import prettyBytes from "pretty-bytes";
import { useUser } from "../../contexts/user";
import useActivePlan from "../../hooks/useActivePlan";
@ -28,17 +29,20 @@ const CurrentPlan = () => {
}
return (
<div>
<div className="flex flex-col h-full">
<h4>{activePlan.name}</h4>
<div className="text-palette-400">
{activePlan.price === 0 && <p>100GB without paying a dime! 🎉</p>}
<div className="text-palette-400 justify-between flex flex-col grow">
{activePlan.price === 0 && activePlan.limits && (
<p>{prettyBytes(activePlan.limits.storageLimit, { binary: true })} without paying a dime! 🎉</p>
)}
{activePlan.price !== 0 &&
(user.subscriptionCancelAtPeriodEnd ? (
<p>Your subscription expires {dayjs(user.subscribedUntil).fromNow()}</p>
) : (
<p className="first-letter:uppercase">{dayjs(user.subscribedUntil).fromNow(true)} until the next payment</p>
))}
<LatestPayment user={user} />
{user.subscriptionStatus && <LatestPayment user={user} />}
<SuggestedPlan plans={plans} activePlan={activePlan} />
</div>
</div>

View File

@ -44,7 +44,7 @@ const useUsageData = () => {
};
const size = (bytes) => {
const text = fileSize(bytes ?? 0, { maximumFractionDigits: 0 });
const text = fileSize(bytes ?? 0, { maximumFractionDigits: 0, binary: true });
const [value, unit] = text.split(" ");
return {

View File

@ -1,6 +1,7 @@
import { useMemo } from "react";
import prettyBytes from "pretty-bytes";
import dayjs from "dayjs";
import { DATE_FORMAT } from "../../lib/config";
const parseFileName = (fileName) => {
const lastDotIndex = Math.max(0, fileName.lastIndexOf(".")) || Infinity;
@ -10,7 +11,7 @@ const parseFileName = (fileName) => {
const formatItem = ({ size, name: rawFileName, uploadedOn, downloadedOn, ...rest }) => {
const [name, type] = parseFileName(rawFileName);
const date = dayjs(uploadedOn || downloadedOn).format("MM/DD/YYYY; HH:MM");
const date = dayjs(uploadedOn || downloadedOn).format(DATE_FORMAT);
return {
...rest,

View File

@ -1,8 +1,19 @@
import * as React from "react";
import styled from "styled-components";
import { PageContainer } from "../PageContainer";
const FooterLink = styled.a.attrs({
className: "text-palette-400 underline decoration-dotted decoration-offset-4 decoration-1",
rel: "noreferrer",
target: "_blank",
})``;
export const Footer = () => (
<PageContainer className="font-content text-palette-300 py-4">
<p>© Skynet Labs Inc. All rights reserved.</p>
<p>
Made by <FooterLink href="https://skynetlabs.com">Skynet Labs</FooterLink>. Open-sourced{" "}
<FooterLink href="https://github.com/SkynetLabs/skynet-webportal">on Github</FooterLink>.
</p>
</PageContainer>
);

View File

@ -2,5 +2,5 @@ import { Link } from "gatsby";
import styled from "styled-components";
export default styled(Link).attrs({
className: "text-primary underline-offset-3 decoration-dotted hover:text-primary-light hover:underline",
className: "text-primary underline-offset-2 decoration-1 decoration-dotted hover:text-primary-light hover:underline",
})``;

View File

@ -3,13 +3,13 @@ import useSWR from "swr";
import { Table, TableBody, TableCell, TableRow } from "../Table";
import { ContainerLoadingIndicator } from "../LoadingIndicator";
import useFormattedFilesData from "../FileList/useFormattedFilesData";
import { ViewAllLink } from "./ViewAllLink";
import useFormattedActivityData from "./useFormattedActivityData";
export default function ActivityTable({ type }) {
const { data, error } = useSWR(`user/${type}?pageSize=3`);
const items = useFormattedActivityData(data?.items || []);
const items = useFormattedFilesData(data?.items || []);
if (!items.length) {
return (

View File

@ -1,23 +1,13 @@
import * as React from "react";
import { Panel } from "../Panel";
import { Tab, TabPanel, Tabs } from "../Tabs";
import ActivityTable from "./ActivityTable";
export default function LatestActivity() {
return (
<Panel title="Latest activity">
<Tabs>
<Tab id="uploads" title="Uploads" />
<Tab id="downloads" title="Downloads" />
<TabPanel tabId="uploads" className="pt-4">
<ActivityTable type="uploads" />
</TabPanel>
<TabPanel tabId="downloads" className="pt-4">
<ActivityTable type="downloads" />
</TabPanel>
</Tabs>
<Panel title="Latest uploads">
<ActivityTable type="uploads" />
</Panel>
);
}

View File

@ -1,26 +0,0 @@
import { useMemo } from "react";
import prettyBytes from "pretty-bytes";
import dayjs from "dayjs";
const parseFileName = (fileName) => {
const lastDotIndex = Math.max(0, fileName.lastIndexOf(".")) || Infinity;
return [fileName.substr(0, lastDotIndex), fileName.substr(lastDotIndex)];
};
const formatItem = ({ size, name: rawFileName, uploadedOn, downloadedOn, ...rest }) => {
const [name, type] = parseFileName(rawFileName);
const date = dayjs(uploadedOn || downloadedOn).format("MM/DD/YYYY; HH:MM");
return {
...rest,
date,
size: prettyBytes(size),
type,
name,
};
};
const useFormattedActivityData = (items) => useMemo(() => items.map(formatItem), [items]);
export default useFormattedActivityData;

View File

@ -0,0 +1 @@
export * from "./Tooltip";

View File

@ -118,7 +118,7 @@ const Uploader = ({ mode }) => {
</div>
{uploads.length > 0 && (
<div className="flex flex-col space-y-4 py-10">
<div className="flex flex-col space-y-4 pt-6 pb-10">
{uploads.map((upload) => (
<UploaderItem key={upload.id} onUploadStateChange={onUploadStateChange} upload={upload} />
))}

View File

@ -34,8 +34,9 @@ export const AccountRemovalForm = ({ abort, onSuccess }) => {
<Form className="flex flex-col gap-4">
<div>
<h4>Delete account</h4>
<p>This will completely delete your account.</p>
<p>
This will completely delete your account. <strong>This process can't be undone.</strong>
<strong>This process cannot be undone.</strong>
</p>
</div>

View File

@ -2,6 +2,7 @@ import * as Yup from "yup";
import { forwardRef, useImperativeHandle, useState } from "react";
import PropTypes from "prop-types";
import { Formik, Form } from "formik";
import cn from "classnames";
import accountsService from "../../services/accountsService";
@ -9,7 +10,6 @@ import { Alert } from "../Alert";
import { Button } from "../Button";
import { CopyButton } from "../CopyButton";
import { TextField } from "../Form/TextField";
import { CircledProgressIcon, PlusIcon } from "../Icons";
const newAPIKeySchema = Yup.object().shape({
name: Yup.string(),
@ -22,7 +22,7 @@ const State = {
};
export const APIKeyType = {
Public: "public",
Sponsor: "sponsor",
General: "general",
};
@ -37,10 +37,10 @@ export const AddAPIKeyForm = forwardRef(({ onSuccess, type }, ref) => {
return (
<div ref={ref} className="flex flex-col gap-4">
{state === State.Success && (
<Alert $variant="success" className="text-center">
<Alert $variant="success">
<strong>Success!</strong>
<p>Please copy your new API key below. We'll never show it again!</p>
<div className="flex items-center gap-2 mt-4 justify-center">
<div className="flex items-center gap-2 mt-4">
<code className="p-2 rounded border border-palette-200 text-xs selection:bg-primary/30 truncate">
{generatedKey}
</code>
@ -62,8 +62,8 @@ export const AddAPIKeyForm = forwardRef(({ onSuccess, type }, ref) => {
.post("user/apikeys", {
json: {
name,
public: type === APIKeyType.Public ? "true" : "false",
skylinks: type === APIKeyType.Public ? [] : null,
public: type === APIKeyType.Sponsor ? "true" : "false",
skylinks: type === APIKeyType.Sponsor ? [] : null,
},
})
.json();
@ -78,26 +78,20 @@ export const AddAPIKeyForm = forwardRef(({ onSuccess, type }, ref) => {
}}
>
{({ errors, touched, isSubmitting }) => (
<Form className="grid grid-cols-[1fr_min-content] w-full gap-y-2 gap-x-4 items-start">
<div className="flex items-center">
<TextField
type="text"
id="name"
name="name"
label="New API Key Name"
placeholder="my_applications_statistics"
error={errors.name}
touched={touched.name}
/>
</div>
<div className="flex mt-5 justify-center">
{isSubmitting ? (
<CircledProgressIcon size={38} className="text-palette-300 animate-[spin_3s_linear_infinite]" />
) : (
<Button type="submit" className="px-2.5" aria-label="Create general API key">
<PlusIcon size={14} />
</Button>
)}
<Form className="flex flex-col gap-4">
<TextField
type="text"
id="name"
name="name"
label="New API Key Label"
placeholder="my_applications_statistics"
error={errors.name}
touched={touched.name}
/>
<div className="flex justify-center">
<Button type="submit" disabled={isSubmitting} className={cn({ "cursor-wait": isSubmitting })}>
{isSubmitting ? "Generating your API key..." : "Generate your API key"}
</Button>
</div>
</Form>
)}
@ -110,5 +104,5 @@ AddAPIKeyForm.displayName = "AddAPIKeyForm";
AddAPIKeyForm.propTypes = {
onSuccess: PropTypes.func.isRequired,
type: PropTypes.oneOf([APIKeyType.Public, APIKeyType.General]).isRequired,
type: PropTypes.oneOf([APIKeyType.Sponsor, APIKeyType.General]).isRequired,
};

View File

@ -19,7 +19,7 @@ const newSkylinkSchema = Yup.object().shape({
}),
});
export const AddSkylinkToAPIKeyForm = ({ addSkylink }) => (
export const AddSkylinkToSponsorKeyForm = ({ addSkylink }) => (
<Formik
initialValues={{
skylink: "",
@ -58,6 +58,6 @@ export const AddSkylinkToAPIKeyForm = ({ addSkylink }) => (
</Formik>
);
AddSkylinkToAPIKeyForm.propTypes = {
AddSkylinkToSponsorKeyForm.propTypes = {
addSkylink: PropTypes.func.isRequired,
};

View File

@ -25,7 +25,7 @@ const skylinkValidator = (optional) => (value) => {
}
};
const newPublicAPIKeySchema = Yup.object().shape({
const newSponsorKeySchema = Yup.object().shape({
name: Yup.string(),
skylinks: Yup.array().of(Yup.string().test("skylink", "Provide a valid Skylink", skylinkValidator(false))),
nextSkylink: Yup.string().when("skylinks", {
@ -41,7 +41,7 @@ const State = {
Failure: "FAILURE",
};
export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
export const AddSponsorKeyForm = forwardRef(({ onSuccess }, ref) => {
const [state, setState] = useState(State.Pure);
const [generatedKey, setGeneratedKey] = useState(null);
@ -52,10 +52,10 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
return (
<div ref={ref} className="flex flex-col gap-4">
{state === State.Success && (
<Alert $variant="success" className="text-center">
<Alert $variant="success">
<strong>Success!</strong>
<p>Please copy your new API key below. We'll never show it again!</p>
<div className="flex items-center gap-2 mt-4 justify-center">
<div className="flex items-center gap-2 mt-4">
<code className="p-2 rounded border border-palette-200 text-xs selection:bg-primary/30 truncate">
{generatedKey}
</code>
@ -72,7 +72,7 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
skylinks: [],
nextSkylink: "",
}}
validationSchema={newPublicAPIKeySchema}
validationSchema={newSponsorKeySchema}
onSubmit={async ({ name, skylinks, nextSkylink }, { resetForm }) => {
try {
const { key } = await accountsService
@ -101,14 +101,14 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
type="text"
id="name"
name="name"
label="Public API Key Name"
label="Sponsor API Key Name"
placeholder="my_applications_statistics"
error={errors.name}
touched={touched.name}
/>
</div>
<div>
<h6 className="text-palette-300 mb-2">Skylinks accessible with the new key</h6>
<h6 className="text-palette-300 mb-2">Skylinks sponsored by the new key</h6>
<FieldArray
name="skylinks"
render={({ push, remove }) => {
@ -182,7 +182,7 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
className={cn("px-2.5", { "cursor-wait": isSubmitting })}
disabled={!isValid || isSubmitting}
>
{isSubmitting ? "Generating" : "Generate"} your public key
{isSubmitting ? "Generating your sponsor key..." : "Generate your sponsor key"}
</Button>
</div>
</Form>
@ -192,8 +192,8 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
);
});
AddPublicAPIKeyForm.displayName = "AddAPIKeyForm";
AddSponsorKeyForm.displayName = "AddSponsorKeyForm";
AddPublicAPIKeyForm.propTypes = {
AddSponsorKeyForm.propTypes = {
onSuccess: PropTypes.func.isRequired,
};

View File

@ -32,14 +32,16 @@ export const SignUpForm = ({ onSuccess, onFailure }) => (
validationSchema={registrationSchema}
onSubmit={async ({ email, password }, { setErrors }) => {
try {
await accountsService.post("user", {
json: {
email,
password,
},
});
const user = await accountsService
.post("user", {
json: {
email,
password,
},
})
.json();
onSuccess();
onSuccess(user);
} catch (err) {
let isFormErrorSet = false;

View File

@ -16,8 +16,8 @@ const Sidebar = () => (
<SidebarLink activeClassName="!border-l-primary" to="/settings/export">
Export
</SidebarLink>
<SidebarLink activeClassName="!border-l-primary" to="/settings/api-keys">
API Keys
<SidebarLink activeClassName="!border-l-primary" to="/settings/developer-settings">
Developer settings
</SidebarLink>
</nav>
</aside>

View File

@ -0,0 +1 @@
export const DATE_FORMAT = "MMM D, YYYY HH:MM";

View File

@ -1,5 +1,4 @@
import { useEffect, useState } from "react";
import { navigate } from "gatsby";
import { useCallback, useState } from "react";
import bytes from "pretty-bytes";
import AuthLayout from "../../layouts/AuthLayout";
@ -10,6 +9,7 @@ import { SignUpForm } from "../../components/forms/SignUpForm";
import { usePortalSettings } from "../../contexts/portal-settings";
import { PlansProvider, usePlans } from "../../contexts/plans";
import { Metadata } from "../../components/Metadata";
import { useUser } from "../../contexts/user";
const FreePortalHeader = () => {
const { plans } = usePlans();
@ -47,14 +47,14 @@ const State = {
const SignUpPage = () => {
const [state, setState] = useState(State.Pure);
const { settings } = usePortalSettings();
const { mutate: refreshUserState } = useUser();
useEffect(() => {
if (state === State.Success) {
const timer = setTimeout(() => navigate(settings.isSubscriptionRequired ? "/upgrade" : "/"), 3000);
return () => clearTimeout(timer);
}
}, [state, settings.isSubscriptionRequired]);
const onUserCreated = useCallback(
(newUser) => {
refreshUserState(newUser);
},
[refreshUserState]
);
return (
<PlansProvider>
@ -70,7 +70,7 @@ const SignUpPage = () => {
{state !== State.Success && (
<>
<SignUpForm onSuccess={() => setState(State.Success)} onFailure={() => setState(State.Failure)} />
<SignUpForm onSuccess={onUserCreated} onFailure={() => setState(State.Failure)} />
<p className="text-sm text-center mt-8">
Already have an account? <HighlightedLink to="/auth/login">Sign in</HighlightedLink>
@ -78,14 +78,6 @@ const SignUpPage = () => {
</>
)}
{state === State.Success && (
<div>
<p className="text-primary font-semibold">Please check your inbox and confirm your email address.</p>
<p>You will be redirected to your dashboard shortly.</p>
<HighlightedLink to="/">Click here to go there now.</HighlightedLink>
</div>
)}
{state === State.Failure && (
<p className="text-error text-center">Something went wrong, please try again later.</p>
)}

View File

@ -1,32 +1,19 @@
import * as React from "react";
import { useSearchParam } from "react-use";
import DashboardLayout from "../layouts/DashboardLayout";
import { Panel } from "../components/Panel";
import { Tab, TabPanel, Tabs } from "../components/Tabs";
import { Metadata } from "../components/Metadata";
import FileList from "../components/FileList/FileList";
const FilesPage = () => {
const defaultTab = useSearchParam("tab");
return (
<>
<Metadata>
<title>My Files</title>
<title>Files</title>
</Metadata>
<Panel title="Files">
<Tabs defaultTab={defaultTab || "uploads"}>
<Tab id="uploads" title="Uploads" />
<Tab id="downloads" title="Downloads" />
<TabPanel tabId="uploads" className="pt-4">
<FileList type="uploads" />
</TabPanel>
<TabPanel tabId="downloads" className="pt-4">
<FileList type="downloads" />
</TabPanel>
</Tabs>
<FileList type="uploads" />
</Panel>
</>
);

View File

@ -6,13 +6,14 @@ import UserSettingsLayout from "../../layouts/UserSettingsLayout";
import { AddAPIKeyForm, APIKeyType } from "../../components/forms/AddAPIKeyForm";
import { APIKeyList } from "../../components/APIKeyList/APIKeyList";
import { Alert } from "../../components/Alert";
import { AddPublicAPIKeyForm } from "../../components/forms/AddPublicAPIKeyForm";
import { AddSponsorKeyForm } from "../../components/forms/AddSponsorKeyForm";
import { Metadata } from "../../components/Metadata";
import HighlightedLink from "../../components/HighlightedLink";
const APIKeysPage = () => {
const { data: apiKeys = [], mutate: reloadKeys, error } = useSWR("user/apikeys");
const generalKeys = apiKeys.filter(({ public: isPublic }) => isPublic === "false");
const publicKeys = apiKeys.filter(({ public: isPublic }) => isPublic === "true");
const DeveloperSettingsPage = () => {
const { data: allKeys = [], mutate: reloadKeys, error } = useSWR("user/apikeys");
const apiKeys = allKeys.filter(({ public: isPublic }) => isPublic === "false");
const sponsorKeys = allKeys.filter(({ public: isPublic }) => isPublic === "true");
const publicFormRef = useRef();
const generalFormRef = useRef();
@ -31,53 +32,57 @@ const APIKeysPage = () => {
return (
<>
<Metadata>
<title>API Keys</title>
<title>Developer settings</title>
</Metadata>
<div className="flex flex-col xl:flex-row">
<div className="flex flex-col gap-10 lg:shrink-0 lg:max-w-[576px] xl:max-w-[524px]">
<div className="flex flex-col gap-10 lg:shrink-0 lg:max-w-[576px] xl:max-w-[524px] leading-relaxed">
<div>
<h4>API Keys</h4>
<p className="leading-relaxed">There are two types of API keys that you can generate for your account.</p>
<p>Make sure to use the appropriate type.</p>
<h4>Developer settings</h4>
<p>API keys allow developers and applications to extend the functionality of your portal account.</p>
<p>Skynet uses two types of API keys, explained below.</p>
</div>
<hr />
<section className="flex flex-col gap-2">
<h5>Public keys</h5>
<p className="text-palette-500">
Public keys provide read access to a selected list of skylinks. You can share them publicly.
<h5>Sponsor keys</h5>
<div className="text-palette-500"></div>
<p>
Sponsor keys allow users without an account on this portal to download skylinks covered by the API key.
</p>
<p>
Learn more about sponsoring content with Sponsor API Keys{" "}
<HighlightedLink as="a" href="#">
here
</HighlightedLink>
.
</p>{" "}
{/* TODO: missing documentation link */}
<div className="mt-4">
<AddPublicAPIKeyForm ref={publicFormRef} onSuccess={refreshState} />
<AddSponsorKeyForm ref={publicFormRef} onSuccess={refreshState} />
</div>
{error ? (
<Alert $variant="error" className="mt-4">
An error occurred while loading your API keys. Please try again later.
An error occurred while loading your sponsor keys. Please try again later.
</Alert>
) : (
<div className="mt-4">
{publicKeys?.length > 0 ? (
<APIKeyList title="Your public keys" keys={publicKeys} reloadKeys={() => refreshState(true)} />
{sponsorKeys?.length > 0 ? (
<APIKeyList title="Your public keys" keys={sponsorKeys} reloadKeys={() => refreshState(true)} />
) : (
<Alert $variant="info">No public API keys found.</Alert>
<Alert $variant="info">No sponsor keys found.</Alert>
)}
</div>
)}
</section>
<hr />
<section className="flex flex-col gap-2">
<h5>General keys</h5>
<h5>API keys</h5>
<p className="text-palette-500">
These keys provide full access to <b>Accounts</b> service and are equivalent to using a JWT token.
These keys allow uploading and downloading skyfiles, as well as reading and writing to the registry.
</p>
<p className="underline">
This type of API keys needs to be kept secret and should never be shared with anyone.
</p>
<div className="mt-4">
<AddAPIKeyForm ref={generalFormRef} onSuccess={refreshState} type={APIKeyType.General} />
</div>
@ -88,10 +93,10 @@ const APIKeysPage = () => {
</Alert>
) : (
<div className="mt-4">
{generalKeys?.length > 0 ? (
<APIKeyList title="Your general keys" keys={generalKeys} reloadKeys={() => refreshState(true)} />
{apiKeys?.length > 0 ? (
<APIKeyList title="Your API keys" keys={apiKeys} reloadKeys={() => refreshState(true)} />
) : (
<Alert $variant="info">No general API keys found.</Alert>
<Alert $variant="info">No API keys found.</Alert>
)}
</div>
)}
@ -105,6 +110,6 @@ const APIKeysPage = () => {
);
};
APIKeysPage.Layout = UserSettingsLayout;
DeveloperSettingsPage.Layout = UserSettingsLayout;
export default APIKeysPage;
export default DeveloperSettingsPage;

View File

@ -38,8 +38,8 @@ const ExportPage = () => {
<section>
<h4>Export</h4>
<p>
Et quidem exercitus quid ex eo delectu rerum, quem modo ista sis aequitate. Probabo, inquit, modo dixi,
constituto.
Select the items you want to export. You can use this data to migrate your account to another Skynet
portal.
</p>
</section>
<hr />

View File

@ -8,6 +8,10 @@ import { Modal } from "../../components/Modal/Modal";
import { AccountRemovalForm } from "../../components/forms/AccountRemovalForm";
import { Alert } from "../../components/Alert";
import { Metadata } from "../../components/Metadata";
import HighlightedLink from "../../components/HighlightedLink";
import { AvatarUploader } from "../../components/AvatarUploader/AvatarUploader";
import { useMedia } from "react-use";
import theme from "../../lib/theme";
const State = {
Pure: "PURE",
@ -19,10 +23,16 @@ const AccountPage = () => {
const { user, mutate: reloadUser } = useUser();
const [state, setState] = useState(State.Pure);
const [removalInitiated, setRemovalInitiated] = useState(false);
const isLargeScreen = useMedia(`(min-width: ${theme.screens.xl})`);
const prompt = () => setRemovalInitiated(true);
const abort = () => setRemovalInitiated(false);
const onAccountRemoved = useCallback(async () => {
await reloadUser(null);
await navigate("/auth/login");
}, [reloadUser]);
const onSettingsUpdated = useCallback(
async (updatedState) => {
try {
@ -45,14 +55,7 @@ const AccountPage = () => {
</Metadata>
<div className="flex flex-col xl:flex-row">
<div className="flex flex-col gap-10 lg:shrink-0 lg:max-w-[576px] xl:max-w-[524px]">
<section>
<h4>Account</h4>
<p>
Tum dicere exorsus est laborum et quasi involuta aperiri, altera prompta et expedita. Primum igitur,
inquit, modo ista sis aequitate.
</p>
</section>
<hr />
<h4>Account</h4>
<section className="flex flex-col gap-8">
{state === State.Failure && (
<Alert $variant="error">There was an error processing your request. Please try again later.</Alert>
@ -63,7 +66,23 @@ const AccountPage = () => {
<hr />
<section>
<h6 className="text-palette-400">Delete account</h6>
<p>This will completely delete your account. This process can't be undone.</p>
<div className="my-4">
<p>
This action will delete your account and <strong>cannot be undone</strong>.
</p>
<p>
Your uploaded files will remain accessible while any portal continues to{" "}
<HighlightedLink
as="a"
href="https://support.skynetlabs.com/key-concepts/faqs#what-is-pinning"
target="_blank"
rel="noreferrer"
>
pin
</HighlightedLink>{" "}
them to Skynet.
</p>
</div>
<button
type="button"
onClick={prompt}
@ -73,9 +92,12 @@ const AccountPage = () => {
</button>
</section>
</div>
<div className="flex w-full justify-start xl:justify-end">
{isLargeScreen && <AvatarUploader className="flex flex-col gap-4" />}
</div>
{removalInitiated && (
<Modal onClose={abort} className="text-center">
<AccountRemovalForm abort={abort} onSuccess={() => navigate("/auth/login")} />
<AccountRemovalForm abort={abort} onSuccess={onAccountRemoved} />
</Modal>
)}
</div>

View File

@ -18,18 +18,13 @@ const NotificationsPage = () => {
<section>
{/* TODO: saves on change */}
<Switch onChange={console.info.bind(console)} labelClassName="!items-start flex-col md:flex-row">
I agreee to get the latest news, updates and special offers delivered to my email inbox.
I agree to receive emails of the latest news, updates and offers.
</Switch>
</section>
<hr />
<section>
<h6 className="text-palette-300">Statistics</h6>
{/* TODO: proper content :) */}
<p>
Si sine causa, nollem me tamen laudandis maioribus meis corrupisti nec in malis. Si sine causa, mox
videro.
</p>
<p>Check below to be notified by email when your usage approaches your plan's limits.</p>
<ul className="mt-7 flex flex-col gap-2">
<li>
{/* TODO: saves on change */}
@ -37,7 +32,7 @@ const NotificationsPage = () => {
</li>
<li>
{/* TODO: saves on change */}
<Switch onChange={console.info.bind(console)}>File limit</Switch>
<Switch onChange={console.info.bind(console)}>Files limit</Switch>
</li>
</ul>
</section>

View File

@ -1,3 +1,3 @@
import { SkynetClient } from "skynet-js";
export default new SkynetClient("https://skynetpro.net"); // TODO: proper API url
export default new SkynetClient(`https://${process.env.GATSBY_PORTAL_DOMAIN}`);

View File

@ -6735,11 +6735,31 @@ dot-prop@^5.2.0:
dependencies:
is-obj "^2.0.0"
dotenv-cli@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-5.1.0.tgz#0d2942b089082da0157f9b26bd6c5c4dd51ef48e"
integrity sha512-NoEZAlKo9WVrG0b3i9mBxdD6INdDuGqdgR74t68t8084QcI077/1MnPerRW1odl+9uULhcdnQp2U0pYVppKHOA==
dependencies:
cross-spawn "^7.0.3"
dotenv "^16.0.0"
dotenv-expand "^8.0.1"
minimist "^1.2.5"
dotenv-expand@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
dotenv-expand@^8.0.1:
version "8.0.3"
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.3.tgz#29016757455bcc748469c83a19b36aaf2b83dd6e"
integrity sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==
dotenv@^16.0.0:
version "16.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411"
integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==
dotenv@^8.0.0, dotenv@^8.6.0:
version "8.6.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"