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:
@ -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 (
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
Reference in New Issue
Block a user