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 { 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) => {
|
||||
|
|
|
@ -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`}>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
.program-log-instruction-name {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.staking-card {
|
||||
h1 {
|
||||
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 {
|
||||
...location,
|
||||
hash: "",
|
||||
search: newParams.toString(),
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue