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:
Steven Luscher 2022-05-02 12:21:13 -07:00 committed by GitHub
parent 8043e88233
commit 2108803b0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 179 additions and 31 deletions

View File

@ -4,6 +4,8 @@ import { TableCardBody } from "components/common/TableCardBody";
import { InstructionLogs } from "utils/program-logs";
import { ProgramName } from "utils/anchor";
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[] = [
"AddressLookupTab1e1111111111111111111111111",
@ -62,17 +64,26 @@ export function ProgramLogsCardBody({
return (
<tr key={index}>
<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`}>
#{index + 1}
</span>
<ProgramName
programId={programId}
cluster={cluster}
url={url}
/>{" "}
Instruction
</div>
<span className="program-log-instruction-name">
<ProgramName
programId={programId}
cluster={cluster}
url={url}
/>{" "}
Instruction
</span>
<span className="fe fe-chevrons-up c-pointer px-2" />
</Link>
{programLogs && (
<div className="d-flex align-items-start flex-column font-monospace p-2 font-size-sm">
{programLogs.logs.map((log, key) => {

View File

@ -12,6 +12,8 @@ import {
useRawTransactionDetails,
} from "providers/transactions/raw";
import { Address } from "components/common/Address";
import { useScrollAnchor } from "providers/scroll-anchor";
import getInstructionCardScrollAnchorId from "utils/get-instruction-card-scroll-anchor-id";
type InstructionProps = {
title: string;
@ -54,9 +56,13 @@ export function InstructionCard({
return setShowRaw((r) => !r);
};
const scrollAnchorRef = useScrollAnchor(
getInstructionCardScrollAnchorId(
childIndex != null ? [index + 1, childIndex] : [index + 1]
)
);
return (
<div className="card">
<div className="card" ref={scrollAnchorRef}>
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
<span className={`badge bg-${resultClass}-soft me-2`}>

View File

@ -10,6 +10,7 @@ import { TransactionsProvider } from "./providers/transactions";
import { AccountsProvider } from "./providers/accounts";
import { BlockProvider } from "./providers/block";
import { EpochProvider } from "./providers/epoch";
import { ScrollAnchorProvider } from "providers/scroll-anchor";
import { StatsProvider } from "providers/stats";
import { MintsProvider } from "providers/mints";
@ -22,24 +23,26 @@ if (process.env.NODE_ENV === "production") {
const root = createRoot(document.getElementById("root")!);
root.render(
<Router>
<ClusterProvider>
<StatsProvider>
<SupplyProvider>
<RichListProvider>
<AccountsProvider>
<BlockProvider>
<EpochProvider>
<MintsProvider>
<TransactionsProvider>
<App />
</TransactionsProvider>
</MintsProvider>
</EpochProvider>
</BlockProvider>
</AccountsProvider>
</RichListProvider>
</SupplyProvider>
</StatsProvider>
</ClusterProvider>
<ScrollAnchorProvider>
<ClusterProvider>
<StatsProvider>
<SupplyProvider>
<RichListProvider>
<AccountsProvider>
<BlockProvider>
<EpochProvider>
<MintsProvider>
<TransactionsProvider>
<App />
</TransactionsProvider>
</MintsProvider>
</EpochProvider>
</BlockProvider>
</AccountsProvider>
</RichListProvider>
</SupplyProvider>
</StatsProvider>
</ClusterProvider>
</ScrollAnchorProvider>
</Router>
);

View File

@ -6,6 +6,8 @@ import { AddressWithContext, programValidator } from "./AddressWithContext";
import { useCluster } from "providers/cluster";
import { getProgramName } from "utils/tx";
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 }) {
return (
@ -30,9 +32,11 @@ function InstructionCard({
const { cluster } = useCluster();
const programId = message.accountKeys[ix.programIdIndex];
const programName = getProgramName(programId.toBase58(), cluster);
const scrollAnchorRef = useScrollAnchor(
getInstructionCardScrollAnchorId([index + 1])
);
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" : ""}`}>
<h3 className="card-header-title mb-0 d-flex align-items-center">
<span className={`badge bg-info-soft me-2`}>#{index + 1}</span>

View File

@ -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]
);
}

View File

@ -373,6 +373,10 @@ pre.json-wrap {
max-width: 36rem;
}
.program-log-instruction-name {
color: $white;
}
.staking-card {
h1 {
margin-bottom: 0.75rem;

View File

@ -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("-")}`;
}

View File

@ -27,6 +27,7 @@ export function pickClusterParams(
return {
...location,
hash: "",
search: newParams.toString(),
};
}