diff --git a/packages/dashboard-v2/src/components/Modal/index.js b/packages/dashboard-v2/src/components/Modal/index.js
index 28d34710..00ce01f6 100644
--- a/packages/dashboard-v2/src/components/Modal/index.js
+++ b/packages/dashboard-v2/src/components/Modal/index.js
@@ -1 +1,2 @@
export * from "./ModalPortal";
+export * from "./Modal";
diff --git a/packages/dashboard-v2/src/components/Tooltip/Tooltip.js b/packages/dashboard-v2/src/components/Tooltip/Tooltip.js
new file mode 100644
index 00000000..d7bfde39
--- /dev/null
+++ b/packages/dashboard-v2/src/components/Tooltip/Tooltip.js
@@ -0,0 +1,29 @@
+import React, { useState } from "react";
+import styled, { keyframes } from "styled-components";
+
+const fadeIn = keyframes`
+ 0% { opacity: 0; }
+ 100% { opacity: 1; }
+`;
+
+const Popper = styled.div.attrs({
+ className: `absolute left-full top-1/2 z-10 px-2 py-1 text-xs
+ bg-black/90 text-white rounded`,
+})`
+ transform: translateY(-50%);
+ animation: ${fadeIn} 0.2s ease-in-out;
+`;
+
+export const Tooltip = ({ message, children, className }) => {
+ const [visible, setVisible] = useState(false);
+
+ const show = () => setVisible(true);
+ const hide = () => setVisible(false);
+
+ return (
+
+ {children}
+ {visible && {message}}
+
+ );
+};
diff --git a/packages/dashboard-v2/src/components/forms/AddAPIKeyForm.js b/packages/dashboard-v2/src/components/forms/AddAPIKeyForm.js
new file mode 100644
index 00000000..703d88a0
--- /dev/null
+++ b/packages/dashboard-v2/src/components/forms/AddAPIKeyForm.js
@@ -0,0 +1,114 @@
+import * as Yup from "yup";
+import { forwardRef, useImperativeHandle, useState } from "react";
+import PropTypes from "prop-types";
+import { Formik, Form } from "formik";
+
+import accountsService from "../../services/accountsService";
+
+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(),
+});
+
+const State = {
+ Pure: "PURE",
+ Success: "SUCCESS",
+ Failure: "FAILURE",
+};
+
+export const APIKeyType = {
+ Public: "public",
+ General: "general",
+};
+
+export const AddAPIKeyForm = forwardRef(({ onSuccess, type }, ref) => {
+ const [state, setState] = useState(State.Pure);
+ const [generatedKey, setGeneratedKey] = useState(null);
+
+ useImperativeHandle(ref, () => ({
+ reset: () => setState(State.Pure),
+ }));
+
+ return (
+
+ {state === State.Success && (
+
+ Success!
+ Please copy your new API key below. We'll never show it again!
+
+
+ {generatedKey}
+
+
+
+
+ )}
+ {state === State.Failure && (
+
We were not able to generate a new key. Please try again later.
+ )}
+
{
+ try {
+ const { key } = await accountsService
+ .post("user/apikeys", {
+ json: {
+ name,
+ public: type === APIKeyType.Public ? "true" : "false",
+ skylinks: type === APIKeyType.Public ? [] : null,
+ },
+ })
+ .json();
+
+ resetForm();
+ setGeneratedKey(key);
+ setState(State.Success);
+ onSuccess();
+ } catch {
+ setState(State.Failure);
+ }
+ }}
+ >
+ {({ errors, touched, isSubmitting }) => (
+
+ )}
+
+
+ );
+});
+
+AddAPIKeyForm.displayName = "AddAPIKeyForm";
+
+AddAPIKeyForm.propTypes = {
+ onSuccess: PropTypes.func.isRequired,
+ type: PropTypes.oneOf([APIKeyType.Public, APIKeyType.General]).isRequired,
+};
diff --git a/packages/dashboard-v2/src/components/forms/AddPublicAPIKeyForm.js b/packages/dashboard-v2/src/components/forms/AddPublicAPIKeyForm.js
new file mode 100644
index 00000000..2184c513
--- /dev/null
+++ b/packages/dashboard-v2/src/components/forms/AddPublicAPIKeyForm.js
@@ -0,0 +1,198 @@
+import * as Yup from "yup";
+import { forwardRef, useImperativeHandle, useState } from "react";
+import PropTypes from "prop-types";
+import { Formik, Form, FieldArray } from "formik";
+import { parseSkylink } from "skynet-js";
+import cn from "classnames";
+
+import accountsService from "../../services/accountsService";
+
+import { Alert } from "../Alert";
+import { Button } from "../Button";
+import { CopyButton } from "../CopyButton";
+import { TextField } from "../Form/TextField";
+import { PlusIcon, TrashIcon } from "../Icons";
+
+const skylinkValidator = (optional) => (value) => {
+ if (!value) {
+ return optional;
+ }
+
+ try {
+ return parseSkylink(value) !== null;
+ } catch {
+ return false;
+ }
+};
+
+const newPublicAPIKeySchema = 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", {
+ is: (skylinks) => skylinks.length === 0,
+ then: (schema) => schema.test("skylink", "Provide a valid Skylink", skylinkValidator(true)),
+ otherwise: (schema) => schema.test("skylink", "Provide a valid Skylink", skylinkValidator(true)),
+ }),
+});
+
+const State = {
+ Pure: "PURE",
+ Success: "SUCCESS",
+ Failure: "FAILURE",
+};
+
+export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
+ const [state, setState] = useState(State.Pure);
+ const [generatedKey, setGeneratedKey] = useState(null);
+
+ useImperativeHandle(ref, () => ({
+ reset: () => setState(State.Pure),
+ }));
+
+ return (
+
+ {state === State.Success && (
+
+ Success!
+ Please copy your new API key below. We'll never show it again!
+
+
+ {generatedKey}
+
+
+
+
+ )}
+ {state === State.Failure && (
+
We were not able to generate a new key. Please try again later.
+ )}
+
{
+ try {
+ const { key } = await accountsService
+ .post("user/apikeys", {
+ json: {
+ name,
+ public: "true",
+ skylinks: [...skylinks, nextSkylink].filter(Boolean).map(parseSkylink),
+ },
+ })
+ .json();
+
+ resetForm();
+ setGeneratedKey(key);
+ setState(State.Success);
+ onSuccess();
+ } catch {
+ setState(State.Failure);
+ }
+ }}
+ >
+ {({ errors, touched, isSubmitting, values, isValid, setFieldValue, setFieldTouched }) => (
+
+ )}
+
+
+ );
+});
+
+AddPublicAPIKeyForm.displayName = "AddAPIKeyForm";
+
+AddPublicAPIKeyForm.propTypes = {
+ onSuccess: PropTypes.func.isRequired,
+};
diff --git a/packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js b/packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js
new file mode 100644
index 00000000..55f27a01
--- /dev/null
+++ b/packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js
@@ -0,0 +1,63 @@
+import * as Yup from "yup";
+import PropTypes from "prop-types";
+import { Formik, Form } from "formik";
+import { parseSkylink } from "skynet-js";
+
+import { Button } from "../Button";
+import { TextField } from "../Form/TextField";
+import { CircledProgressIcon, PlusIcon } from "../Icons";
+
+const newSkylinkSchema = Yup.object().shape({
+ skylink: Yup.string()
+ .required("Skylink is required")
+ .test("skylink", "Provide a valid Skylink", (value) => {
+ try {
+ return parseSkylink(value) !== null;
+ } catch {
+ return false;
+ }
+ }),
+});
+
+export const AddSkylinkToAPIKeyForm = ({ addSkylink }) => (
+
{
+ if (await addSkylink(parseSkylink(skylink))) {
+ resetForm();
+ }
+ }}
+ >
+ {({ errors, touched, isSubmitting }) => (
+
+ )}
+
+);
+
+AddSkylinkToAPIKeyForm.propTypes = {
+ addSkylink: PropTypes.func.isRequired,
+};
diff --git a/packages/dashboard-v2/src/pages/settings/api-keys.js b/packages/dashboard-v2/src/pages/settings/api-keys.js
index 8ed8b1c8..1f6c93c5 100644
--- a/packages/dashboard-v2/src/pages/settings/api-keys.js
+++ b/packages/dashboard-v2/src/pages/settings/api-keys.js
@@ -1,63 +1,100 @@
import useSWR from "swr";
-import dayjs from "dayjs";
+import { useCallback, useRef } from "react";
import UserSettingsLayout from "../../layouts/UserSettingsLayout";
-import { TextInputBasic } from "../../components/TextInputBasic";
-import { Button } from "../../components/Button";
-import { TrashIcon } from "../../components/Icons";
+import { AddAPIKeyForm, APIKeyType } from "../../components/forms/AddAPIKeyForm";
+import { APIKeyList } from "../../components/APIKeyList/APIKeyList";
+import { Alert } from "../../components/Alert";
+import { AddPublicAPIKeyForm } from "../../components/forms/AddPublicAPIKeyForm";
const APIKeysPage = () => {
- const { data: apiKeys } = useSWR("user/apikeys");
+ 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 publicFormRef = useRef();
+ const generalFormRef = useRef();
+
+ const refreshState = useCallback(
+ (resetForms) => {
+ if (resetForms) {
+ publicFormRef.current?.reset();
+ generalFormRef.current?.reset();
+ }
+ reloadKeys();
+ },
+ [reloadKeys]
+ );
return (
<>
-
+
API Keys
-
- At vero eos et caritatem, quae sine metu contineret, saluti prospexit civium, qua. Laudem et dolorem
- aspernari ut ad naturam aut fu.
+
There are two types of API keys that you can generate for your account.
+
Make sure to use the appropriate type.
+
+
+
+
+
+ Public keys
+
+ Public keys provide read access to a selected list of skylinks. You can share them publicly.
+
+
+
+ {error ? (
+
+ An error occurred while loading your API keys. Please try again later.
+
+ ) : (
+
+ {publicKeys?.length > 0 ? (
+
refreshState(true)} />
+ ) : (
+ No public API keys found.
+ )}
+
+ )}
-
-
-
-
-
-
+
+
+ General keys
+
+ These keys provide full access to Accounts service and are equivalent to using a JWT token.
+
+
+ This type of API keys needs to be kept secret and should never be shared with anyone.
+
+
+
+
+ {error ? (
+
+ An error occurred while loading your API keys. Please try again later.
+
+ ) : (
+
+ {generalKeys?.length > 0 ? (
+
refreshState(true)} />
+ ) : (
+ No general API keys found.
+ )}
+
+ )}
- {apiKeys?.length > 0 && (
-
- API Keys
-
- {apiKeys.map(({ id, name, createdAt }) => (
- -
- {name || id}
-
- {dayjs(createdAt).format("MMM DD, YYYY")}
-
-
-
-
- ))}
-
-
- )}
-
-
+
+
>
diff --git a/packages/dashboard-v2/static/images/api-keys.svg b/packages/dashboard-v2/static/images/api-keys.svg
new file mode 100644
index 00000000..35fe3744
--- /dev/null
+++ b/packages/dashboard-v2/static/images/api-keys.svg
@@ -0,0 +1 @@
+
diff --git a/packages/dashboard-v2/tailwind.config.js b/packages/dashboard-v2/tailwind.config.js
index 076d6912..636cd40e 100644
--- a/packages/dashboard-v2/tailwind.config.js
+++ b/packages/dashboard-v2/tailwind.config.js
@@ -54,6 +54,7 @@ module.exports = {
wiggle: "wiggle 3s ease-in-out infinite",
},
width: {
+ modal: "500px",
page: "100%",
"page-md": "640px",
"page-lg": "896px",
@@ -64,6 +65,9 @@ module.exports = {
minWidth: {
button: "112px",
},
+ maxWidth: {
+ modal: "calc(100vw - 1rem)",
+ },
},
},
plugins: [