Merge pull request #1996 from SkynetLabs/dashboard-v2-copy-changes
Dashboard v2 - addressing ops team feedback + DX improvements
This commit is contained in:
commit
162e6de77a
|
@ -11,15 +11,20 @@ This is a Gatsby application. To run it locally, all you need is:
|
||||||
|
|
||||||
## Accessing remote APIs
|
## 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:
|
||||||
|
|
||||||
```
|
```env
|
||||||
127.0.0.1 local.skynetpro.net
|
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.
|
> It's recommended to keep the 2LD the same, so any cookies dispatched by the API work without issues.
|
||||||
If you're on macOS, you may need to `sudo` the command to successfully bind to port `443`.
|
|
||||||
|
|
||||||
> **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.
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
|
require("dotenv").config({
|
||||||
|
path: `.env.${process.env.NODE_ENV}`,
|
||||||
|
});
|
||||||
|
|
||||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||||
|
|
||||||
|
const { GATSBY_PORTAL_DOMAIN } = process.env;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
siteMetadata: {
|
siteMetadata: {
|
||||||
title: "Skynet Account",
|
title: `Account Dashboard`,
|
||||||
siteUrl: `https://account.${process.env.GATSBY_PORTAL_DOMAIN}/`,
|
siteUrl: `https://account.${GATSBY_PORTAL_DOMAIN}`,
|
||||||
},
|
},
|
||||||
trailingSlash: "never",
|
trailingSlash: "never",
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -24,13 +30,27 @@ module.exports = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
developMiddleware: (app) => {
|
developMiddleware: (app) => {
|
||||||
|
// Proxy Accounts service API requests:
|
||||||
app.use(
|
app.use(
|
||||||
"/api/",
|
"/api/",
|
||||||
createProxyMiddleware({
|
createProxyMiddleware({
|
||||||
target: "https://account.skynetpro.net",
|
target: `https://account.${GATSBY_PORTAL_DOMAIN}`,
|
||||||
secure: false, // Do not reject self-signed certificates.
|
secure: false, // Do not reject self-signed certificates.
|
||||||
changeOrigin: true,
|
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": "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"develop": "gatsby develop",
|
"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",
|
"start": "gatsby develop",
|
||||||
"build": "gatsby build",
|
"build": "gatsby build",
|
||||||
"serve": "gatsby serve",
|
"serve": "gatsby serve",
|
||||||
|
@ -60,6 +60,8 @@
|
||||||
"babel-loader": "^8.2.3",
|
"babel-loader": "^8.2.3",
|
||||||
"babel-plugin-preval": "^5.1.0",
|
"babel-plugin-preval": "^5.1.0",
|
||||||
"babel-plugin-styled-components": "^2.0.2",
|
"babel-plugin-styled-components": "^2.0.2",
|
||||||
|
"dotenv": "^16.0.0",
|
||||||
|
"dotenv-cli": "^5.1.0",
|
||||||
"eslint": "^8.9.0",
|
"eslint": "^8.9.0",
|
||||||
"eslint-config-react-app": "^7.0.0",
|
"eslint-config-react-app": "^7.0.0",
|
||||||
"eslint-plugin-storybook": "^0.5.6",
|
"eslint-plugin-storybook": "^0.5.6",
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useCallback, useState } from "react";
|
||||||
|
|
||||||
import { Alert } from "../Alert";
|
import { Alert } from "../Alert";
|
||||||
import { Button } from "../Button";
|
import { Button } from "../Button";
|
||||||
import { AddSkylinkToAPIKeyForm } from "../forms/AddSkylinkToAPIKeyForm";
|
import { AddSkylinkToSponsorKeyForm } from "../forms/AddSkylinkToSponsorKeyForm";
|
||||||
import { CogIcon, TrashIcon } from "../Icons";
|
import { CogIcon, TrashIcon } from "../Icons";
|
||||||
import { Modal } from "../Modal";
|
import { Modal } from "../Modal";
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import { useAPIKeyRemoval } from "./useAPIKeyRemoval";
|
||||||
|
|
||||||
export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
|
export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
|
||||||
const { id, name, createdAt, skylinks } = apiKey;
|
const { id, name, createdAt, skylinks } = apiKey;
|
||||||
const isPublic = apiKey.public === "true";
|
const isSponsorKey = apiKey.public === "true";
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
const onSkylinkListEdited = useCallback(() => {
|
const onSkylinkListEdited = useCallback(() => {
|
||||||
|
@ -53,9 +53,9 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
|
||||||
}, [abortEdit]);
|
}, [abortEdit]);
|
||||||
|
|
||||||
const skylinksNumber = skylinks?.length ?? 0;
|
const skylinksNumber = skylinks?.length ?? 0;
|
||||||
const isNotConfigured = isPublic && skylinksNumber === 0;
|
const isNotConfigured = isSponsorKey && skylinksNumber === 0;
|
||||||
const skylinksPhrasePrefix = skylinksNumber === 0 ? "No" : skylinksNumber;
|
const skylinksPhrasePrefix = skylinksNumber === 0 ? "No" : skylinksNumber;
|
||||||
const skylinksPhrase = `${skylinksPhrasePrefix} ${skylinksNumber === 1 ? "skylink" : "skylinks"} configured`;
|
const skylinksPhrase = `${skylinksPhrasePrefix} ${skylinksNumber === 1 ? "skylink" : "skylinks"} sponsored`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<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="col-span-2 sm:col-span-1 flex items-center">
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<span className={cn("truncate", { "text-palette-300": !name })}>{name || "unnamed key"}</span>
|
<span className={cn("truncate", { "text-palette-300": !name })}>{name || "unnamed key"}</span>
|
||||||
<button
|
{isSponsorKey && (
|
||||||
onClick={promptEdit}
|
<button
|
||||||
className={cn("text-xs hover:underline decoration-dotted", {
|
onClick={promptEdit}
|
||||||
"text-error": isNotConfigured,
|
className={cn("text-xs hover:underline decoration-dotted", {
|
||||||
"text-palette-400": !isNotConfigured,
|
"text-error": isNotConfigured,
|
||||||
})}
|
"text-palette-400": !isNotConfigured,
|
||||||
>
|
})}
|
||||||
{skylinksPhrase}
|
>
|
||||||
</button>
|
{skylinksPhrase}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="col-span-2 my-4 border-t border-t-palette-200/50 sm:hidden" />
|
<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="text-palette-400">{dayjs(createdAt).format("MMM DD, YYYY")}</span>
|
||||||
<span className="flex items-center justify-end">
|
<span className="flex items-center justify-end">
|
||||||
{isPublic && (
|
{isSponsorKey && (
|
||||||
<button
|
<button
|
||||||
title="Add or remove skylinks"
|
title="Add or remove skylinks"
|
||||||
aria-label="Add or remove skylinks"
|
aria-label="Add or remove skylinks"
|
||||||
|
@ -119,7 +121,7 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
|
||||||
)}
|
)}
|
||||||
{editInitiated && (
|
{editInitiated && (
|
||||||
<Modal onClose={closeEditModal} className="flex flex-col gap-4 text-center sm:px-8 sm:py-6">
|
<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 ? (
|
{skylinks?.length > 0 ? (
|
||||||
<ul className="text-xs flex flex-col gap-2">
|
<ul className="text-xs flex flex-col gap-2">
|
||||||
{skylinks.map((skylink) => (
|
{skylinks.map((skylink) => (
|
||||||
|
@ -143,7 +145,7 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
|
||||||
|
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{error && <Alert $variant="error">{error}</Alert>}
|
{error && <Alert $variant="error">{error}</Alert>}
|
||||||
<AddSkylinkToAPIKeyForm addSkylink={addSkylink} />
|
<AddSkylinkToSponsorKeyForm addSkylink={addSkylink} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 justify-center mt-4">
|
<div className="flex gap-4 justify-center mt-4">
|
||||||
<Button onClick={closeEditModal}>Close</Button>
|
<Button onClick={closeEditModal}>Close</Button>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useUser } from "../../contexts/user";
|
import { useUser } from "../../contexts/user";
|
||||||
import { SimpleUploadIcon } from "../Icons";
|
// import { SimpleUploadIcon } from "../Icons";
|
||||||
|
|
||||||
const AVATAR_PLACEHOLDER = "/images/avatar-placeholder.svg";
|
const AVATAR_PLACEHOLDER = "/images/avatar-placeholder.svg";
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ export const AvatarUploader = (props) => {
|
||||||
>
|
>
|
||||||
<img src={imageUrl} className="w-[160px]" alt="" />
|
<img src={imageUrl} className="w-[160px]" alt="" />
|
||||||
</div>
|
</div>
|
||||||
|
{/* TODO: uncomment when avatar uploads work
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-4 hover:underline decoration-1 decoration-dashed underline-offset-2 decoration-gray-400"
|
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
|
<SimpleUploadIcon size={20} className="shrink-0" /> Upload profile picture
|
||||||
</button>
|
</button>
|
||||||
{/* TODO: actual uploading */}
|
|
||||||
</div>
|
</div>
|
||||||
|
*/}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
import prettyBytes from "pretty-bytes";
|
||||||
|
|
||||||
import { useUser } from "../../contexts/user";
|
import { useUser } from "../../contexts/user";
|
||||||
import useActivePlan from "../../hooks/useActivePlan";
|
import useActivePlan from "../../hooks/useActivePlan";
|
||||||
|
@ -28,17 +29,20 @@ const CurrentPlan = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col h-full">
|
||||||
<h4>{activePlan.name}</h4>
|
<h4>{activePlan.name}</h4>
|
||||||
<div className="text-palette-400">
|
<div className="text-palette-400 justify-between flex flex-col grow">
|
||||||
{activePlan.price === 0 && <p>100GB without paying a dime! 🎉</p>}
|
{activePlan.price === 0 && activePlan.limits && (
|
||||||
|
<p>{prettyBytes(activePlan.limits.storageLimit, { binary: true })} without paying a dime! 🎉</p>
|
||||||
|
)}
|
||||||
{activePlan.price !== 0 &&
|
{activePlan.price !== 0 &&
|
||||||
(user.subscriptionCancelAtPeriodEnd ? (
|
(user.subscriptionCancelAtPeriodEnd ? (
|
||||||
<p>Your subscription expires {dayjs(user.subscribedUntil).fromNow()}</p>
|
<p>Your subscription expires {dayjs(user.subscribedUntil).fromNow()}</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="first-letter:uppercase">{dayjs(user.subscribedUntil).fromNow(true)} until the next payment</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} />
|
<SuggestedPlan plans={plans} activePlan={activePlan} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,7 +44,7 @@ const useUsageData = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const size = (bytes) => {
|
const size = (bytes) => {
|
||||||
const text = fileSize(bytes ?? 0, { maximumFractionDigits: 0 });
|
const text = fileSize(bytes ?? 0, { maximumFractionDigits: 0, binary: true });
|
||||||
const [value, unit] = text.split(" ");
|
const [value, unit] = text.split(" ");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { DATE_FORMAT } from "../../lib/config";
|
||||||
|
|
||||||
const parseFileName = (fileName) => {
|
const parseFileName = (fileName) => {
|
||||||
const lastDotIndex = Math.max(0, fileName.lastIndexOf(".")) || Infinity;
|
const lastDotIndex = Math.max(0, fileName.lastIndexOf(".")) || Infinity;
|
||||||
|
@ -10,7 +11,7 @@ const parseFileName = (fileName) => {
|
||||||
|
|
||||||
const formatItem = ({ size, name: rawFileName, uploadedOn, downloadedOn, ...rest }) => {
|
const formatItem = ({ size, name: rawFileName, uploadedOn, downloadedOn, ...rest }) => {
|
||||||
const [name, type] = parseFileName(rawFileName);
|
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 {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { PageContainer } from "../PageContainer";
|
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 = () => (
|
export const Footer = () => (
|
||||||
<PageContainer className="font-content text-palette-300 py-4">
|
<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>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,5 +2,5 @@ import { Link } from "gatsby";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
export default styled(Link).attrs({
|
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",
|
||||||
})``;
|
})``;
|
||||||
|
|
|
@ -3,13 +3,13 @@ import useSWR from "swr";
|
||||||
|
|
||||||
import { Table, TableBody, TableCell, TableRow } from "../Table";
|
import { Table, TableBody, TableCell, TableRow } from "../Table";
|
||||||
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||||
|
import useFormattedFilesData from "../FileList/useFormattedFilesData";
|
||||||
|
|
||||||
import { ViewAllLink } from "./ViewAllLink";
|
import { ViewAllLink } from "./ViewAllLink";
|
||||||
import useFormattedActivityData from "./useFormattedActivityData";
|
|
||||||
|
|
||||||
export default function ActivityTable({ type }) {
|
export default function ActivityTable({ type }) {
|
||||||
const { data, error } = useSWR(`user/${type}?pageSize=3`);
|
const { data, error } = useSWR(`user/${type}?pageSize=3`);
|
||||||
const items = useFormattedActivityData(data?.items || []);
|
const items = useFormattedFilesData(data?.items || []);
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,23 +1,13 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { Panel } from "../Panel";
|
import { Panel } from "../Panel";
|
||||||
import { Tab, TabPanel, Tabs } from "../Tabs";
|
|
||||||
|
|
||||||
import ActivityTable from "./ActivityTable";
|
import ActivityTable from "./ActivityTable";
|
||||||
|
|
||||||
export default function LatestActivity() {
|
export default function LatestActivity() {
|
||||||
return (
|
return (
|
||||||
<Panel title="Latest activity">
|
<Panel title="Latest uploads">
|
||||||
<Tabs>
|
<ActivityTable type="uploads" />
|
||||||
<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>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./Tooltip";
|
|
@ -118,7 +118,7 @@ const Uploader = ({ mode }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{uploads.length > 0 && (
|
{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) => (
|
{uploads.map((upload) => (
|
||||||
<UploaderItem key={upload.id} onUploadStateChange={onUploadStateChange} upload={upload} />
|
<UploaderItem key={upload.id} onUploadStateChange={onUploadStateChange} upload={upload} />
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -34,8 +34,9 @@ export const AccountRemovalForm = ({ abort, onSuccess }) => {
|
||||||
<Form className="flex flex-col gap-4">
|
<Form className="flex flex-col gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h4>Delete account</h4>
|
<h4>Delete account</h4>
|
||||||
|
<p>This will completely delete your account.</p>
|
||||||
<p>
|
<p>
|
||||||
This will completely delete your account. <strong>This process can't be undone.</strong>
|
<strong>This process cannot be undone.</strong>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import * as Yup from "yup";
|
||||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { Formik, Form } from "formik";
|
import { Formik, Form } from "formik";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
import accountsService from "../../services/accountsService";
|
import accountsService from "../../services/accountsService";
|
||||||
|
|
||||||
|
@ -9,7 +10,6 @@ import { Alert } from "../Alert";
|
||||||
import { Button } from "../Button";
|
import { Button } from "../Button";
|
||||||
import { CopyButton } from "../CopyButton";
|
import { CopyButton } from "../CopyButton";
|
||||||
import { TextField } from "../Form/TextField";
|
import { TextField } from "../Form/TextField";
|
||||||
import { CircledProgressIcon, PlusIcon } from "../Icons";
|
|
||||||
|
|
||||||
const newAPIKeySchema = Yup.object().shape({
|
const newAPIKeySchema = Yup.object().shape({
|
||||||
name: Yup.string(),
|
name: Yup.string(),
|
||||||
|
@ -22,7 +22,7 @@ const State = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const APIKeyType = {
|
export const APIKeyType = {
|
||||||
Public: "public",
|
Sponsor: "sponsor",
|
||||||
General: "general",
|
General: "general",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -37,10 +37,10 @@ export const AddAPIKeyForm = forwardRef(({ onSuccess, type }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="flex flex-col gap-4">
|
<div ref={ref} className="flex flex-col gap-4">
|
||||||
{state === State.Success && (
|
{state === State.Success && (
|
||||||
<Alert $variant="success" className="text-center">
|
<Alert $variant="success">
|
||||||
<strong>Success!</strong>
|
<strong>Success!</strong>
|
||||||
<p>Please copy your new API key below. We'll never show it again!</p>
|
<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">
|
<code className="p-2 rounded border border-palette-200 text-xs selection:bg-primary/30 truncate">
|
||||||
{generatedKey}
|
{generatedKey}
|
||||||
</code>
|
</code>
|
||||||
|
@ -62,8 +62,8 @@ export const AddAPIKeyForm = forwardRef(({ onSuccess, type }, ref) => {
|
||||||
.post("user/apikeys", {
|
.post("user/apikeys", {
|
||||||
json: {
|
json: {
|
||||||
name,
|
name,
|
||||||
public: type === APIKeyType.Public ? "true" : "false",
|
public: type === APIKeyType.Sponsor ? "true" : "false",
|
||||||
skylinks: type === APIKeyType.Public ? [] : null,
|
skylinks: type === APIKeyType.Sponsor ? [] : null,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
|
@ -78,26 +78,20 @@ export const AddAPIKeyForm = forwardRef(({ onSuccess, type }, ref) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ errors, touched, isSubmitting }) => (
|
{({ errors, touched, isSubmitting }) => (
|
||||||
<Form className="grid grid-cols-[1fr_min-content] w-full gap-y-2 gap-x-4 items-start">
|
<Form className="flex flex-col gap-4">
|
||||||
<div className="flex items-center">
|
<TextField
|
||||||
<TextField
|
type="text"
|
||||||
type="text"
|
id="name"
|
||||||
id="name"
|
name="name"
|
||||||
name="name"
|
label="New API Key Label"
|
||||||
label="New API Key Name"
|
placeholder="my_applications_statistics"
|
||||||
placeholder="my_applications_statistics"
|
error={errors.name}
|
||||||
error={errors.name}
|
touched={touched.name}
|
||||||
touched={touched.name}
|
/>
|
||||||
/>
|
<div className="flex justify-center">
|
||||||
</div>
|
<Button type="submit" disabled={isSubmitting} className={cn({ "cursor-wait": isSubmitting })}>
|
||||||
<div className="flex mt-5 justify-center">
|
{isSubmitting ? "Generating your API key..." : "Generate your API key"}
|
||||||
{isSubmitting ? (
|
</Button>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
|
@ -110,5 +104,5 @@ AddAPIKeyForm.displayName = "AddAPIKeyForm";
|
||||||
|
|
||||||
AddAPIKeyForm.propTypes = {
|
AddAPIKeyForm.propTypes = {
|
||||||
onSuccess: PropTypes.func.isRequired,
|
onSuccess: PropTypes.func.isRequired,
|
||||||
type: PropTypes.oneOf([APIKeyType.Public, APIKeyType.General]).isRequired,
|
type: PropTypes.oneOf([APIKeyType.Sponsor, APIKeyType.General]).isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,7 +19,7 @@ const newSkylinkSchema = Yup.object().shape({
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AddSkylinkToAPIKeyForm = ({ addSkylink }) => (
|
export const AddSkylinkToSponsorKeyForm = ({ addSkylink }) => (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
skylink: "",
|
skylink: "",
|
||||||
|
@ -58,6 +58,6 @@ export const AddSkylinkToAPIKeyForm = ({ addSkylink }) => (
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
|
|
||||||
AddSkylinkToAPIKeyForm.propTypes = {
|
AddSkylinkToSponsorKeyForm.propTypes = {
|
||||||
addSkylink: PropTypes.func.isRequired,
|
addSkylink: PropTypes.func.isRequired,
|
||||||
};
|
};
|
|
@ -25,7 +25,7 @@ const skylinkValidator = (optional) => (value) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const newPublicAPIKeySchema = Yup.object().shape({
|
const newSponsorKeySchema = Yup.object().shape({
|
||||||
name: Yup.string(),
|
name: Yup.string(),
|
||||||
skylinks: Yup.array().of(Yup.string().test("skylink", "Provide a valid Skylink", skylinkValidator(false))),
|
skylinks: Yup.array().of(Yup.string().test("skylink", "Provide a valid Skylink", skylinkValidator(false))),
|
||||||
nextSkylink: Yup.string().when("skylinks", {
|
nextSkylink: Yup.string().when("skylinks", {
|
||||||
|
@ -41,7 +41,7 @@ const State = {
|
||||||
Failure: "FAILURE",
|
Failure: "FAILURE",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
|
export const AddSponsorKeyForm = forwardRef(({ onSuccess }, ref) => {
|
||||||
const [state, setState] = useState(State.Pure);
|
const [state, setState] = useState(State.Pure);
|
||||||
const [generatedKey, setGeneratedKey] = useState(null);
|
const [generatedKey, setGeneratedKey] = useState(null);
|
||||||
|
|
||||||
|
@ -52,10 +52,10 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="flex flex-col gap-4">
|
<div ref={ref} className="flex flex-col gap-4">
|
||||||
{state === State.Success && (
|
{state === State.Success && (
|
||||||
<Alert $variant="success" className="text-center">
|
<Alert $variant="success">
|
||||||
<strong>Success!</strong>
|
<strong>Success!</strong>
|
||||||
<p>Please copy your new API key below. We'll never show it again!</p>
|
<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">
|
<code className="p-2 rounded border border-palette-200 text-xs selection:bg-primary/30 truncate">
|
||||||
{generatedKey}
|
{generatedKey}
|
||||||
</code>
|
</code>
|
||||||
|
@ -72,7 +72,7 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
|
||||||
skylinks: [],
|
skylinks: [],
|
||||||
nextSkylink: "",
|
nextSkylink: "",
|
||||||
}}
|
}}
|
||||||
validationSchema={newPublicAPIKeySchema}
|
validationSchema={newSponsorKeySchema}
|
||||||
onSubmit={async ({ name, skylinks, nextSkylink }, { resetForm }) => {
|
onSubmit={async ({ name, skylinks, nextSkylink }, { resetForm }) => {
|
||||||
try {
|
try {
|
||||||
const { key } = await accountsService
|
const { key } = await accountsService
|
||||||
|
@ -101,14 +101,14 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
|
||||||
type="text"
|
type="text"
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
label="Public API Key Name"
|
label="Sponsor API Key Name"
|
||||||
placeholder="my_applications_statistics"
|
placeholder="my_applications_statistics"
|
||||||
error={errors.name}
|
error={errors.name}
|
||||||
touched={touched.name}
|
touched={touched.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<FieldArray
|
||||||
name="skylinks"
|
name="skylinks"
|
||||||
render={({ push, remove }) => {
|
render={({ push, remove }) => {
|
||||||
|
@ -182,7 +182,7 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
|
||||||
className={cn("px-2.5", { "cursor-wait": isSubmitting })}
|
className={cn("px-2.5", { "cursor-wait": isSubmitting })}
|
||||||
disabled={!isValid || isSubmitting}
|
disabled={!isValid || isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Generating" : "Generate"} your public key
|
{isSubmitting ? "Generating your sponsor key..." : "Generate your sponsor key"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</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,
|
onSuccess: PropTypes.func.isRequired,
|
||||||
};
|
};
|
|
@ -32,14 +32,16 @@ export const SignUpForm = ({ onSuccess, onFailure }) => (
|
||||||
validationSchema={registrationSchema}
|
validationSchema={registrationSchema}
|
||||||
onSubmit={async ({ email, password }, { setErrors }) => {
|
onSubmit={async ({ email, password }, { setErrors }) => {
|
||||||
try {
|
try {
|
||||||
await accountsService.post("user", {
|
const user = await accountsService
|
||||||
json: {
|
.post("user", {
|
||||||
email,
|
json: {
|
||||||
password,
|
email,
|
||||||
},
|
password,
|
||||||
});
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
onSuccess();
|
onSuccess(user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
let isFormErrorSet = false;
|
let isFormErrorSet = false;
|
||||||
|
|
||||||
|
|
|
@ -16,8 +16,8 @@ const Sidebar = () => (
|
||||||
<SidebarLink activeClassName="!border-l-primary" to="/settings/export">
|
<SidebarLink activeClassName="!border-l-primary" to="/settings/export">
|
||||||
Export
|
Export
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SidebarLink activeClassName="!border-l-primary" to="/settings/api-keys">
|
<SidebarLink activeClassName="!border-l-primary" to="/settings/developer-settings">
|
||||||
API Keys
|
Developer settings
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export const DATE_FORMAT = "MMM D, YYYY HH:MM";
|
|
@ -1,5 +1,4 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { navigate } from "gatsby";
|
|
||||||
import bytes from "pretty-bytes";
|
import bytes from "pretty-bytes";
|
||||||
|
|
||||||
import AuthLayout from "../../layouts/AuthLayout";
|
import AuthLayout from "../../layouts/AuthLayout";
|
||||||
|
@ -10,6 +9,7 @@ import { SignUpForm } from "../../components/forms/SignUpForm";
|
||||||
import { usePortalSettings } from "../../contexts/portal-settings";
|
import { usePortalSettings } from "../../contexts/portal-settings";
|
||||||
import { PlansProvider, usePlans } from "../../contexts/plans";
|
import { PlansProvider, usePlans } from "../../contexts/plans";
|
||||||
import { Metadata } from "../../components/Metadata";
|
import { Metadata } from "../../components/Metadata";
|
||||||
|
import { useUser } from "../../contexts/user";
|
||||||
|
|
||||||
const FreePortalHeader = () => {
|
const FreePortalHeader = () => {
|
||||||
const { plans } = usePlans();
|
const { plans } = usePlans();
|
||||||
|
@ -47,14 +47,14 @@ const State = {
|
||||||
const SignUpPage = () => {
|
const SignUpPage = () => {
|
||||||
const [state, setState] = useState(State.Pure);
|
const [state, setState] = useState(State.Pure);
|
||||||
const { settings } = usePortalSettings();
|
const { settings } = usePortalSettings();
|
||||||
|
const { mutate: refreshUserState } = useUser();
|
||||||
|
|
||||||
useEffect(() => {
|
const onUserCreated = useCallback(
|
||||||
if (state === State.Success) {
|
(newUser) => {
|
||||||
const timer = setTimeout(() => navigate(settings.isSubscriptionRequired ? "/upgrade" : "/"), 3000);
|
refreshUserState(newUser);
|
||||||
|
},
|
||||||
return () => clearTimeout(timer);
|
[refreshUserState]
|
||||||
}
|
);
|
||||||
}, [state, settings.isSubscriptionRequired]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PlansProvider>
|
<PlansProvider>
|
||||||
|
@ -70,7 +70,7 @@ const SignUpPage = () => {
|
||||||
|
|
||||||
{state !== State.Success && (
|
{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">
|
<p className="text-sm text-center mt-8">
|
||||||
Already have an account? <HighlightedLink to="/auth/login">Sign in</HighlightedLink>
|
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 && (
|
{state === State.Failure && (
|
||||||
<p className="text-error text-center">Something went wrong, please try again later.</p>
|
<p className="text-error text-center">Something went wrong, please try again later.</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,32 +1,19 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useSearchParam } from "react-use";
|
|
||||||
|
|
||||||
import DashboardLayout from "../layouts/DashboardLayout";
|
import DashboardLayout from "../layouts/DashboardLayout";
|
||||||
|
|
||||||
import { Panel } from "../components/Panel";
|
import { Panel } from "../components/Panel";
|
||||||
import { Tab, TabPanel, Tabs } from "../components/Tabs";
|
|
||||||
import { Metadata } from "../components/Metadata";
|
import { Metadata } from "../components/Metadata";
|
||||||
import FileList from "../components/FileList/FileList";
|
import FileList from "../components/FileList/FileList";
|
||||||
|
|
||||||
const FilesPage = () => {
|
const FilesPage = () => {
|
||||||
const defaultTab = useSearchParam("tab");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Metadata>
|
<Metadata>
|
||||||
<title>My Files</title>
|
<title>Files</title>
|
||||||
</Metadata>
|
</Metadata>
|
||||||
<Panel title="Files">
|
<Panel title="Files">
|
||||||
<Tabs defaultTab={defaultTab || "uploads"}>
|
<FileList type="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>
|
|
||||||
</Panel>
|
</Panel>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,13 +6,14 @@ import UserSettingsLayout from "../../layouts/UserSettingsLayout";
|
||||||
import { AddAPIKeyForm, APIKeyType } from "../../components/forms/AddAPIKeyForm";
|
import { AddAPIKeyForm, APIKeyType } from "../../components/forms/AddAPIKeyForm";
|
||||||
import { APIKeyList } from "../../components/APIKeyList/APIKeyList";
|
import { APIKeyList } from "../../components/APIKeyList/APIKeyList";
|
||||||
import { Alert } from "../../components/Alert";
|
import { Alert } from "../../components/Alert";
|
||||||
import { AddPublicAPIKeyForm } from "../../components/forms/AddPublicAPIKeyForm";
|
import { AddSponsorKeyForm } from "../../components/forms/AddSponsorKeyForm";
|
||||||
import { Metadata } from "../../components/Metadata";
|
import { Metadata } from "../../components/Metadata";
|
||||||
|
import HighlightedLink from "../../components/HighlightedLink";
|
||||||
|
|
||||||
const APIKeysPage = () => {
|
const DeveloperSettingsPage = () => {
|
||||||
const { data: apiKeys = [], mutate: reloadKeys, error } = useSWR("user/apikeys");
|
const { data: allKeys = [], mutate: reloadKeys, error } = useSWR("user/apikeys");
|
||||||
const generalKeys = apiKeys.filter(({ public: isPublic }) => isPublic === "false");
|
const apiKeys = allKeys.filter(({ public: isPublic }) => isPublic === "false");
|
||||||
const publicKeys = apiKeys.filter(({ public: isPublic }) => isPublic === "true");
|
const sponsorKeys = allKeys.filter(({ public: isPublic }) => isPublic === "true");
|
||||||
|
|
||||||
const publicFormRef = useRef();
|
const publicFormRef = useRef();
|
||||||
const generalFormRef = useRef();
|
const generalFormRef = useRef();
|
||||||
|
@ -31,53 +32,57 @@ const APIKeysPage = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Metadata>
|
<Metadata>
|
||||||
<title>API Keys</title>
|
<title>Developer settings</title>
|
||||||
</Metadata>
|
</Metadata>
|
||||||
<div className="flex flex-col xl:flex-row">
|
<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>
|
<div>
|
||||||
<h4>API Keys</h4>
|
<h4>Developer settings</h4>
|
||||||
<p className="leading-relaxed">There are two types of API keys that you can generate for your account.</p>
|
<p>API keys allow developers and applications to extend the functionality of your portal account.</p>
|
||||||
<p>Make sure to use the appropriate type.</p>
|
<p>Skynet uses two types of API keys, explained below.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<section className="flex flex-col gap-2">
|
<section className="flex flex-col gap-2">
|
||||||
<h5>Public keys</h5>
|
<h5>Sponsor keys</h5>
|
||||||
<p className="text-palette-500">
|
<div className="text-palette-500"></div>
|
||||||
Public keys provide read access to a selected list of skylinks. You can share them publicly.
|
<p>
|
||||||
|
Sponsor keys allow users without an account on this portal to download skylinks covered by the API key.
|
||||||
</p>
|
</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">
|
<div className="mt-4">
|
||||||
<AddPublicAPIKeyForm ref={publicFormRef} onSuccess={refreshState} />
|
<AddSponsorKeyForm ref={publicFormRef} onSuccess={refreshState} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error ? (
|
{error ? (
|
||||||
<Alert $variant="error" className="mt-4">
|
<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>
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
{publicKeys?.length > 0 ? (
|
{sponsorKeys?.length > 0 ? (
|
||||||
<APIKeyList title="Your public keys" keys={publicKeys} reloadKeys={() => refreshState(true)} />
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<section className="flex flex-col gap-2">
|
<section className="flex flex-col gap-2">
|
||||||
<h5>General keys</h5>
|
<h5>API keys</h5>
|
||||||
<p className="text-palette-500">
|
<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>
|
||||||
<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">
|
<div className="mt-4">
|
||||||
<AddAPIKeyForm ref={generalFormRef} onSuccess={refreshState} type={APIKeyType.General} />
|
<AddAPIKeyForm ref={generalFormRef} onSuccess={refreshState} type={APIKeyType.General} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -88,10 +93,10 @@ const APIKeysPage = () => {
|
||||||
</Alert>
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
{generalKeys?.length > 0 ? (
|
{apiKeys?.length > 0 ? (
|
||||||
<APIKeyList title="Your general keys" keys={generalKeys} reloadKeys={() => refreshState(true)} />
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -105,6 +110,6 @@ const APIKeysPage = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
APIKeysPage.Layout = UserSettingsLayout;
|
DeveloperSettingsPage.Layout = UserSettingsLayout;
|
||||||
|
|
||||||
export default APIKeysPage;
|
export default DeveloperSettingsPage;
|
|
@ -38,8 +38,8 @@ const ExportPage = () => {
|
||||||
<section>
|
<section>
|
||||||
<h4>Export</h4>
|
<h4>Export</h4>
|
||||||
<p>
|
<p>
|
||||||
Et quidem exercitus quid ex eo delectu rerum, quem modo ista sis aequitate. Probabo, inquit, modo dixi,
|
Select the items you want to export. You can use this data to migrate your account to another Skynet
|
||||||
constituto.
|
portal.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
|
@ -8,6 +8,10 @@ import { Modal } from "../../components/Modal/Modal";
|
||||||
import { AccountRemovalForm } from "../../components/forms/AccountRemovalForm";
|
import { AccountRemovalForm } from "../../components/forms/AccountRemovalForm";
|
||||||
import { Alert } from "../../components/Alert";
|
import { Alert } from "../../components/Alert";
|
||||||
import { Metadata } from "../../components/Metadata";
|
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 = {
|
const State = {
|
||||||
Pure: "PURE",
|
Pure: "PURE",
|
||||||
|
@ -19,10 +23,16 @@ const AccountPage = () => {
|
||||||
const { user, mutate: reloadUser } = useUser();
|
const { user, mutate: reloadUser } = useUser();
|
||||||
const [state, setState] = useState(State.Pure);
|
const [state, setState] = useState(State.Pure);
|
||||||
const [removalInitiated, setRemovalInitiated] = useState(false);
|
const [removalInitiated, setRemovalInitiated] = useState(false);
|
||||||
|
const isLargeScreen = useMedia(`(min-width: ${theme.screens.xl})`);
|
||||||
|
|
||||||
const prompt = () => setRemovalInitiated(true);
|
const prompt = () => setRemovalInitiated(true);
|
||||||
const abort = () => setRemovalInitiated(false);
|
const abort = () => setRemovalInitiated(false);
|
||||||
|
|
||||||
|
const onAccountRemoved = useCallback(async () => {
|
||||||
|
await reloadUser(null);
|
||||||
|
await navigate("/auth/login");
|
||||||
|
}, [reloadUser]);
|
||||||
|
|
||||||
const onSettingsUpdated = useCallback(
|
const onSettingsUpdated = useCallback(
|
||||||
async (updatedState) => {
|
async (updatedState) => {
|
||||||
try {
|
try {
|
||||||
|
@ -45,14 +55,7 @@ const AccountPage = () => {
|
||||||
</Metadata>
|
</Metadata>
|
||||||
<div className="flex flex-col xl:flex-row">
|
<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]">
|
||||||
<section>
|
<h4>Account</h4>
|
||||||
<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 />
|
|
||||||
<section className="flex flex-col gap-8">
|
<section className="flex flex-col gap-8">
|
||||||
{state === State.Failure && (
|
{state === State.Failure && (
|
||||||
<Alert $variant="error">There was an error processing your request. Please try again later.</Alert>
|
<Alert $variant="error">There was an error processing your request. Please try again later.</Alert>
|
||||||
|
@ -63,7 +66,23 @@ const AccountPage = () => {
|
||||||
<hr />
|
<hr />
|
||||||
<section>
|
<section>
|
||||||
<h6 className="text-palette-400">Delete account</h6>
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={prompt}
|
onClick={prompt}
|
||||||
|
@ -73,9 +92,12 @@ const AccountPage = () => {
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex w-full justify-start xl:justify-end">
|
||||||
|
{isLargeScreen && <AvatarUploader className="flex flex-col gap-4" />}
|
||||||
|
</div>
|
||||||
{removalInitiated && (
|
{removalInitiated && (
|
||||||
<Modal onClose={abort} className="text-center">
|
<Modal onClose={abort} className="text-center">
|
||||||
<AccountRemovalForm abort={abort} onSuccess={() => navigate("/auth/login")} />
|
<AccountRemovalForm abort={abort} onSuccess={onAccountRemoved} />
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,18 +18,13 @@ const NotificationsPage = () => {
|
||||||
<section>
|
<section>
|
||||||
{/* TODO: saves on change */}
|
{/* TODO: saves on change */}
|
||||||
<Switch onChange={console.info.bind(console)} labelClassName="!items-start flex-col md:flex-row">
|
<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>
|
</Switch>
|
||||||
</section>
|
</section>
|
||||||
<hr />
|
<hr />
|
||||||
<section>
|
<section>
|
||||||
<h6 className="text-palette-300">Statistics</h6>
|
<h6 className="text-palette-300">Statistics</h6>
|
||||||
{/* TODO: proper content :) */}
|
<p>Check below to be notified by email when your usage approaches your plan's limits.</p>
|
||||||
<p>
|
|
||||||
Si sine causa, nollem me tamen laudandis maioribus meis corrupisti nec in malis. Si sine causa, mox
|
|
||||||
videro.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ul className="mt-7 flex flex-col gap-2">
|
<ul className="mt-7 flex flex-col gap-2">
|
||||||
<li>
|
<li>
|
||||||
{/* TODO: saves on change */}
|
{/* TODO: saves on change */}
|
||||||
|
@ -37,7 +32,7 @@ const NotificationsPage = () => {
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{/* TODO: saves on change */}
|
{/* TODO: saves on change */}
|
||||||
<Switch onChange={console.info.bind(console)}>File limit</Switch>
|
<Switch onChange={console.info.bind(console)}>Files limit</Switch>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import { SkynetClient } from "skynet-js";
|
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}`);
|
||||||
|
|
|
@ -6735,11 +6735,31 @@ dot-prop@^5.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-obj "^2.0.0"
|
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:
|
dotenv-expand@^5.1.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
|
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
|
||||||
integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
|
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:
|
dotenv@^8.0.0, dotenv@^8.6.0:
|
||||||
version "8.6.0"
|
version "8.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
|
||||||
|
|
Reference in New Issue