feat: you can now deep link to a particular instruction in Explorer (#24861)
* fix: when creating new cluster URLs, don't carry the fragment forward * feat: added a utility that computes a transaction's URL fragment * feat: introduced a React context you can use to scroll to elements * feat: you can now deep link to a particular transaction in Explorer
This commit is contained in:
parent
8043e88233
commit
2108803b0c
|
@ -4,6 +4,8 @@ import { TableCardBody } from "components/common/TableCardBody";
|
||||||
import { InstructionLogs } from "utils/program-logs";
|
import { InstructionLogs } from "utils/program-logs";
|
||||||
import { ProgramName } from "utils/anchor";
|
import { ProgramName } from "utils/anchor";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import getInstructionCardScrollAnchorId from "utils/get-instruction-card-scroll-anchor-id";
|
||||||
|
|
||||||
const NATIVE_PROGRAMS_MISSING_INVOKE_LOG: string[] = [
|
const NATIVE_PROGRAMS_MISSING_INVOKE_LOG: string[] = [
|
||||||
"AddressLookupTab1e1111111111111111111111111",
|
"AddressLookupTab1e1111111111111111111111111",
|
||||||
|
@ -62,17 +64,26 @@ export function ProgramLogsCardBody({
|
||||||
return (
|
return (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td>
|
<td>
|
||||||
<div className="d-flex align-items-center">
|
<Link
|
||||||
|
className="d-flex align-items-center"
|
||||||
|
to={(location) => ({
|
||||||
|
...location,
|
||||||
|
hash: `#${getInstructionCardScrollAnchorId([index + 1])}`,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<span className={`badge bg-${badgeColor}-soft me-2`}>
|
<span className={`badge bg-${badgeColor}-soft me-2`}>
|
||||||
#{index + 1}
|
#{index + 1}
|
||||||
</span>
|
</span>
|
||||||
<ProgramName
|
<span className="program-log-instruction-name">
|
||||||
programId={programId}
|
<ProgramName
|
||||||
cluster={cluster}
|
programId={programId}
|
||||||
url={url}
|
cluster={cluster}
|
||||||
/>{" "}
|
url={url}
|
||||||
Instruction
|
/>{" "}
|
||||||
</div>
|
Instruction
|
||||||
|
</span>
|
||||||
|
<span className="fe fe-chevrons-up c-pointer px-2" />
|
||||||
|
</Link>
|
||||||
{programLogs && (
|
{programLogs && (
|
||||||
<div className="d-flex align-items-start flex-column font-monospace p-2 font-size-sm">
|
<div className="d-flex align-items-start flex-column font-monospace p-2 font-size-sm">
|
||||||
{programLogs.logs.map((log, key) => {
|
{programLogs.logs.map((log, key) => {
|
||||||
|
|
|
@ -12,6 +12,8 @@ import {
|
||||||
useRawTransactionDetails,
|
useRawTransactionDetails,
|
||||||
} from "providers/transactions/raw";
|
} from "providers/transactions/raw";
|
||||||
import { Address } from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
import { useScrollAnchor } from "providers/scroll-anchor";
|
||||||
|
import getInstructionCardScrollAnchorId from "utils/get-instruction-card-scroll-anchor-id";
|
||||||
|
|
||||||
type InstructionProps = {
|
type InstructionProps = {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -54,9 +56,13 @@ export function InstructionCard({
|
||||||
|
|
||||||
return setShowRaw((r) => !r);
|
return setShowRaw((r) => !r);
|
||||||
};
|
};
|
||||||
|
const scrollAnchorRef = useScrollAnchor(
|
||||||
|
getInstructionCardScrollAnchorId(
|
||||||
|
childIndex != null ? [index + 1, childIndex] : [index + 1]
|
||||||
|
)
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="card" ref={scrollAnchorRef}>
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<h3 className="card-header-title mb-0 d-flex align-items-center">
|
<h3 className="card-header-title mb-0 d-flex align-items-center">
|
||||||
<span className={`badge bg-${resultClass}-soft me-2`}>
|
<span className={`badge bg-${resultClass}-soft me-2`}>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { TransactionsProvider } from "./providers/transactions";
|
||||||
import { AccountsProvider } from "./providers/accounts";
|
import { AccountsProvider } from "./providers/accounts";
|
||||||
import { BlockProvider } from "./providers/block";
|
import { BlockProvider } from "./providers/block";
|
||||||
import { EpochProvider } from "./providers/epoch";
|
import { EpochProvider } from "./providers/epoch";
|
||||||
|
import { ScrollAnchorProvider } from "providers/scroll-anchor";
|
||||||
import { StatsProvider } from "providers/stats";
|
import { StatsProvider } from "providers/stats";
|
||||||
import { MintsProvider } from "providers/mints";
|
import { MintsProvider } from "providers/mints";
|
||||||
|
|
||||||
|
@ -22,24 +23,26 @@ if (process.env.NODE_ENV === "production") {
|
||||||
const root = createRoot(document.getElementById("root")!);
|
const root = createRoot(document.getElementById("root")!);
|
||||||
root.render(
|
root.render(
|
||||||
<Router>
|
<Router>
|
||||||
<ClusterProvider>
|
<ScrollAnchorProvider>
|
||||||
<StatsProvider>
|
<ClusterProvider>
|
||||||
<SupplyProvider>
|
<StatsProvider>
|
||||||
<RichListProvider>
|
<SupplyProvider>
|
||||||
<AccountsProvider>
|
<RichListProvider>
|
||||||
<BlockProvider>
|
<AccountsProvider>
|
||||||
<EpochProvider>
|
<BlockProvider>
|
||||||
<MintsProvider>
|
<EpochProvider>
|
||||||
<TransactionsProvider>
|
<MintsProvider>
|
||||||
<App />
|
<TransactionsProvider>
|
||||||
</TransactionsProvider>
|
<App />
|
||||||
</MintsProvider>
|
</TransactionsProvider>
|
||||||
</EpochProvider>
|
</MintsProvider>
|
||||||
</BlockProvider>
|
</EpochProvider>
|
||||||
</AccountsProvider>
|
</BlockProvider>
|
||||||
</RichListProvider>
|
</AccountsProvider>
|
||||||
</SupplyProvider>
|
</RichListProvider>
|
||||||
</StatsProvider>
|
</SupplyProvider>
|
||||||
</ClusterProvider>
|
</StatsProvider>
|
||||||
|
</ClusterProvider>
|
||||||
|
</ScrollAnchorProvider>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,6 +6,8 @@ import { AddressWithContext, programValidator } from "./AddressWithContext";
|
||||||
import { useCluster } from "providers/cluster";
|
import { useCluster } from "providers/cluster";
|
||||||
import { getProgramName } from "utils/tx";
|
import { getProgramName } from "utils/tx";
|
||||||
import { HexData } from "components/common/HexData";
|
import { HexData } from "components/common/HexData";
|
||||||
|
import getInstructionCardScrollAnchorId from "utils/get-instruction-card-scroll-anchor-id";
|
||||||
|
import { useScrollAnchor } from "providers/scroll-anchor";
|
||||||
|
|
||||||
export function InstructionsSection({ message }: { message: Message }) {
|
export function InstructionsSection({ message }: { message: Message }) {
|
||||||
return (
|
return (
|
||||||
|
@ -30,9 +32,11 @@ function InstructionCard({
|
||||||
const { cluster } = useCluster();
|
const { cluster } = useCluster();
|
||||||
const programId = message.accountKeys[ix.programIdIndex];
|
const programId = message.accountKeys[ix.programIdIndex];
|
||||||
const programName = getProgramName(programId.toBase58(), cluster);
|
const programName = getProgramName(programId.toBase58(), cluster);
|
||||||
|
const scrollAnchorRef = useScrollAnchor(
|
||||||
|
getInstructionCardScrollAnchorId([index + 1])
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div className="card" id={`instruction-index-${index + 1}`} key={index}>
|
<div className="card" key={index} ref={scrollAnchorRef}>
|
||||||
<div className={`card-header${!expanded ? " border-bottom-none" : ""}`}>
|
<div className={`card-header${!expanded ? " border-bottom-none" : ""}`}>
|
||||||
<h3 className="card-header-title mb-0 d-flex align-items-center">
|
<h3 className="card-header-title mb-0 d-flex align-items-center">
|
||||||
<span className={`badge bg-info-soft me-2`}>#{index + 1}</span>
|
<span className={`badge bg-info-soft me-2`}>#{index + 1}</span>
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
ReactNode,
|
||||||
|
RefCallback,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
type URLFragment = string;
|
||||||
|
|
||||||
|
type RegisterScrollAnchorFn = (key: string, element: HTMLElement) => void;
|
||||||
|
|
||||||
|
const ScrollAnchorContext = createContext<RegisterScrollAnchorFn>(
|
||||||
|
typeof WeakRef !== "undefined"
|
||||||
|
? (key: string) => {
|
||||||
|
console.warn(
|
||||||
|
`Ignoring registration of scroll anchor for key \`${key}\`.` +
|
||||||
|
"Did you forget to wrap your app in a `ScrollAnchorProvider`?"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: // This entire implementation gets disabled if WeakRef is not supported
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ScrollAnchorProvider =
|
||||||
|
typeof WeakRef !== "undefined"
|
||||||
|
? function ScrollAnchorProvider({ children }: { children: ReactNode }) {
|
||||||
|
const location = useLocation();
|
||||||
|
const registeredScrollTargets = useRef<{
|
||||||
|
[fragment: URLFragment]: WeakRef<HTMLElement>[] | undefined;
|
||||||
|
}>({});
|
||||||
|
const scrollEnabled = useRef<boolean>(false);
|
||||||
|
const maybeScroll = useCallback(() => {
|
||||||
|
if (scrollEnabled.current === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetsByFragment = registeredScrollTargets.current;
|
||||||
|
const fragment = location.hash.replace(/^#/, "");
|
||||||
|
const targets = targetsByFragment[fragment];
|
||||||
|
if (!targets) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
targets.some((targetWeakRef) => {
|
||||||
|
const target = targetWeakRef.deref();
|
||||||
|
if (!target) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
scrollEnabled.current = false;
|
||||||
|
target.scrollIntoView();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [location.hash]);
|
||||||
|
const registerScrollAnchor = useCallback(
|
||||||
|
(fragment: string, element: HTMLElement) => {
|
||||||
|
const targetsByFragment = registeredScrollTargets.current;
|
||||||
|
const targets = (targetsByFragment[fragment] =
|
||||||
|
targetsByFragment[fragment] || []);
|
||||||
|
targets.unshift(new WeakRef(element));
|
||||||
|
maybeScroll();
|
||||||
|
},
|
||||||
|
[maybeScroll]
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
let distanceScrolled = 0;
|
||||||
|
let lastKnownScrollPosition = window.scrollY;
|
||||||
|
const handleScroll = () => {
|
||||||
|
const currentScrollPosition = window.scrollY;
|
||||||
|
distanceScrolled += Math.abs(
|
||||||
|
lastKnownScrollPosition - currentScrollPosition
|
||||||
|
);
|
||||||
|
lastKnownScrollPosition = currentScrollPosition;
|
||||||
|
if (distanceScrolled > 44) {
|
||||||
|
// If the user has scrolled the page while waiting for the target
|
||||||
|
// to appear during initial load, we do not want to steal control
|
||||||
|
// away from them.
|
||||||
|
scrollEnabled.current = false;
|
||||||
|
window.removeEventListener("scroll", handleScroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", handleScroll);
|
||||||
|
};
|
||||||
|
}, [location]);
|
||||||
|
useEffect(() => {
|
||||||
|
scrollEnabled.current = true;
|
||||||
|
maybeScroll();
|
||||||
|
}, [location, maybeScroll]);
|
||||||
|
return (
|
||||||
|
<ScrollAnchorContext.Provider value={registerScrollAnchor}>
|
||||||
|
{children}
|
||||||
|
</ScrollAnchorContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: // This entire implementation gets disabled if WeakRef is not supported
|
||||||
|
React.Fragment;
|
||||||
|
|
||||||
|
export function useScrollAnchor(key: URLFragment): RefCallback<HTMLElement> {
|
||||||
|
const registerScrollTarget = useContext(ScrollAnchorContext);
|
||||||
|
return useCallback(
|
||||||
|
(instance) => {
|
||||||
|
if (!instance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
registerScrollTarget(key, instance);
|
||||||
|
},
|
||||||
|
[key, registerScrollTarget]
|
||||||
|
);
|
||||||
|
}
|
|
@ -373,6 +373,10 @@ pre.json-wrap {
|
||||||
max-width: 36rem;
|
max-width: 36rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.program-log-instruction-name {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
.staking-card {
|
.staking-card {
|
||||||
h1 {
|
h1 {
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default function getInstructionCardScrollAnchorId(
|
||||||
|
// An array of instruction sequence numbers, starting with the
|
||||||
|
// top level instruction number. Instruction numbers start from 1.
|
||||||
|
instructionNumberPath: number[]
|
||||||
|
): string {
|
||||||
|
return `ix-${instructionNumberPath.join("-")}`;
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ export function pickClusterParams(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...location,
|
...location,
|
||||||
|
hash: "",
|
||||||
search: newParams.toString(),
|
search: newParams.toString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue