diff --git a/app/components/common/BaseInstructionCard.tsx b/app/components/common/BaseInstructionCard.tsx index 0c843a33d..76bf708e5 100644 --- a/app/components/common/BaseInstructionCard.tsx +++ b/app/components/common/BaseInstructionCard.tsx @@ -1,5 +1,6 @@ import { Address } from '@components/common/Address'; import { useScrollAnchor } from '@providers/scroll-anchor'; +import { CollapsibleCard } from '@shared/ui/collapsible-card'; import { cn } from '@shared/utils'; import { ParsedInstruction, SignatureResult, TransactionInstruction } from '@solana/web3.js'; import getInstructionCardScrollAnchorId from '@utils/get-instruction-card-scroll-anchor-id'; @@ -46,7 +47,6 @@ export function BaseInstructionCard({ }: InstructionProps) { const [resultClass] = ixResult(result, index); const [showRaw, setShowRaw] = React.useState(defaultRaw || false); - const [expanded, setExpanded] = React.useState(true); const rawClickHandler = () => { if (!defaultRaw && !showRaw && !raw) { // trigger handler to simulate behaviour for the InstructionCard for the transcation which contains logic in it to fetch raw transaction data @@ -59,93 +59,87 @@ export function BaseInstructionCard({ getInstructionCardScrollAnchorId(childIndex != null ? [index + 1, childIndex + 1] : [index + 1]), ); return ( -
-
-

+ #{index + 1} {childIndex !== undefined ? `.${childIndex + 1}` : ''} {title} -

- + + } + headerButtons={
{headerButtons} - {collapsible && ( - - )}
+ } + > +
+ + + {showRaw ? ( + <> + + + + + {'parsed' in ix ? ( + + {raw ? : null} + + ) : ( + + )} + + ) : ( + children + )} + {innerCards && innerCards.length > 0 && ( + <> + + + + + + + + )} + {eventCards && eventCards.length > 0 && ( + <> + + + + + + + + )} + +
Program +
+
Inner Instructions
+ {/* !e-m-0 overrides the 1.5rem margin from inner-cards + so the card aligns with the "Inner Instructions" label above */} +
{innerCards}
+
Events
+
{eventCards}
+
- {expanded && ( -
- - - {showRaw ? ( - <> - - - - - {'parsed' in ix ? ( - - {raw ? : null} - - ) : ( - - )} - - ) : ( - children - )} - {innerCards && innerCards.length > 0 && ( - <> - - - - - - - - )} - {eventCards && eventCards.length > 0 && ( - <> - - - - - - - - )} - -
Program -
-
Inner Instructions
- {/* !e-m-0 overrides the 1.5rem margin from inner-cards - so the card aligns with the "Inner Instructions" label above */} -
{innerCards}
-
Events
-
{eventCards}
-
-
- )} -
+ ); } diff --git a/app/components/common/InspectorInstructionCard.tsx b/app/components/common/InspectorInstructionCard.tsx index 7a7699b9e..57a12f1f3 100644 --- a/app/components/common/InspectorInstructionCard.tsx +++ b/app/components/common/InspectorInstructionCard.tsx @@ -1,5 +1,6 @@ import { ProgramField } from '@entities/instruction-card'; import { useScrollAnchor } from '@providers/scroll-anchor'; +import { CollapsibleCard } from '@shared/ui/collapsible-card'; import { cn } from '@shared/utils'; import { ParsedInstruction, SignatureResult, TransactionInstruction, VersionedMessage } from '@solana/web3.js'; import getInstructionCardScrollAnchorId from '@utils/get-instruction-card-scroll-anchor-id'; @@ -54,24 +55,31 @@ export function InspectorInstructionCard({ ); return ( -
-
-

+ #{index + 1} {childIndex !== undefined ? `.${childIndex + 1}` : ''} {title} -

- + + } + headerButtons={ -
+ } + >
@@ -102,7 +110,7 @@ export function InspectorInstructionCard({
-
+ ); } diff --git a/app/components/inspector/AccountsCard.tsx b/app/components/inspector/AccountsCard.tsx index 24ef888e5..5617fc0ee 100755 --- a/app/components/inspector/AccountsCard.tsx +++ b/app/components/inspector/AccountsCard.tsx @@ -3,7 +3,7 @@ import { ErrorCard } from '@components/common/ErrorCard'; import { TableCardBody } from '@components/common/TableCardBody'; import { type AccountInfo, useAccountsInfo } from '@entities/account'; import { useCluster } from '@providers/cluster'; -import { cn } from '@shared/utils'; +import { CollapsibleCard } from '@shared/ui/collapsible-card'; import { PublicKey, VersionedMessage } from '@solana/web3.js'; import React, { useMemo } from 'react'; @@ -12,7 +12,6 @@ import { toHex } from '@/app/shared/lib/bytes'; import { AddressFromLookupTableWithContext, AddressWithContext } from './AddressWithContext'; export function AccountsCard({ message }: { message: VersionedMessage }) { - const [expanded, setExpanded] = React.useState(true); const { url } = useCluster(); const pubkeys = useMemo(() => message.staticAccountKeys, [message.staticAccountKeys]); @@ -113,32 +112,17 @@ export function AccountsCard({ message }: { message: VersionedMessage }) { } return ( -
-
-

{`Account List (${numAccounts})`}

- -
- {expanded && ( - <> - {accountRows} - {!loading && totalAccountSize > 0 && ( -
-
- - Total Account Size: - - {totalAccountSize.toLocaleString('en-US')} bytes -
-
- )} - + + {accountRows} + {!loading && totalAccountSize > 0 && ( +
+
+ Total Account Size: + {totalAccountSize.toLocaleString('en-US')} bytes +
+
)} -
+ ); } diff --git a/app/components/inspector/AddressTableLookupsCard.tsx b/app/components/inspector/AddressTableLookupsCard.tsx index 37857a25f..318d5a82c 100755 --- a/app/components/inspector/AddressTableLookupsCard.tsx +++ b/app/components/inspector/AddressTableLookupsCard.tsx @@ -1,13 +1,11 @@ import { Address } from '@components/common/Address'; import { useAddressLookupTable } from '@providers/accounts'; import { FetchStatus } from '@providers/cache'; -import { cn } from '@shared/utils'; +import { CollapsibleCard } from '@shared/ui/collapsible-card'; import { PublicKey, VersionedMessage } from '@solana/web3.js'; import React from 'react'; export function AddressTableLookupsCard({ message }: { message: VersionedMessage }) { - const [expanded, setExpanded] = React.useState(true); - const lookupRows = React.useMemo(() => { let key = 0; return message.addressTableLookups.flatMap(lookup => { @@ -32,42 +30,31 @@ export function AddressTableLookupsCard({ message }: { message: VersionedMessage if (message.version === 'legacy') return null; return ( -
-
-

Address Table Lookup(s)

- -
- {expanded && ( -
- - + +
+
+ + + + + + + + + {lookupRows.length > 0 ? ( + {lookupRows} + ) : ( + - - - - + - - {lookupRows.length > 0 ? ( - {lookupRows} - ) : ( - - - - - - )} -
Address Lookup Table AddressTable IndexResolved AddressDetails
Address Lookup Table AddressTable IndexResolved AddressDetails + No entries found +
- No entries found -
-
- )} -
+ + )} + +
+ ); } diff --git a/app/components/inspector/UnknownDetailsCard.tsx b/app/components/inspector/UnknownDetailsCard.tsx index 12cf744d6..a5231ad7d 100644 --- a/app/components/inspector/UnknownDetailsCard.tsx +++ b/app/components/inspector/UnknownDetailsCard.tsx @@ -1,9 +1,8 @@ import { TableCardBody } from '@components/common/TableCardBody'; import { ProgramField } from '@entities/instruction-card'; import { useScrollAnchor } from '@providers/scroll-anchor'; -import { cn } from '@shared/utils'; +import { CollapsibleCard } from '@shared/ui/collapsible-card'; import { TransactionInstruction } from '@solana/web3.js'; -import React from 'react'; import getInstructionCardScrollAnchorId from '@/app/utils/get-instruction-card-scroll-anchor-id'; @@ -18,31 +17,23 @@ export function UnknownDetailsCard({ ix: TransactionInstruction; programName: string; }) { - const [expanded, setExpanded] = React.useState(false); - const scrollAnchorRef = useScrollAnchor(getInstructionCardScrollAnchorId([index + 1])); return ( -
-
-

- #{index + 1} + + #{index + 1} {programName} Instruction -

- - -
- {expanded && ( - - - - - )} -
+ + } + > + + + + + ); } diff --git a/app/components/shared/ui/collapsible-card.stories.tsx b/app/components/shared/ui/collapsible-card.stories.tsx new file mode 100644 index 000000000..a298d3309 --- /dev/null +++ b/app/components/shared/ui/collapsible-card.stories.tsx @@ -0,0 +1,112 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { CollapsibleCard } from './collapsible-card'; + +const meta: Meta = { + argTypes: { + collapsible: { + control: 'boolean', + }, + defaultExpanded: { + control: 'boolean', + }, + }, + component: CollapsibleCard, + decorators: [ + Story => ( +
+ +
+ ), + ], + parameters: { + layout: 'centered', + }, + title: 'Components/Shared/UI/CollapsibleCard', +}; + +export default meta; +type Story = StoryObj; + +const SampleContent = () => ( +
+ + + + + + + + + + + + + + + + + + + + + +
NameValue
Account #1Gzf3…k9Pq
Account #25xRt…mN7v
Account #3BqWu…dL2j
+
+); + +export const Default: Story = { + args: {} as never, + render: () => ( + + + + ), +}; + +export const StartsCollapsed: Story = { + args: {} as never, + render: () => ( + + + + ), +}; + +export const WithHeaderButtons: Story = { + args: {} as never, + render: () => ( + Raw} + > + + + ), +}; + +export const NonCollapsible: Story = { + args: {} as never, + render: () => ( + + + + ), +}; + +export const WithBadgeTitle: Story = { + args: {} as never, + render: () => ( + + #1 + Token Program: Transfer + + } + > + + + ), +}; diff --git a/app/components/shared/ui/collapsible-card.tsx b/app/components/shared/ui/collapsible-card.tsx new file mode 100644 index 000000000..b8f9e9998 --- /dev/null +++ b/app/components/shared/ui/collapsible-card.tsx @@ -0,0 +1,37 @@ +import { cn } from '@shared/utils'; +import React from 'react'; + +type CollapsibleCardProps = { + title: React.ReactNode; + children: React.ReactNode; + defaultExpanded?: boolean; + className?: string; + headerButtons?: React.ReactNode; + collapsible?: boolean; +}; + +export const CollapsibleCard = React.forwardRef( + ({ title, children, defaultExpanded = true, className, headerButtons, collapsible = true }, ref) => { + const [expanded, setExpanded] = React.useState(defaultExpanded); + + return ( +
+
+

{title}

+ {headerButtons} + {collapsible && ( + + )} +
+ {(!collapsible || expanded) && children} +
+ ); + }, +); +CollapsibleCard.displayName = 'CollapsibleCard'; diff --git a/app/components/transaction/AccountsCard.tsx b/app/components/transaction/AccountsCard.tsx index 7eba9a504..10f67df1b 100644 --- a/app/components/transaction/AccountsCard.tsx +++ b/app/components/transaction/AccountsCard.tsx @@ -7,6 +7,7 @@ import { type AccountInfo, useAccountsInfo } from '@entities/account'; import { useCluster } from '@providers/cluster'; import { useTransactionDetails } from '@providers/transactions'; import { Button } from '@shared/ui/button'; +import { CollapsibleCard } from '@shared/ui/collapsible-card'; import { cn } from '@shared/utils'; import { PublicKey } from '@solana/web3.js'; import { SignatureProps } from '@utils/index'; @@ -21,7 +22,6 @@ export function AccountsCard({ signature }: SignatureProps) { const details = useTransactionDetails(signature); const { url } = useCluster(); const [showRaw, setShowRaw] = useState(false); - const [expanded, setExpanded] = useState(true); const transactionWithMeta = details?.data?.transactionWithMeta; const message = transactionWithMeta?.transaction.message; @@ -100,71 +100,64 @@ export function AccountsCard({ signature }: SignatureProps) { }); return ( -
-
-

{`Account Input(s) (${message.accountKeys.length})`}

+ setShowRaw(r => !r)} > Raw - -
- {expanded && - (showRaw ? ( -
- -
- ) : ( -
- - + } + > + {showRaw ? ( +
+ +
+ ) : ( +
+
+ + + + + + + + + + + {accountRows} + {totalAccountSize > 0 && ( + - - - - - - + + + + - - {accountRows} - {totalAccountSize > 0 && ( - - - - - - - - )} -
#AddressChange (SOL)Post Balance (SOL)Size (bytes)Details
#AddressChange (SOL)Post Balance (SOL)Size (bytes)Details +

+ reflects current account state +

+
+

+ Total Account Size: +

+
+ + {totalAccountSize.toLocaleString('en-US')} + +
-

- reflects current account state -

-
-

- Total Account Size: -

-
- - {totalAccountSize.toLocaleString('en-US')} - - -
-
- ))} -
+ + )} + + + )} + ); } diff --git a/app/components/transaction/TokenBalancesCard.tsx b/app/components/transaction/TokenBalancesCard.tsx index 17fb6a16b..14f73c237 100644 --- a/app/components/transaction/TokenBalancesCard.tsx +++ b/app/components/transaction/TokenBalancesCard.tsx @@ -1,7 +1,7 @@ import { Address } from '@components/common/Address'; import { BalanceDelta } from '@components/common/BalanceDelta'; import { useTransactionDetails } from '@providers/transactions'; -import { cn } from '@shared/utils'; +import { CollapsibleCard } from '@shared/ui/collapsible-card'; import { ParsedMessageAccount, PublicKey, TokenBalance } from '@solana/web3.js'; import { SignatureProps } from '@utils/index'; import { BigNumber } from 'bignumber.js'; @@ -54,7 +54,6 @@ export type TokenBalancesCardInnerProps = { export function TokenBalancesCardInner({ rows }: TokenBalancesCardInnerProps) { const { cluster, url } = useCluster(); const [tokenSymbols, setTokenSymbols] = useState>(new Map()); - const [expanded, setExpanded] = useState(true); useAsyncEffect(async isMounted => { const mints = rows.map(r => new PublicKey(r.mint)); @@ -66,40 +65,29 @@ export function TokenBalancesCardInner({ rows }: TokenBalancesCardInnerProps) { }, []); return ( -
-
-

Token Balances

- + +
+ + + + + + + + + + + {rows.map(row => ( + + ))} + +
AddressTokenChangePost Balance
- {expanded && ( -
- - - - - - - - - - - {rows.map(row => ( - - ))} - -
AddressTokenChangePost Balance
-
- )} -
+ ); } diff --git a/app/features/cu-profiling/ui/CUProfilingCard.tsx b/app/features/cu-profiling/ui/CUProfilingCard.tsx index 75cf0c041..e36e02476 100644 --- a/app/features/cu-profiling/ui/CUProfilingCard.tsx +++ b/app/features/cu-profiling/ui/CUProfilingCard.tsx @@ -1,3 +1,4 @@ +import { CollapsibleCard } from '@shared/ui/collapsible-card'; import { InstructionCUData } from '@utils/cu-profiling'; import { BarElement, CategoryScale, Chart, ChartData, ChartOptions, LinearScale, Tooltip } from 'chart.js'; import React from 'react'; @@ -241,10 +242,7 @@ export function CUProfilingCard({ instructions, unitsConsumed }: CUProfilingCard if (instructions.length === 0) return null; return ( -
-
-

CU profiling

-
+
{Boolean(unitsConsumed) &&
Total: {unitsConsumed?.toLocaleString()} CU
} @@ -278,6 +276,6 @@ export function CUProfilingCard({ instructions, unitsConsumed }: CUProfilingCard })}
-
+ ); } diff --git a/bench/BUILD.md b/bench/BUILD.md index d83aae058..7947912c6 100644 --- a/bench/BUILD.md +++ b/bench/BUILD.md @@ -18,7 +18,7 @@ | Dynamic | `/address/[address]/idl` | 130 kB | 1.27 MB | | Dynamic | `/address/[address]/instructions` | 10 kB | 1.13 MB | | Dynamic | `/address/[address]/metadata` | 10 kB | 1.03 MB | -| Dynamic | `/address/[address]/nftoken-collection-nfts` | 10 kB | 1.10 MB | +| Dynamic | `/address/[address]/nftoken-collection-nfts` | 10 kB | 1.08 MB | | Dynamic | `/address/[address]/program-multisig` | 10 kB | 1.08 MB | | Dynamic | `/address/[address]/rewards` | 10 kB | 1.02 MB | | Dynamic | `/address/[address]/security` | 10 kB | 1.08 MB |