feat(dashboard-v2): proper copies about api keys

This commit is contained in:
Michał Leszczyk 2022-04-12 13:20:40 +02:00
parent 53b0687b21
commit efed2045af
No known key found for this signature in database
GPG Key ID: FA123CA8BAA2FBF4
7 changed files with 81 additions and 76 deletions

View File

@ -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) => (

View File

@ -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",
})``; })``;

View File

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

View File

@ -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,
}; };

View File

@ -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>
@ -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,7 +192,7 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
); );
}); });
AddPublicAPIKeyForm.displayName = "AddAPIKeyForm"; AddPublicAPIKeyForm.displayName = "AddPublicAPIKeyForm";
AddPublicAPIKeyForm.propTypes = { AddPublicAPIKeyForm.propTypes = {
onSuccess: PropTypes.func.isRequired, onSuccess: PropTypes.func.isRequired,

View File

@ -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>

View File

@ -8,11 +8,12 @@ import { APIKeyList } from "../../components/APIKeyList/APIKeyList";
import { Alert } from "../../components/Alert"; import { Alert } from "../../components/Alert";
import { AddPublicAPIKeyForm } from "../../components/forms/AddPublicAPIKeyForm"; import { AddPublicAPIKeyForm } from "../../components/forms/AddPublicAPIKeyForm";
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,60 @@ 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} /> <AddPublicAPIKeyForm 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 provide full access to <b>Accounts</b> service and are equivalent to using a JWT token.
</p> </p>
<p className="underline"> <p className="font-bold">
This type of API keys needs to be kept secret and should never be shared with anyone. This type of API keys needs to be kept secret and should never be shared with anyone.
</p> </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 +96,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 +113,6 @@ const APIKeysPage = () => {
); );
}; };
APIKeysPage.Layout = UserSettingsLayout; DeveloperSettingsPage.Layout = UserSettingsLayout;
export default APIKeysPage; export default DeveloperSettingsPage;