Skip to content

Commit b75239b

Browse files
authored
[Transaction Dashboard] Classic Operations (#1813)
1 parent c5e47e9 commit b75239b

File tree

11 files changed

+507
-49
lines changed

11 files changed

+507
-49
lines changed

src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractStorage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useStore } from "@/store/useStore";
66
import { ErrorText } from "@/components/ErrorText";
77
import { Box } from "@/components/layout/Box";
88
import { DataTable } from "@/components/DataTable";
9-
import { ScValPrettyJson } from "@/components/ScValPrettyJson";
9+
import { ScValPrettyJson } from "@/components/StellarDataRenderer";
1010
import { PoweredByStellarExpert } from "@/components/PoweredByStellarExpert";
1111

1212
import { useSEContractStorage } from "@/query/external/useSEContracStorage";
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { useEffect, useState } from "react";
2+
import { Badge, Button, Card, Heading, Icon } from "@stellar/design-system";
3+
4+
import { ClassicOpPrettyJson } from "@/components/StellarDataRenderer";
5+
6+
import { Box } from "@/components/layout/Box";
7+
8+
const PAGE_SIZE = 5;
9+
10+
type ClassicOperation = {
11+
body: any;
12+
};
13+
14+
export const ClassicOperations = ({
15+
operations,
16+
}: {
17+
operations: ClassicOperation[];
18+
}) => {
19+
// Pagination
20+
const [currentPage, setCurrentPage] = useState(1);
21+
const [totalPageCount, setTotalPageCount] = useState(1);
22+
23+
useEffect(() => {
24+
setTotalPageCount(Math.ceil(operations.length / PAGE_SIZE));
25+
}, [operations.length]);
26+
27+
// @TODO to be replaced with an empty state component
28+
if (!operations.length) {
29+
return (
30+
<div className="TransactionClassicOperations">
31+
<Card>
32+
<Box gap="lg">
33+
<Heading size="sm" as="h4">
34+
No operations found
35+
</Heading>
36+
</Box>
37+
</Card>
38+
</div>
39+
);
40+
}
41+
42+
// Calculate pagination
43+
const startIndex = (currentPage - 1) * PAGE_SIZE;
44+
const endIndex = startIndex + PAGE_SIZE;
45+
const paginatedOperations = operations.slice(startIndex, endIndex);
46+
47+
const handlePrevPage = () => {
48+
setCurrentPage((prev) => Math.max(1, prev - 1));
49+
};
50+
51+
const handleNextPage = () => {
52+
setCurrentPage((prev) => Math.min(totalPageCount, prev + 1));
53+
};
54+
55+
const handleFirstPage = () => {
56+
setCurrentPage(1);
57+
};
58+
59+
const handleLastPage = () => {
60+
setCurrentPage(totalPageCount);
61+
};
62+
63+
return (
64+
<div className="TransactionClassicOperations">
65+
<Card>
66+
<Box gap="lg">
67+
<Heading as="h2" size="xs" weight="medium">
68+
Operations
69+
</Heading>
70+
71+
{paginatedOperations.map((operation, index) => {
72+
const actualIndex = startIndex + index;
73+
74+
// Check if operation.body is a string (e.g., "end_sponsoring_future_reserves")
75+
const isBodyString = typeof operation.body === "string";
76+
77+
const operationType: string = isBodyString
78+
? operation.body
79+
: Object.keys(operation.body)[0];
80+
81+
const operationData = isBodyString
82+
? null
83+
: operation.body[operationType];
84+
85+
const isPrimitive = typeof operationData !== "object";
86+
87+
return (
88+
<div
89+
key={actualIndex}
90+
className="TransactionClassicOperations__operation"
91+
>
92+
<Card>
93+
<Box gap="md">
94+
<Box
95+
gap="md"
96+
direction="row"
97+
align="center"
98+
justify="space-between"
99+
>
100+
<Badge variant="secondary">{operationType}</Badge>
101+
</Box>
102+
{operationData !== null && (
103+
<Card variant="secondary">
104+
<div className="TransactionClassicOperations__operationDetails">
105+
{isPrimitive ? (
106+
<InfoField label="value" value={operationData} />
107+
) : (
108+
Object.keys(operationData).map((val, idx) => (
109+
<InfoField
110+
label={val}
111+
value={operationData[val]}
112+
key={idx}
113+
/>
114+
))
115+
)}
116+
</div>
117+
</Card>
118+
)}
119+
</Box>
120+
</Card>
121+
</div>
122+
);
123+
})}
124+
125+
{/* Pagination Controls */}
126+
{totalPageCount > 1 && (
127+
<Box
128+
gap="md"
129+
direction="row"
130+
align="center"
131+
wrap="wrap"
132+
justify="end"
133+
>
134+
<Box gap="xs" direction="row" align="center">
135+
{/* First page */}
136+
<Button
137+
variant="tertiary"
138+
size="sm"
139+
onClick={handleFirstPage}
140+
disabled={currentPage === 1}
141+
>
142+
First
143+
</Button>
144+
145+
{/* Previous page */}
146+
<Button
147+
variant="tertiary"
148+
size="sm"
149+
icon={<Icon.ArrowLeft />}
150+
onClick={handlePrevPage}
151+
disabled={currentPage === 1}
152+
/>
153+
154+
{/* Page count */}
155+
<div className="DataTable__pagination">{`Page ${currentPage} of ${totalPageCount}`}</div>
156+
157+
{/* Next page */}
158+
<Button
159+
variant="tertiary"
160+
size="sm"
161+
icon={<Icon.ArrowRight />}
162+
onClick={handleNextPage}
163+
disabled={currentPage === totalPageCount}
164+
/>
165+
166+
{/* Last page */}
167+
<Button
168+
variant="tertiary"
169+
size="sm"
170+
onClick={handleLastPage}
171+
disabled={currentPage === totalPageCount}
172+
>
173+
Last
174+
</Button>
175+
</Box>
176+
</Box>
177+
)}
178+
</Box>
179+
</Card>
180+
</div>
181+
);
182+
};
183+
184+
const InfoField = ({ label, value }: { label: string; value: any }) => {
185+
return (
186+
<Box gap="xs" direction="row" addlClassName="InfoFieldItem">
187+
<div className="InfoFieldItem__label">{getLabel(label)}</div>
188+
<div className="InfoFieldItem__value">
189+
<ClassicOpPrettyJson value={value} />
190+
</div>
191+
</Box>
192+
);
193+
};
194+
195+
// =============================================================================
196+
// Helpers
197+
// =============================================================================
198+
199+
// Capitalizes and adds spaces to labels
200+
const getLabel = (label: string) => {
201+
const withSpaces = label.replace(/_/g, " ");
202+
return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1);
203+
};

