diff --git a/packages/dashboard-v2/gatsby-config.js b/packages/dashboard-v2/gatsby-config.js index 160a7784..ce35de3a 100644 --- a/packages/dashboard-v2/gatsby-config.js +++ b/packages/dashboard-v2/gatsby-config.js @@ -5,6 +5,7 @@ module.exports = { title: `Accounts Dashboard`, siteUrl: `https://www.yourdomain.tld`, }, + trailingSlash: "never", plugins: [ "gatsby-plugin-image", "gatsby-plugin-provide-react", diff --git a/packages/dashboard-v2/src/components/APIKeyList/APIKey.js b/packages/dashboard-v2/src/components/APIKeyList/APIKey.js index 257797f3..a11d1808 100644 --- a/packages/dashboard-v2/src/components/APIKeyList/APIKey.js +++ b/packages/dashboard-v2/src/components/APIKeyList/APIKey.js @@ -5,12 +5,11 @@ import { useCallback, useState } from "react"; import { Alert } from "../Alert"; import { Button } from "../Button"; import { AddSkylinkToAPIKeyForm } from "../forms/AddSkylinkToAPIKeyForm"; -import { CogIcon, ImportantNoteIcon, TrashIcon } from "../Icons"; +import { CogIcon, TrashIcon } from "../Icons"; import { Modal } from "../Modal"; import { useAPIKeyEdit } from "./useAPIKeyEdit"; import { useAPIKeyRemoval } from "./useAPIKeyRemoval"; -import { Tooltip } from "../Tooltip/Tooltip"; export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => { const { id, name, createdAt, skylinks } = apiKey; @@ -53,22 +52,28 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => { abortEdit(); }, [abortEdit]); - const needsAttention = isPublic && skylinks?.length === 0; + const skylinksNumber = skylinks?.length ?? 0; + const isNotConfigured = isPublic && skylinksNumber === 0; return (
  • - {name || "unnamed key"} - {needsAttention && ( - - - - )} + + {name || "unnamed key"} + + {dayjs(createdAt).format("MMM DD, YYYY")} @@ -77,16 +82,12 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => { )} - 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..25890275 --- /dev/null +++ b/packages/dashboard-v2/src/components/forms/AddPublicAPIKeyForm.js @@ -0,0 +1,194 @@ +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 }) => ( +
    +
    + +
    +
    +
    Skylinks accessible with the new key
    + { + const { skylinks = [] } = values; + const { skylinks: skylinksErrors = [] } = errors; + const { skylinks: skylinksTouched = [] } = touched; + + const appendSkylink = (skylink) => { + push(skylink); + setFieldValue("nextSkylink", "", false); + setFieldTouched("nextSkylink", false); + }; + const isNextSkylinkInvalid = Boolean( + errors.nextSkylink || !touched.nextSkylink || !values.nextSkylink + ); + + return ( +
    + {skylinks.map((_, index) => ( +
    + + + + +
    + ))} + +
    + { + if (event.key === "Enter" && isValid) { + event.preventDefault(); + appendSkylink(values.nextSkylink); + } + }} + /> + +
    +
    + ); + }} + /> +
    + +
    + +
    +
    + )} +
    +
    + ); +}); + +AddPublicAPIKeyForm.displayName = "AddAPIKeyForm"; + +AddPublicAPIKeyForm.propTypes = { + onSuccess: 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 3cbefb17..05b2701c 100644 --- a/packages/dashboard-v2/src/pages/settings/api-keys.js +++ b/packages/dashboard-v2/src/pages/settings/api-keys.js @@ -6,6 +6,7 @@ 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"; const APIKeysPage = () => { const { data: apiKeys = [], mutate: reloadKeys, error } = useSWR("user/apikeys"); @@ -45,7 +46,7 @@ const APIKeysPage = () => {

    - +
    {error ? (