explorer: add error message to copy component (#15423)

* explorer: add error message to copy component

* Merge copyable and copy button components

Co-authored-by: Justin Starry <justin@solana.com>
This commit is contained in:
Josh
2021-02-18 19:52:05 -08:00
committed by GitHub
parent 4e84869c8e
commit e0e4bed205
9 changed files with 130 additions and 133 deletions

View File

@ -1,11 +1,11 @@
import React, { useState } from "react"; import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from "@solana/web3.js";
import { clusterPath } from "utils/url"; import { clusterPath } from "utils/url";
import { displayAddress } from "utils/tx"; import { displayAddress } from "utils/tx";
import { useCluster } from "providers/cluster"; import { useCluster } from "providers/cluster";
import { Copyable } from "./Copyable";
type CopyState = "copy" | "copied";
type Props = { type Props = {
pubkey: PublicKey; pubkey: PublicKey;
alignRight?: boolean; alignRight?: boolean;
@ -23,31 +23,15 @@ export function Address({
truncate, truncate,
truncateUnknown, truncateUnknown,
}: Props) { }: Props) {
const [state, setState] = useState<CopyState>("copy");
const address = pubkey.toBase58(); const address = pubkey.toBase58();
const { cluster } = useCluster(); const { cluster } = useCluster();
const copyToClipboard = () => navigator.clipboard.writeText(address);
const handleClick = () =>
copyToClipboard().then(() => {
setState("copied");
setTimeout(() => setState("copy"), 1000);
});
const copyIcon =
state === "copy" ? (
<span className="fe fe-copy" onClick={handleClick}></span>
) : (
<span className="fe fe-check-circle"></span>
);
if (truncateUnknown && address === displayAddress(address, cluster)) { if (truncateUnknown && address === displayAddress(address, cluster)) {
truncate = true; truncate = true;
} }
const content = ( const content = (
<> <Copyable text={address} replaceText={!alignRight}>
<span className="c-pointer font-size-tiny mr-2">{copyIcon}</span>
<span className="text-monospace"> <span className="text-monospace">
{link ? ( {link ? (
<Link <Link
@ -62,7 +46,7 @@ export function Address({
</span> </span>
)} )}
</span> </span>
</> </Copyable>
); );
return ( return (

View File

@ -1,56 +1,98 @@
import React, { useState, ReactNode } from "react"; import React, { ReactNode, useState } from "react";
type CopyableProps = { type CopyState = "copy" | "copied" | "errored";
export function Copyable({
text,
children,
replaceText,
}: {
text: string; text: string;
children: ReactNode; children: ReactNode;
bottom?: boolean; replaceText?: boolean;
right?: boolean;
};
type State = "hide" | "copy" | "copied";
function Popover({
state,
bottom,
right,
}: {
state: State;
bottom?: boolean;
right?: boolean;
}) { }) {
if (state === "hide") return null; const [state, setState] = useState<CopyState>("copy");
const text = state === "copy" ? "Copy" : "Copied!";
return (
<div
className={`popover bs-popover-${bottom ? "bottom" : "top"}${
right ? " right" : ""
} show`}
>
<div className={`arrow${right ? " right" : ""}`} />
<div className="popover-body">{text}</div>
</div>
);
}
export function Copyable({ bottom, right, text, children }: CopyableProps) { const handleClick = async () => {
const [state, setState] = useState<State>("hide"); try {
await navigator.clipboard.writeText(text);
const copyToClipboard = () => navigator.clipboard.writeText(text);
const handleClick = () =>
copyToClipboard().then(() => {
setState("copied"); setState("copied");
setTimeout(() => setState("hide"), 1000); } catch (err) {
}); setState("errored");
}
setTimeout(() => setState("copy"), 1000);
};
function CopyIcon() {
if (state === "copy") {
return (
<span className="fe fe-copy c-pointer" onClick={handleClick}></span>
);
} else if (state === "copied") {
return <span className="fe fe-check-circle"></span>;
} else if (state === "errored") {
return (
<span
className="fe fe-x-circle"
title="Please check your browser's copy permissions."
></span>
);
}
return null;
}
let message = "";
let textColor = "";
if (state === "copied") {
message = "Copied";
textColor = "text-info";
} else if (state === "errored") {
message = "Copy Failed";
textColor = "text-danger";
}
function PrependCopyIcon() {
return (
<>
<span className="font-size-tiny mr-2">
<span className={textColor}>
<span className="mr-2">{message}</span>
<CopyIcon />
</span>
</span>
{children}
</>
);
}
function ReplaceWithMessage() {
return (
<span className="d-flex flex-column flex-nowrap">
<span className="font-size-tiny">
<span className={textColor}>
<CopyIcon />
<span className="ml-2">{message}</span>
</span>
</span>
<span className="v-hidden">{children}</span>
</span>
);
}
if (state === "copy") {
return <PrependCopyIcon />;
} else if (replaceText) {
return <ReplaceWithMessage />;
}
return ( return (
<div <>
className="popover-container c-pointer" <span className="d-none d-lg-inline">
onClick={handleClick} <PrependCopyIcon />
onMouseOver={() => setState("copy")} </span>
onMouseOut={() => state === "copy" && setState("hide")} <span className="d-inline d-lg-none">
> <ReplaceWithMessage />
{children} </span>
<Popover bottom={bottom} right={right} state={state} /> </>
</div>
); );
} }

View File

@ -1,9 +1,9 @@
import React, { useState } from "react"; import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { TransactionSignature } from "@solana/web3.js"; import { TransactionSignature } from "@solana/web3.js";
import { clusterPath } from "utils/url"; import { clusterPath } from "utils/url";
import { Copyable } from "./Copyable";
type CopyState = "copy" | "copied";
type Props = { type Props = {
signature: TransactionSignature; signature: TransactionSignature;
alignRight?: boolean; alignRight?: boolean;
@ -12,45 +12,26 @@ type Props = {
}; };
export function Signature({ signature, alignRight, link, truncate }: Props) { export function Signature({ signature, alignRight, link, truncate }: Props) {
const [state, setState] = useState<CopyState>("copy");
const copyToClipboard = () => navigator.clipboard.writeText(signature);
const handleClick = () =>
copyToClipboard().then(() => {
setState("copied");
setTimeout(() => setState("copy"), 1000);
});
const copyIcon =
state === "copy" ? (
<span className="fe fe-copy" onClick={handleClick}></span>
) : (
<span className="fe fe-check-circle"></span>
);
const copyButton = (
<span className="c-pointer font-size-tiny mr-2">{copyIcon}</span>
);
return ( return (
<div <div
className={`d-flex align-items-center ${ className={`d-flex align-items-center ${
alignRight ? "justify-content-end" : "" alignRight ? "justify-content-end" : ""
}`} }`}
> >
{copyButton} <Copyable text={signature} replaceText={!alignRight}>
<span className="text-monospace"> <span className="text-monospace">
{link ? ( {link ? (
<Link <Link
className={truncate ? "text-truncate signature-truncate" : ""} className={truncate ? "text-truncate signature-truncate" : ""}
to={clusterPath(`/tx/${signature}`)} to={clusterPath(`/tx/${signature}`)}
> >
{signature} {signature}
</Link> </Link>
) : ( ) : (
signature signature
)} )}
</span> </span>
</Copyable>
</div> </div>
); );
} }

View File

@ -1,40 +1,21 @@
import React, { useState } from "react"; import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { clusterPath } from "utils/url"; import { clusterPath } from "utils/url";
import { Copyable } from "./Copyable";
type CopyState = "copy" | "copied";
type Props = { type Props = {
slot: number; slot: number;
link?: boolean; link?: boolean;
}; };
export function Slot({ slot, link }: Props) { export function Slot({ slot, link }: Props) {
const [state, setState] = useState<CopyState>("copy");
const copyToClipboard = () => navigator.clipboard.writeText(slot.toString());
const handleClick = () =>
copyToClipboard().then(() => {
setState("copied");
setTimeout(() => setState("copy"), 1000);
});
const copyIcon =
state === "copy" ? (
<span className="fe fe-copy" onClick={handleClick}></span>
) : (
<span className="fe fe-check-circle"></span>
);
const copyButton = (
<span className="c-pointer font-size-tiny mr-2">{copyIcon}</span>
);
return link ? ( return link ? (
<span className="text-monospace"> <Copyable text={slot.toString()}>
{copyButton} <span className="text-monospace">
<Link to={clusterPath(`/block/${slot}`)}> <Link to={clusterPath(`/block/${slot}`)}>
{slot.toLocaleString("en-US")} {slot.toLocaleString("en-US")}
</Link> </Link>
</span> </span>
</Copyable>
) : ( ) : (
<span className="text-monospace">{slot.toLocaleString("en-US")}</span> <span className="text-monospace">{slot.toLocaleString("en-US")}</span>
); );

View File

@ -52,7 +52,7 @@ export function AllocateWithSeedDetailsCard(props: {
<tr> <tr>
<td>Seed</td> <td>Seed</td>
<td className="text-lg-right"> <td className="text-lg-right">
<Copyable right text={info.seed}> <Copyable text={info.seed}>
<code>{info.seed}</code> <code>{info.seed}</code>
</Copyable> </Copyable>
</td> </td>

View File

@ -52,7 +52,7 @@ export function AssignWithSeedDetailsCard(props: {
<tr> <tr>
<td>Seed</td> <td>Seed</td>
<td className="text-lg-right"> <td className="text-lg-right">
<Copyable right text={info.seed}> <Copyable text={info.seed}>
<code>{info.seed}</code> <code>{info.seed}</code>
</Copyable> </Copyable>
</td> </td>

View File

@ -60,7 +60,7 @@ export function CreateWithSeedDetailsCard(props: {
<tr> <tr>
<td>Seed</td> <td>Seed</td>
<td className="text-lg-right"> <td className="text-lg-right">
<Copyable right text={info.seed}> <Copyable text={info.seed}>
<code>{info.seed}</code> <code>{info.seed}</code>
</Copyable> </Copyable>
</td> </td>

View File

@ -65,7 +65,7 @@ export function TransferWithSeedDetailsCard(props: {
<tr> <tr>
<td>Seed</td> <td>Seed</td>
<td className="text-lg-right"> <td className="text-lg-right">
<Copyable right text={info.sourceSeed}> <Copyable text={info.sourceSeed}>
<code>{info.sourceSeed}</code> <code>{info.sourceSeed}</code>
</Copyable> </Copyable>
</td> </td>

View File

@ -71,6 +71,15 @@ ul.log-messages {
cursor: pointer; cursor: pointer;
} }
.v-hidden {
visibility: hidden;
max-height: 0;
}
.flex-nowrap {
flex-wrap: nowrap;
}
.dropdown-exit { .dropdown-exit {
position: absolute; position: absolute;
z-index: 100; z-index: 100;