src/app/(sidebar)/transaction-dashboard/components/Events.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Badge, Button, Card, Icon, Text } from "@stellar/design-system";
55
import { stringify } from "lossless-json";
66

77
import { Box } from "@/components/layout/Box";
8-
import { ScValPrettyJson } from "@/components/ScValPrettyJson";
8+
import { ScValPrettyJson } from "@/components/StellarDataRenderer";
99
import { ExpandBox } from "@/components/ExpandBox";
1010
import { CopyJsonPayloadButton } from "@/components/CopyJsonPayloadButton";
1111
import { TransactionTabEmptyMessage } from "@/components/TransactionTabEmptyMessage";

src/app/(sidebar)/transaction-dashboard/page.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { Events } from "./components/Events";
3131
import { TokenSummary } from "./components/TokenSummary";
3232
import { Contracts } from "./components/Contracts";
3333
import { ResourceProfiler } from "./components/ResourceProfiler";
34+
import { ClassicOperations } from "./components/ClassicOperations";
3435

3536
import "./styles.scss";
3637

@@ -74,7 +75,7 @@ export default function TransactionDashboard() {
7475
const isLoading = isTxDetailsLoading || isTxDetailsFetching;
7576
const isDataLoaded = Boolean(txDetails);
7677

77-
const { isSorobanTx } = getTxData(txDetails || null);
78+
const { isSorobanTx, operations } = getTxData(txDetails || null);
7879
const isTxNotFound = txDetails?.status === "NOT_FOUND";
7980

8081
const queryClient = useQueryClient();
@@ -292,6 +293,10 @@ export default function TransactionDashboard() {
292293
isTxNotFound={isTxNotFound}
293294
/>
294295

296+
{!isSorobanTx && operations ? (
297+
<ClassicOperations operations={operations} />
298+
) : null}
299+
295300
{isSorobanTx ? (
296301
<Card>
297302
<Box

src/app/(sidebar)/transaction-dashboard/styles.scss

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,3 +513,33 @@
513513
}
514514
}
515515
}
516+
517+
// =============================================================================
518+
// Transaction Dashboard - Classic Operations
519+
// =============================================================================
520+
.TransactionClassicOperations {
521+
&__operation {
522+
display: flex;
523+
flex-direction: column;
524+
525+
.Card {
526+
--Card-padding: #{pxToRem(14px)};
527+
}
528+
}
529+
530+
h2 {
531+
font-size: pxToRem(16px);
532+
line-height: pxToRem(24px);
533+
}
534+
535+
&__operationDetails {
536+
display: grid;
537+
gap: pxToRem(6px);
538+
}
539+
540+
.InfoFieldItem {
541+
&__label {
542+
font-weight: var(--sds-fw-medium);
543+
}
544+
}
545+
}

0 commit comments

Comments
 (0)