This repository has been archived on 2022-10-07. You can view files and clone it, but cannot push or open issues or pull requests.
skynet-webportal/packages/dashboard-v2/src/components/forms/AddPublicAPIKeyForm.js

199 lines
7.0 KiB
JavaScript

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 (
<div ref={ref} className="flex flex-col gap-4">
{state === State.Success && (
<Alert $variant="success" className="text-center">
<strong>Success!</strong>
<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">
<code className="p-2 rounded border border-palette-200 text-xs selection:bg-primary/30 truncate">
{generatedKey}
</code>
<CopyButton value={generatedKey} className="whitespace-nowrap" />
</div>
</Alert>
)}
{state === State.Failure && (
<Alert $variant="error">We were not able to generate a new key. Please try again later.</Alert>
)}
<Formik
initialValues={{
name: "",
skylinks: [],
nextSkylink: "",
}}
validationSchema={newPublicAPIKeySchema}
onSubmit={async ({ name, skylinks, nextSkylink }, { resetForm }) => {
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 }) => (
<Form className="flex flex-col gap-4">
<div className="flex items-center">
<TextField
type="text"
id="name"
name="name"
label="Public API Key Name"
placeholder="my_applications_statistics"
error={errors.name}
touched={touched.name}
/>
</div>
<div>
<h6 className="text-palette-300 mb-2">Skylinks accessible with the new key</h6>
<FieldArray
name="skylinks"
render={({ push, remove }) => {
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 (
<div className="flex flex-col gap-2">
{skylinks.map((_, index) => (
<div key={index} className="flex gap-4 items-start">
<TextField
type="text"
name={`skylinks.${index}`}
placeholder={`${index + 1}. skylink`}
error={skylinksErrors[index]}
touched={skylinksTouched[index]}
/>
<span className="w-[24px] shrink-0 mt-3">
<button type="button" onClick={() => remove(index)}>
<TrashIcon size={16} />
</button>
</span>
</div>
))}
<div className="flex gap-4 items-start">
<TextField
type="text"
name="nextSkylink"
placeholder={`Paste next skylink here`}
error={errors.nextSkylink}
touched={touched.nextSkylink}
onKeyPress={(event) => {
if (event.key === "Enter" && isValid) {
event.preventDefault();
appendSkylink(values.nextSkylink);
}
}}
/>
<button
type="button"
onClick={() => appendSkylink(values.nextSkylink)}
className={cn("shrink-0 mt-1.5 w-[24px] h-[24px]", {
"text-palette-300 cursor-not-allowed": isNextSkylinkInvalid,
})}
disabled={isNextSkylinkInvalid}
>
<PlusIcon size={15} />
</button>
</div>
</div>
);
}}
/>
</div>
<div className="flex mt-5 justify-center">
<Button
type="submit"
className={cn("px-2.5", { "cursor-wait": isSubmitting })}
disabled={!isValid || isSubmitting}
>
{isSubmitting ? "Generating" : "Generate"} your public key
</Button>
</div>
</Form>
)}
</Formik>
</div>
);
});
AddPublicAPIKeyForm.displayName = "AddAPIKeyForm";
AddPublicAPIKeyForm.propTypes = {
onSuccess: PropTypes.func.isRequired,
};