-
Notifications
You must be signed in to change notification settings - Fork 80
Commit
- Loading branch information
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { Button, ButtonProps } from "@/components/ui/button" | ||
import { CheckIcon, CopyIcon } from "lucide-react" | ||
import { useState } from "react" | ||
import { useDebounceCallback } from "usehooks-ts" | ||
|
||
export namespace CopyButton { | ||
export interface Props extends ButtonProps { | ||
text: string | ||
copyResetDelayMs?: number | ||
} | ||
} | ||
|
||
const COPY_RESET_DELAY_MS = 2000 | ||
|
||
export const CopyButton: React.FC<CopyButton.Props> = ({ | ||
Check warning on line 15 in projects/wallet-template/src/containers/WalletPopup/components/CopyButton.tsx GitHub Actions / build (20.x)
Check warning on line 15 in projects/wallet-template/src/containers/WalletPopup/components/CopyButton.tsx GitHub Actions / build (18.x)
Check warning on line 15 in projects/wallet-template/src/containers/WalletPopup/components/CopyButton.tsx GitHub Actions / playwright-test-examples
Check warning on line 15 in projects/wallet-template/src/containers/WalletPopup/components/CopyButton.tsx GitHub Actions / playwright-test-wallet-template
Check warning on line 15 in projects/wallet-template/src/containers/WalletPopup/components/CopyButton.tsx GitHub Actions / connect-flaky-tests
Check warning on line 15 in projects/wallet-template/src/containers/WalletPopup/components/CopyButton.tsx GitHub Actions / playwright-test-extension
|
||
text, | ||
onClick, | ||
copyResetDelayMs, | ||
...props | ||
}) => { | ||
const [isCopied, setIsCopied] = useState(false) | ||
const setIsCopiedDebounced = useDebounceCallback( | ||
setIsCopied, | ||
copyResetDelayMs ?? COPY_RESET_DELAY_MS, | ||
) | ||
|
||
const copy = () => { | ||
copyToClipboard(text) | ||
setIsCopied(true) | ||
setIsCopiedDebounced(false) | ||
} | ||
|
||
return ( | ||
<Button | ||
variant={isCopied ? "ghost" : "outline"} | ||
size="sm" | ||
onClick={(e) => { | ||
copy() | ||
onClick?.(e) | ||
}} | ||
{...props} | ||
> | ||
{isCopied ? ( | ||
<> | ||
<CheckIcon className="w-4 h-4 mr-2" /> | ||
Copied | ||
</> | ||
) : ( | ||
<> | ||
<CopyIcon className="w-4 h-4 mr-2" /> | ||
Copy JSON | ||
</> | ||
)} | ||
</Button> | ||
) | ||
} | ||
|
||
function copyToClipboard(text: string) { | ||
navigator.clipboard.writeText(text) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,76 +1,154 @@ | ||
import React from "react" | ||
import React, { useState } from "react" | ||
import { useForm, SubmitHandler } from "react-hook-form" | ||
import { CheckCircle, Loader } from "lucide-react" | ||
import { useSWRConfig } from "swr" | ||
import { CheckCircle, UploadIcon } from "lucide-react" | ||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" | ||
|
||
import { rpc } from "../../api" | ||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" | ||
import { Label } from "@/components/ui/label" | ||
import { Textarea } from "@/components/ui/textarea" | ||
import { Input } from "@/components/ui/input" | ||
import { Button } from "@/components/ui/button" | ||
import { z } from "zod" | ||
import { zodResolver } from "@hookform/resolvers/zod" | ||
import { | ||
Form, | ||
FormControl, | ||
FormField, | ||
FormItem, | ||
FormLabel, | ||
FormMessage, | ||
} from "@/components/ui/form" | ||
|
||
type FormFields = { | ||
chainSpec: string | ||
rawChainSpec: string | ||
} | ||
|
||
export const AddChainSpec: React.FC = () => { | ||
const formSchema = z.object({ | ||
rawChainSpec: z | ||
.string({ required_error: "Raw Chain Spec is required" }) | ||
.min(1, "Chain Spec is empty") | ||
.trim(), | ||
}) | ||
|
||
export namespace AddChainSpec { | ||
export type Props = { | ||
addChainSpec: (rawChainSpec: string) => Promise<void> | ||
} | ||
} | ||
|
||
type InputMethod = "upload" | "paste" | (string & {}) | ||
|
||
export const AddChainSpec: React.FC<AddChainSpec.Props> = ({ | ||
addChainSpec, | ||
}) => { | ||
const form = useForm<z.infer<typeof formSchema>>({ | ||
resolver: zodResolver(formSchema), | ||
defaultValues: { | ||
rawChainSpec: "", | ||
}, | ||
}) | ||
const { | ||
register, | ||
handleSubmit, | ||
formState: { isSubmitting, errors, isSubmitted, isSubmitSuccessful }, | ||
} = useForm<FormFields>() | ||
const { mutate } = useSWRConfig() | ||
} = form | ||
const [inputMethod, setInputMethod] = useState<InputMethod>("paste") | ||
|
||
const onSubmit: SubmitHandler<FormFields> = async ({ chainSpec }) => { | ||
await rpc.client.addChainSpec(chainSpec) | ||
await mutate("rpc.getChainSpecs") | ||
const onSubmit: SubmitHandler<FormFields> = async ({ | ||
rawChainSpec: chainSpec, | ||
}) => { | ||
await addChainSpec(chainSpec) | ||
} | ||
|
||
return ( | ||
<section aria-labelledby="manual-entry-heading"> | ||
<h2 id="manual-entry-heading" className="font-semibold"> | ||
Enter Chain Specification | ||
</h2> | ||
<form onSubmit={handleSubmit(onSubmit)}> | ||
<textarea | ||
{...register("chainSpec", { | ||
required: "You must specify a chain specification", | ||
})} | ||
className="w-full border border-gray-200 rounded-md p-2 mt-2" | ||
aria-label="Manual chainspec input" | ||
placeholder="Paste your chain specification here..." | ||
rows={8} | ||
></textarea> | ||
{errors.chainSpec && ( | ||
<p className="text-red-500 text-sm mt-1"> | ||
{errors.chainSpec.message} | ||
</p> | ||
)} | ||
|
||
<div className="mt-4 flex justify-center items-center"> | ||
<button | ||
type="submit" | ||
className="border border-gray-300 text-gray-600 py-2 px-4 rounded hover:bg-gray-100 flex items-center disabled:opacity-50" | ||
disabled={isSubmitting} | ||
> | ||
{isSubmitting ? ( | ||
<> | ||
<Loader className="animate-spin h-5 w-5 mr-3" /> | ||
Submitting... | ||
</> | ||
) : ( | ||
"Submit Chain Specification" | ||
)} | ||
</button> | ||
</div> | ||
<Card> | ||
<CardHeader> | ||
<CardTitle>Allow a Chain Spec</CardTitle> | ||
</CardHeader> | ||
<CardContent> | ||
<Form {...form}> | ||
<form onSubmit={handleSubmit(onSubmit)}> | ||
<div className="grid gap-4 mb-4"> | ||
<RadioGroup | ||
value={inputMethod} | ||
onValueChange={setInputMethod} | ||
className="mb-4" | ||
> | ||
<div className="flex items-center mb-2"> | ||
<RadioGroupItem value="paste" /> | ||
<Label className="ml-2 text-foreground"> | ||
Paste Chain Spec JSON | ||
</Label> | ||
</div> | ||
<div className="flex items-center"> | ||
<RadioGroupItem value="upload" /> | ||
<Label className="ml-2 text-foreground"> | ||
Upload Chain Spec File | ||
</Label> | ||
</div> | ||
</RadioGroup> | ||
{inputMethod === "paste" && ( | ||
<FormField | ||
control={form.control} | ||
name="rawChainSpec" | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel>Paste Chain Spec JSON</FormLabel> | ||
<FormControl> | ||
<Textarea | ||
placeholder="Paste your chainspec JSON here..." | ||
{...field} | ||
/> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
)} | ||
{inputMethod === "upload" && ( | ||
<FormField | ||
control={form.control} | ||
name="rawChainSpec" | ||
render={({ field: { value, onChange, ...fieldProps } }) => ( | ||
<FormItem> | ||
<FormLabel>Upload Chain Spec File</FormLabel> | ||
<FormControl> | ||
<Input | ||
{...fieldProps} | ||
type="file" | ||
onChange={async (event) => | ||
onChange( | ||
event.target.files && | ||
(await event.target.files[0].text()), | ||
) | ||
} | ||
/> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
)} | ||
</div> | ||
<div className="flex justify-end"> | ||
<Button type="submit" disabled={isSubmitting}> | ||
<UploadIcon className="w-4 h-4 mr-2" /> | ||
Submit | ||
</Button> | ||
</div> | ||
</form> | ||
</Form> | ||
{isSubmitted && isSubmitSuccessful && ( | ||
<p className="text-green-500 text-center mt-2"> | ||
<p className="mt-2 text-center text-primary"> | ||
<CheckCircle className="inline-block mr-2" /> | ||
Chain specification submitted successfully. | ||
</p> | ||
)} | ||
{isSubmitted && !isSubmitSuccessful && ( | ||
<p className="text-red-500 text-center mt-2"> | ||
{isSubmitted && !isSubmitSuccessful && errors && ( | ||
<p className="mt-2 text-center text-destructive"> | ||
Error submitting chain specification. | ||
</p> | ||
)} | ||
</form> | ||
</section> | ||
</CardContent> | ||
</Card> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" | ||
import { useState } from "react" | ||
import { ListChainSpecs } from "./ListChainSpecs" | ||
import useSWR from "swr" | ||
import { rpc } from "../../api" | ||
import { ChainSpec } from "@/background/types" | ||
import { AddChainSpec } from "./AddChainSpec" | ||
|
||
export const ChainSpecs = () => { | ||
const { data: chainSpecs, mutate } = useSWR( | ||
"rpc.getChainSpecs", | ||
() => rpc.client.getChainSpecs(), | ||
{ revalidateOnFocus: true }, | ||
) | ||
|
||
const [view, setView] = useState<"list" | "allow" | (string & {})>("list") | ||
|
||
const addChainSpec = async (rawChainSpec: string) => { | ||
await rpc.client.addChainSpec(rawChainSpec) | ||
await mutate() | ||
} | ||
|
||
const removeChainSpec = async (chainSpec: ChainSpec) => { | ||
await rpc.client.removeChainSpec(chainSpec.genesisHash) | ||
await mutate() | ||
} | ||
|
||
return ( | ||
<div className="container"> | ||
<h2 className="mb-6 text-3xl font-semibold"> | ||
Manage Chain Specifications | ||
</h2> | ||
|
||
<Tabs defaultValue={view} onValueChange={setView}> | ||
<TabsList className="mb-6"> | ||
<TabsTrigger value="list">Allowlist</TabsTrigger> | ||
<TabsTrigger value="allow">Add ChainSpec</TabsTrigger> | ||
</TabsList> | ||
|
||
<TabsContent value="list"> | ||
<ListChainSpecs | ||
chainSpecs={chainSpecs ?? []} | ||
removeChainSpec={removeChainSpec} | ||
/> | ||
</TabsContent> | ||
|
||
<TabsContent value="allow"> | ||
<AddChainSpec addChainSpec={addChainSpec} /> | ||
</TabsContent> | ||
</Tabs> | ||
</div> | ||
) | ||
} |