From 1933ccd270266f48ed173f72a081db10f28d45cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Thu, 24 Mar 2022 18:47:35 +0100 Subject: [PATCH 01/11] feat(dashboard-v2): add 'info' variant to Alert component --- packages/dashboard-v2/src/components/Alert/Alert.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dashboard-v2/src/components/Alert/Alert.js b/packages/dashboard-v2/src/components/Alert/Alert.js index 0e6ab345..4db72620 100644 --- a/packages/dashboard-v2/src/components/Alert/Alert.js +++ b/packages/dashboard-v2/src/components/Alert/Alert.js @@ -3,6 +3,7 @@ import cn from "classnames"; export const Alert = styled.div.attrs(({ $variant }) => ({ className: cn("px-3 py-2 sm:px-6 sm:py-4 rounded border", { + "bg-blue-100 border-blue-200 text-palette-400": $variant === "info", "bg-red-100 border-red-200 text-error": $variant === "error", "bg-green-100 border-green-200 text-palette-400": $variant === "success", }), From ddd109ab0dd2f8a5b10ff2f7f7958ee38b5a6d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Thu, 24 Mar 2022 18:48:04 +0100 Subject: [PATCH 02/11] fix(dashboard-v2): update side image on /api-keys --- packages/dashboard-v2/static/images/api-keys.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/dashboard-v2/static/images/api-keys.svg 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 @@ + From 4c8328c21ff341e6e22471a3da2cba32b5499f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Thu, 24 Mar 2022 19:13:19 +0100 Subject: [PATCH 03/11] feat(dashboard-v2): implement data mutations for API keys --- .../src/components/APIKeyList/APIKey.js | 123 ++++++++++++++++++ .../src/components/APIKeyList/APIKeyList.js | 14 ++ .../src/components/APIKeyList/index.js | 1 + .../components/APIKeyList/useAPIKeyEdit.js | 31 +++++ .../components/APIKeyList/useAPIKeyRemoval.js | 39 ++++++ .../src/components/Modal/Modal.js | 2 +- .../src/components/Modal/index.js | 1 + .../src/components/forms/AddAPIKeyForm.js | 114 ++++++++++++++++ .../forms/AddSkylinkToAPIKeyForm.js | 73 +++++++++++ .../src/pages/settings/api-keys.js | 118 +++++++++++------ packages/dashboard-v2/tailwind.config.js | 4 + 11 files changed, 477 insertions(+), 43 deletions(-) create mode 100644 packages/dashboard-v2/src/components/APIKeyList/APIKey.js create mode 100644 packages/dashboard-v2/src/components/APIKeyList/APIKeyList.js create mode 100644 packages/dashboard-v2/src/components/APIKeyList/index.js create mode 100644 packages/dashboard-v2/src/components/APIKeyList/useAPIKeyEdit.js create mode 100644 packages/dashboard-v2/src/components/APIKeyList/useAPIKeyRemoval.js create mode 100644 packages/dashboard-v2/src/components/forms/AddAPIKeyForm.js create mode 100644 packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js diff --git a/packages/dashboard-v2/src/components/APIKeyList/APIKey.js b/packages/dashboard-v2/src/components/APIKeyList/APIKey.js new file mode 100644 index 00000000..61170327 --- /dev/null +++ b/packages/dashboard-v2/src/components/APIKeyList/APIKey.js @@ -0,0 +1,123 @@ +import dayjs from "dayjs"; +import { useCallback, useState } from "react"; +import { Alert } from "../Alert"; +import { Button } from "../Button"; +import { AddSkylinkToAPIKeyForm } from "../forms/AddSkylinkToAPIKeyForm"; +import { CogIcon, TrashIcon } from "../Icons"; +import { Modal } from "../Modal"; +import { useAPIKeyEdit } from "./useAPIKeyEdit"; +import { useAPIKeyRemoval } from "./useAPIKeyRemoval"; + +export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => { + const { id, name, createdAt, skylinks } = apiKey; + const isPublic = apiKey.public === "true"; + const [error, setError] = useState(null); + + const onSkylinkListEdited = useCallback(() => { + setError(null); + onEdited(); + }, [onEdited]); + + const onSkylinkListEditFailure = (errorMessage) => setError(errorMessage); + + const { + removalError, + removalInitiated, + prompt: promptRemoval, + abort: abortRemoval, + confirm: confirmRemoval, + } = useAPIKeyRemoval({ + key: apiKey, + onSuccess: onRemoved, + onFailure: onRemovalError, + }); + + const { + editInitiated, + prompt: promptEdit, + abort: abortEdit, + removeSkylink, + } = useAPIKeyEdit({ + key: apiKey, + onSkylinkRemoved: onSkylinkListEdited, + onSkylinkRemovalFailure: onSkylinkListEditFailure, + }); + + const closeEditModal = useCallback(() => { + setError(null); + abortEdit(); + }, [abortEdit]); + + return ( +
  • + {name || id} + + {dayjs(createdAt).format("MMM DD, YYYY")} + + {isPublic && ( + + )} + + + + {removalInitiated && ( + +

    Delete API key

    +
    +

    Are you sure you want to delete the following API key?

    +

    {name || id}

    +
    + {removalError && {removalError}} + +
    + + +
    +
    + )} + {editInitiated && ( + +

    Covered Skylinks

    + {skylinks?.length > 0 ? ( +
      + {skylinks.map((skylink) => ( +
    • + + {skylink} + + +
    • + ))} +
    + ) : ( + No skylinks here yet. You can add the first one below 🙃 + )} + +
    + {error && {error}} + +
    +
    + +
    +
    + )} +
  • + ); +}; diff --git a/packages/dashboard-v2/src/components/APIKeyList/APIKeyList.js b/packages/dashboard-v2/src/components/APIKeyList/APIKeyList.js new file mode 100644 index 00000000..3d3e504d --- /dev/null +++ b/packages/dashboard-v2/src/components/APIKeyList/APIKeyList.js @@ -0,0 +1,14 @@ +import { APIKey } from "./APIKey"; + +export const APIKeyList = ({ keys, reloadKeys, title }) => { + return ( + <> +
    {title}
    +
      + {keys.map((key) => ( + + ))} +
    + + ); +}; diff --git a/packages/dashboard-v2/src/components/APIKeyList/index.js b/packages/dashboard-v2/src/components/APIKeyList/index.js new file mode 100644 index 00000000..8ade7744 --- /dev/null +++ b/packages/dashboard-v2/src/components/APIKeyList/index.js @@ -0,0 +1 @@ +export * from "./APIKeyList"; diff --git a/packages/dashboard-v2/src/components/APIKeyList/useAPIKeyEdit.js b/packages/dashboard-v2/src/components/APIKeyList/useAPIKeyEdit.js new file mode 100644 index 00000000..052102af --- /dev/null +++ b/packages/dashboard-v2/src/components/APIKeyList/useAPIKeyEdit.js @@ -0,0 +1,31 @@ +import { useCallback, useState } from "react"; +import accountsService from "../../services/accountsService"; + +export const useAPIKeyEdit = ({ key, onSkylinkRemoved, onSkylinkRemovalFailure }) => { + const [editInitiated, setEditInitiated] = useState(false); + + const prompt = () => setEditInitiated(true); + const abort = () => setEditInitiated(false); + const removeSkylink = useCallback( + async (skylink) => { + try { + await accountsService.patch(`user/apikeys/${key.id}`, { + json: { + remove: [skylink], + }, + }); + onSkylinkRemoved(); + } catch { + onSkylinkRemovalFailure(); + } + }, + [onSkylinkRemoved, onSkylinkRemovalFailure, key] + ); + + return { + editInitiated, + prompt, + abort, + removeSkylink, + }; +}; diff --git a/packages/dashboard-v2/src/components/APIKeyList/useAPIKeyRemoval.js b/packages/dashboard-v2/src/components/APIKeyList/useAPIKeyRemoval.js new file mode 100644 index 00000000..46d5d424 --- /dev/null +++ b/packages/dashboard-v2/src/components/APIKeyList/useAPIKeyRemoval.js @@ -0,0 +1,39 @@ +import { useCallback, useState } from "react"; +import accountsService from "../../services/accountsService"; + +export const useAPIKeyRemoval = ({ key, onSuccess }) => { + const [removalInitiated, setRemovalInitiated] = useState(false); + const [removalError, setRemovalError] = useState(null); + + const prompt = () => setRemovalInitiated(true); + const abort = () => setRemovalInitiated(false); + + const confirm = useCallback(async () => { + setRemovalError(null); + try { + await accountsService.delete(`user/apikeys/${key.id}`); + setRemovalInitiated(false); + onSuccess(); + } catch (err) { + let message = "There was an error processing your request. Please try again later."; + + if (err.response) { + const response = await err.response.json(); + + if (response.message) { + message = response.message; + } + } + + setRemovalError(message); + } + }, [onSuccess, key]); + + return { + removalInitiated, + removalError, + prompt, + abort, + confirm, + }; +}; diff --git a/packages/dashboard-v2/src/components/Modal/Modal.js b/packages/dashboard-v2/src/components/Modal/Modal.js index 6fd8e00a..c183e190 100644 --- a/packages/dashboard-v2/src/components/Modal/Modal.js +++ b/packages/dashboard-v2/src/components/Modal/Modal.js @@ -10,7 +10,7 @@ import { Overlay } from "./Overlay"; export const Modal = ({ children, className, onClose }) => ( -
    +
    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/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 }) => ( +
    +
    + +
    +
    + {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/AddSkylinkToAPIKeyForm.js b/packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js new file mode 100644 index 00000000..fda27d36 --- /dev/null +++ b/packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js @@ -0,0 +1,73 @@ +import * as Yup from "yup"; +import PropTypes from "prop-types"; +import { Formik, Form } from "formik"; + +import accountsService from "../../services/accountsService"; + +import { Button } from "../Button"; +import { TextField } from "../Form/TextField"; +import { CircledProgressIcon, PlusIcon } from "../Icons"; + +const newSkylinkSchema = Yup.object().shape({ + skylink: Yup.string().required("Provide a valid Skylink"), // TODO: Comprehensive Skylink validation +}); + +export const AddSkylinkToAPIKeyForm = ({ keyId, onSuccess, onFailure }) => ( + { + try { + await accountsService + .patch(`user/apikeys/${keyId}`, { + json: { + add: [skylink], + }, + }) + .json(); + + resetForm(); + onSuccess(); + } catch (err) { + if (err.response) { + const { message } = await err.response.json(); + onFailure(message); + } else { + onFailure("Unknown error occured, please try again."); + } + } + }} + > + {({ errors, touched, isSubmitting }) => ( +
    +
    + +
    +
    + {isSubmitting ? ( + + ) : ( + + )} +
    +
    + )} +
    +); + +AddSkylinkToAPIKeyForm.propTypes = { + onFailure: PropTypes.func.isRequired, + onSuccess: PropTypes.func.isRequired, + keyId: PropTypes.string.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..3cbefb17 100644 --- a/packages/dashboard-v2/src/pages/settings/api-keys.js +++ b/packages/dashboard-v2/src/pages/settings/api-keys.js @@ -1,63 +1,97 @@ 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"; 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
    +

    + Give 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
    +

    + Give full access to Accounts service and are equivalent to using a JWT token. +

    +

    This type of API keys need to be kept secret and never 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/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: [ From 614b7791ec10bd272704a367505a72b95f8b50ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 25 Mar 2022 11:22:36 +0100 Subject: [PATCH 04/11] refactor(dashboard-v2): use a hook for adding skylinks to pubkeys --- .../src/components/APIKeyList/APIKey.js | 11 ++++---- .../components/APIKeyList/useAPIKeyEdit.js | 28 +++++++++++++------ .../components/APIKeyList/useAPIKeyRemoval.js | 6 ++-- .../forms/AddSkylinkToAPIKeyForm.js | 24 ++-------------- 4 files changed, 33 insertions(+), 36 deletions(-) diff --git a/packages/dashboard-v2/src/components/APIKeyList/APIKey.js b/packages/dashboard-v2/src/components/APIKeyList/APIKey.js index 61170327..086dc493 100644 --- a/packages/dashboard-v2/src/components/APIKeyList/APIKey.js +++ b/packages/dashboard-v2/src/components/APIKeyList/APIKey.js @@ -36,11 +36,12 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => { editInitiated, prompt: promptEdit, abort: abortEdit, + addSkylink, removeSkylink, } = useAPIKeyEdit({ key: apiKey, - onSkylinkRemoved: onSkylinkListEdited, - onSkylinkRemovalFailure: onSkylinkListEditFailure, + onSkylinkListUpdate: onSkylinkListEdited, + onSkylinkListUpdateFailure: onSkylinkListEditFailure, }); const closeEditModal = useCallback(() => { @@ -95,8 +96,8 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => { {skylinks?.length > 0 ? (
      {skylinks.map((skylink) => ( -
    • - +
    • + {skylink} diff --git a/packages/dashboard-v2/src/components/APIKeyList/useAPIKeyEdit.js b/packages/dashboard-v2/src/components/APIKeyList/useAPIKeyEdit.js index 052102af..a821ca02 100644 --- a/packages/dashboard-v2/src/components/APIKeyList/useAPIKeyEdit.js +++ b/packages/dashboard-v2/src/components/APIKeyList/useAPIKeyEdit.js @@ -1,31 +1,43 @@ import { useCallback, useState } from "react"; import accountsService from "../../services/accountsService"; -export const useAPIKeyEdit = ({ key, onSkylinkRemoved, onSkylinkRemovalFailure }) => { +export const useAPIKeyEdit = ({ key, onSkylinkListUpdate, onSkylinkListUpdateFailure }) => { const [editInitiated, setEditInitiated] = useState(false); const prompt = () => setEditInitiated(true); const abort = () => setEditInitiated(false); - const removeSkylink = useCallback( - async (skylink) => { + const updateSkylinkList = useCallback( + async (action, skylink) => { try { await accountsService.patch(`user/apikeys/${key.id}`, { json: { - remove: [skylink], + [action]: [skylink], }, }); - onSkylinkRemoved(); - } catch { - onSkylinkRemovalFailure(); + onSkylinkListUpdate(); + + return true; + } catch (err) { + if (err.response) { + const { message } = await err.response.json(); + onSkylinkListUpdateFailure(message); + } else { + onSkylinkListUpdateFailure("Unknown error occured, please try again."); + } + + return false; } }, - [onSkylinkRemoved, onSkylinkRemovalFailure, key] + [onSkylinkListUpdate, onSkylinkListUpdateFailure, key] ); + const addSkylink = (skylink) => updateSkylinkList("add", skylink); + const removeSkylink = (skylink) => updateSkylinkList("remove", skylink); return { editInitiated, prompt, abort, + addSkylink, removeSkylink, }; }; diff --git a/packages/dashboard-v2/src/components/APIKeyList/useAPIKeyRemoval.js b/packages/dashboard-v2/src/components/APIKeyList/useAPIKeyRemoval.js index 46d5d424..b9c53bd9 100644 --- a/packages/dashboard-v2/src/components/APIKeyList/useAPIKeyRemoval.js +++ b/packages/dashboard-v2/src/components/APIKeyList/useAPIKeyRemoval.js @@ -5,7 +5,10 @@ export const useAPIKeyRemoval = ({ key, onSuccess }) => { const [removalInitiated, setRemovalInitiated] = useState(false); const [removalError, setRemovalError] = useState(null); - const prompt = () => setRemovalInitiated(true); + const prompt = () => { + setRemovalError(null); + setRemovalInitiated(true); + }; const abort = () => setRemovalInitiated(false); const confirm = useCallback(async () => { @@ -19,7 +22,6 @@ export const useAPIKeyRemoval = ({ key, onSuccess }) => { if (err.response) { const response = await err.response.json(); - if (response.message) { message = response.message; } diff --git a/packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js b/packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js index fda27d36..8c01a0fb 100644 --- a/packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js +++ b/packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js @@ -12,31 +12,15 @@ const newSkylinkSchema = Yup.object().shape({ skylink: Yup.string().required("Provide a valid Skylink"), // TODO: Comprehensive Skylink validation }); -export const AddSkylinkToAPIKeyForm = ({ keyId, onSuccess, onFailure }) => ( +export const AddSkylinkToAPIKeyForm = ({ addSkylink }) => ( { - try { - await accountsService - .patch(`user/apikeys/${keyId}`, { - json: { - add: [skylink], - }, - }) - .json(); - + if (await addSkylink(skylink)) { resetForm(); - onSuccess(); - } catch (err) { - if (err.response) { - const { message } = await err.response.json(); - onFailure(message); - } else { - onFailure("Unknown error occured, please try again."); - } } }} > @@ -67,7 +51,5 @@ export const AddSkylinkToAPIKeyForm = ({ keyId, onSuccess, onFailure }) => ( ); AddSkylinkToAPIKeyForm.propTypes = { - onFailure: PropTypes.func.isRequired, - onSuccess: PropTypes.func.isRequired, - keyId: PropTypes.string.isRequired, + addSkylink: PropTypes.func.isRequired, }; From 9dd4b8eb172b1b2de99bdf20aa51689062b7e9bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 25 Mar 2022 11:45:58 +0100 Subject: [PATCH 05/11] feat(dashboard-v2): add skylink validation & parsing --- .../components/forms/AddSkylinkToAPIKeyForm.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js b/packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js index 8c01a0fb..fd9fe54b 100644 --- a/packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js +++ b/packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js @@ -1,15 +1,22 @@ import * as Yup from "yup"; import PropTypes from "prop-types"; import { Formik, Form } from "formik"; - -import accountsService from "../../services/accountsService"; +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("Provide a valid Skylink"), // TODO: Comprehensive Skylink validation + 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 }) => ( @@ -19,7 +26,7 @@ export const AddSkylinkToAPIKeyForm = ({ addSkylink }) => ( }} validationSchema={newSkylinkSchema} onSubmit={async ({ skylink }, { resetForm }) => { - if (await addSkylink(skylink)) { + if (await addSkylink(parseSkylink(skylink))) { resetForm(); } }} From 08bab146ec644d0323da37c04b106b68270c3e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 25 Mar 2022 12:44:09 +0100 Subject: [PATCH 06/11] feat(dashboard-v2): add node about public key needing configuration --- .../src/components/APIKeyList/APIKey.js | 24 +++++++++++++-- .../Icons/icons/ImportantNoteIcon.js | 11 +++++++ .../src/components/Icons/index.js | 1 + .../src/components/Tooltip/Tooltip.js | 29 +++++++++++++++++++ .../forms/AddSkylinkToAPIKeyForm.js | 1 + 5 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 packages/dashboard-v2/src/components/Icons/icons/ImportantNoteIcon.js create mode 100644 packages/dashboard-v2/src/components/Tooltip/Tooltip.js diff --git a/packages/dashboard-v2/src/components/APIKeyList/APIKey.js b/packages/dashboard-v2/src/components/APIKeyList/APIKey.js index 086dc493..257797f3 100644 --- a/packages/dashboard-v2/src/components/APIKeyList/APIKey.js +++ b/packages/dashboard-v2/src/components/APIKeyList/APIKey.js @@ -1,12 +1,16 @@ import dayjs from "dayjs"; +import cn from "classnames"; import { useCallback, useState } from "react"; + import { Alert } from "../Alert"; import { Button } from "../Button"; import { AddSkylinkToAPIKeyForm } from "../forms/AddSkylinkToAPIKeyForm"; -import { CogIcon, TrashIcon } from "../Icons"; +import { CogIcon, ImportantNoteIcon, 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; @@ -49,9 +53,23 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => { abortEdit(); }, [abortEdit]); + const needsAttention = isPublic && skylinks?.length === 0; + return ( -
    • - {name || id} +
    • + + {name || "unnamed key"} + {needsAttention && ( + + + + )} + {dayjs(createdAt).format("MMM DD, YYYY")} diff --git a/packages/dashboard-v2/src/components/Icons/icons/ImportantNoteIcon.js b/packages/dashboard-v2/src/components/Icons/icons/ImportantNoteIcon.js new file mode 100644 index 00000000..94514716 --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/icons/ImportantNoteIcon.js @@ -0,0 +1,11 @@ +import { withIconProps } from "../withIconProps"; + +export const ImportantNoteIcon = withIconProps(({ size, ...props }) => ( + + + + + + + +)); diff --git a/packages/dashboard-v2/src/components/Icons/index.js b/packages/dashboard-v2/src/components/Icons/index.js index e2af85e9..0bb96694 100644 --- a/packages/dashboard-v2/src/components/Icons/index.js +++ b/packages/dashboard-v2/src/components/Icons/index.js @@ -14,3 +14,4 @@ export * from "./icons/CopyIcon"; export * from "./icons/ShareIcon"; export * from "./icons/SimpleUploadIcon"; export * from "./icons/TrashIcon"; +export * from "./icons/ImportantNoteIcon"; 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..7344bf01 --- /dev/null +++ b/packages/dashboard-v2/src/components/Tooltip/Tooltip.js @@ -0,0 +1,29 @@ +import React, { Children, cloneElement, 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/AddSkylinkToAPIKeyForm.js b/packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js index fd9fe54b..55f27a01 100644 --- a/packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js +++ b/packages/dashboard-v2/src/components/forms/AddSkylinkToAPIKeyForm.js @@ -39,6 +39,7 @@ export const AddSkylinkToAPIKeyForm = ({ addSkylink }) => ( id="skylink" name="skylink" label="New Skylink" + placeholder="Paste a new Skylink here" error={errors.skylink} touched={touched.skylink} /> From 46d295e54ceb675d9dc0ec489d988e924acf62f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 25 Mar 2022 12:48:11 +0100 Subject: [PATCH 07/11] chore(dashboard-v2): cleanup unused imports --- packages/dashboard-v2/src/components/Tooltip/Tooltip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dashboard-v2/src/components/Tooltip/Tooltip.js b/packages/dashboard-v2/src/components/Tooltip/Tooltip.js index 7344bf01..d7bfde39 100644 --- a/packages/dashboard-v2/src/components/Tooltip/Tooltip.js +++ b/packages/dashboard-v2/src/components/Tooltip/Tooltip.js @@ -1,4 +1,4 @@ -import React, { Children, cloneElement, useState } from "react"; +import React, { useState } from "react"; import styled, { keyframes } from "styled-components"; const fadeIn = keyframes` From 45dc78ed193b2fc44ed6e1c9836cbdfbccfa10e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 25 Mar 2022 17:47:44 +0100 Subject: [PATCH 08/11] feat(dashboard-v2): add dedicated form for public api keys --- packages/dashboard-v2/gatsby-config.js | 1 + .../src/components/APIKeyList/APIKey.js | 39 ++-- .../components/forms/AddPublicAPIKeyForm.js | 194 ++++++++++++++++++ .../src/pages/settings/api-keys.js | 3 +- 4 files changed, 217 insertions(+), 20 deletions(-) create mode 100644 packages/dashboard-v2/src/components/forms/AddPublicAPIKeyForm.js 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 ? ( From 34bb7d55d1ac772539edacaa8805d2520f38332c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Mon, 28 Mar 2022 08:07:14 +0200 Subject: [PATCH 09/11] style(dashboard-v2): uncapitalize Skylinks --- .../dashboard-v2/src/components/APIKeyList/APIKey.js | 8 +++++--- .../src/components/forms/AddPublicAPIKeyForm.js | 10 +++++++--- packages/dashboard-v2/src/pages/settings/api-keys.js | 4 ++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/dashboard-v2/src/components/APIKeyList/APIKey.js b/packages/dashboard-v2/src/components/APIKeyList/APIKey.js index a11d1808..5cb6680a 100644 --- a/packages/dashboard-v2/src/components/APIKeyList/APIKey.js +++ b/packages/dashboard-v2/src/components/APIKeyList/APIKey.js @@ -54,6 +54,8 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => { const skylinksNumber = skylinks?.length ?? 0; const isNotConfigured = isPublic && skylinksNumber === 0; + const skylinksPhrasePrefix = skylinksNumber === 0 ? "No" : skylinksNumber; + const skylinksPhrase = `${skylinksPhrasePrefix} ${skylinksNumber === 1 ? "skylink" : "skylinks"} configured`; return (
    • { "text-palette-400": !isNotConfigured, })} > - {skylinksNumber} {skylinksNumber === 1 ? "Skylink" : "Skylinks"} configured + {skylinksPhrase} @@ -80,7 +82,7 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => { {isPublic && (
    -
    diff --git a/packages/dashboard-v2/src/pages/settings/api-keys.js b/packages/dashboard-v2/src/pages/settings/api-keys.js index 05b2701c..8b7cf87f 100644 --- a/packages/dashboard-v2/src/pages/settings/api-keys.js +++ b/packages/dashboard-v2/src/pages/settings/api-keys.js @@ -42,7 +42,7 @@ const APIKeysPage = () => {
    Public keys

    - Give read access to a selected list of Skylinks. You can share them publicly. + Public keys provide read access to a selected list of skylinks. You can share them publicly.

    @@ -68,7 +68,7 @@ const APIKeysPage = () => {
    General keys

    - Give full access to Accounts service and are equivalent to using a JWT token. + These keys provide full access to Accounts service and are equivalent to using a JWT token.

    This type of API keys need to be kept secret and never shared with anyone.

    From 07a5b79710601caf2034827efcf6c84a7e48e804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Mon, 28 Mar 2022 08:29:03 +0200 Subject: [PATCH 10/11] style(dashboard-v2): prettier run --- packages/dashboard-v2/src/pages/settings/api-keys.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dashboard-v2/src/pages/settings/api-keys.js b/packages/dashboard-v2/src/pages/settings/api-keys.js index 8b7cf87f..05013a92 100644 --- a/packages/dashboard-v2/src/pages/settings/api-keys.js +++ b/packages/dashboard-v2/src/pages/settings/api-keys.js @@ -42,7 +42,7 @@ const APIKeysPage = () => {
    Public keys

    - Public keys provide read access to a selected list of skylinks. You can share them publicly. + Public keys provide read access to a selected list of skylinks. You can share them publicly.

    From 34fbdf9e64ab9b035c4281f3266239888d948337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Mon, 28 Mar 2022 09:29:26 +0200 Subject: [PATCH 11/11] style(dashboard-v2): wording --- packages/dashboard-v2/src/pages/settings/api-keys.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/dashboard-v2/src/pages/settings/api-keys.js b/packages/dashboard-v2/src/pages/settings/api-keys.js index 05013a92..1f6c93c5 100644 --- a/packages/dashboard-v2/src/pages/settings/api-keys.js +++ b/packages/dashboard-v2/src/pages/settings/api-keys.js @@ -70,7 +70,9 @@ const APIKeysPage = () => {

    These keys provide full access to Accounts service and are equivalent to using a JWT token.

    -

    This type of API keys need to be kept secret and never shared with anyone.

    +

    + This type of API keys needs to be kept secret and should never be shared with anyone. +