Skip to content

Commit b359eda

Browse files
committed
Refactor panel styles and enhance transaction details UI
- Removed inline margin styles from panels in CollectionPage and NftPage for consistency. - Added a new CentauriNodeGraph component to visualize node work and transactions on the Home page. - Updated the Home page layout to include new links for Transactions and Accounts. - Improved the transaction details panel by adding a copy button for transaction hashes and sender addresses. - Enhanced the display of sender addresses in the transaction panel for better user experience.
1 parent c9a395a commit b359eda

11 files changed

Lines changed: 544 additions & 143 deletions

File tree

crates/kanari-rpc-api/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ pub struct TransactionDetails {
220220
pub gas_used: Option<u64>,
221221
pub tx_type: String,
222222
pub sender: String,
223+
#[serde(skip_serializing_if = "Option::is_none")]
224+
pub sender_address: Option<String>,
223225
pub sequence_number: u64,
224226
pub gas_limit: u64,
225227
pub gas_price: u64,

crates/kanari-rpc-server/src/transaction/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ fn base_transaction_details(
159159
block_height: Option<u64>,
160160
tx_type: &str,
161161
sender: String,
162+
sender_address: String,
162163
sequence_number: u64,
163164
gas_limit: u64,
164165
gas_price: u64,
@@ -170,6 +171,7 @@ fn base_transaction_details(
170171
gas_used: None,
171172
tx_type: tx_type.to_string(),
172173
sender,
174+
sender_address: Some(sender_address),
173175
sequence_number,
174176
gas_limit,
175177
gas_price,
@@ -192,6 +194,9 @@ fn map_transaction_to_details(
192194
) -> TransactionDetails {
193195
let hash = format!("0x{}", tx_hash_hex);
194196
let status_str = status.to_string();
197+
let sender_address = Address::parse_to_account_address(tx.sender_address())
198+
.map(|addr| addr.to_hex_literal())
199+
.unwrap_or_else(|_| tx.sender_address().to_string());
195200

196201
match tx {
197202
Transaction::PublishModule {
@@ -218,6 +223,7 @@ fn map_transaction_to_details(
218223
block_height,
219224
"publish_module",
220225
sender.clone(),
226+
sender_address.clone(),
221227
*sequence_number,
222228
*gas_limit,
223229
*gas_price,
@@ -241,6 +247,7 @@ fn map_transaction_to_details(
241247
block_height,
242248
tx.tx_type_label(),
243249
sender.clone(),
250+
sender_address.clone(),
244251
*sequence_number,
245252
*gas_limit,
246253
*gas_price,

kanariexplorer/app/account/page.tsx

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useSearchParams } from "next/navigation";
55
import TransactionDetailsModal from "../components/TransactionDetailsModal";
66
import {
77
asArray,
8+
CopyButton,
89
EmptyState,
910
formatBalance,
1011
PageHeader,
@@ -18,6 +19,18 @@ import { getAccount, getAllBalances, getAllTransactions, getOwnedNfts, getOwnedO
1819

1920
type AccountTab = "coins" | "nfts" | "objects" | "activity";
2021

22+
function readBytes(value: unknown, key: string) {
23+
const record = typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
24+
const item = record[key];
25+
if (!Array.isArray(item)) return [];
26+
return item.filter((entry): entry is number => typeof entry === "number" && Number.isFinite(entry));
27+
}
28+
29+
function formatHex(bytes: number[]) {
30+
if (bytes.length === 0) return "-";
31+
return `0x${bytes.map((byte) => Math.max(0, Math.min(255, byte)).toString(16).padStart(2, "0")).join("")}`;
32+
}
33+
2134
function AccountContent() {
2235
const searchParams = useSearchParams();
2336
const [address, setAddress] = useState(searchParams.get("address") ?? "");
@@ -91,11 +104,16 @@ function AccountContent() {
91104
</PageHeader>
92105

93106
{account ? (
94-
<section className="panel" style={{ marginTop: 18 }}>
107+
<section className="panel account-state-panel">
95108
<div className="panel-head">
96109
<div>
97110
<h2 className="panel-title">Account State</h2>
98-
<p className="panel-subtitle mono">{readString(account, "address", address)}</p>
111+
<div className="panel-subtitle mono account-address-copy">
112+
<span className="copy-row copy-row--wrap">
113+
<span className="break-anywhere">{readString(account, "address", address)}</span>
114+
<CopyButton value={readString(account, "address", address)} label="Copy account address" />
115+
</span>
116+
</div>
99117
</div>
100118
<StatusPill label={`Sequence ${readString(account, "sequence_number", "0")}`} />
101119
</div>
@@ -174,17 +192,20 @@ function AccountContent() {
174192
<div className="data-row" key={`${hash}-${index}`}>
175193
<div>
176194
<p className="tiny-label">Txn Hash</p>
177-
<button className="hash-button mono" type="button" onClick={() => openTransaction(hash)}>
178-
{shortHash(hash)}
179-
</button>
195+
<span className="copy-row copy-row--wrap">
196+
<button className="hash-button mono break-anywhere" type="button" onClick={() => openTransaction(hash)}>
197+
{hash}
198+
</button>
199+
<CopyButton value={hash} label="Copy transaction hash" />
200+
</span>
180201
</div>
181202
<div>
182203
<p className="tiny-label">Type</p>
183204
<span className="tag">{readString(transaction, "tx_type", "operation")}</span>
184205
</div>
185206
<div>
186207
<p className="tiny-label">Target</p>
187-
<span className="mono muted-text">{readString(transaction, "module", "-")}</span>
208+
<span className="mono muted-text">{shortHash(readString(transaction, "module", "-"))}</span>
188209
</div>
189210
<div>
190211
<p className="tiny-label">Status</p>
@@ -206,24 +227,53 @@ function AccountContent() {
206227
<div className="data-list">
207228
{objects.map((object, index) => {
208229
const objectId = readString(object, "object_id", readString(object, "id", `object-${index}`));
230+
const objectType = readString(object, "type_", readString(object, "type", readString(object, "object_type", "-")));
231+
const owner = readString(object, "owner", address);
232+
const dataBytes = readBytes(object, "data");
233+
const objectJson =
234+
object && typeof object === "object" && !Array.isArray(object)
235+
? { ...(object as Record<string, unknown>), data_hex: formatHex(dataBytes) }
236+
: { value: object, data_hex: formatHex(dataBytes) };
209237
return (
210-
<div className="data-row" key={`${objectId}-${index}`}>
211-
<div className="primary-text">
212-
<strong className="mono">{shortHash(objectId)}</strong>
213-
<div className="muted-text mono">{readString(object, "type", readString(object, "object_type", "object"))}</div>
214-
</div>
215-
<div>
216-
<p className="tiny-label">Owner</p>
217-
<span className="mono muted-text">{shortHash(readString(object, "owner", address))}</span>
238+
<div className="data-row data-row--objects" key={`${objectId}-${index}`}>
239+
<div className="object-main primary-text">
240+
<p className="tiny-label">Object ID</p>
241+
<span className="copy-row copy-row--inline">
242+
<strong className="mono break-anywhere">{objectId}</strong>
243+
<CopyButton value={objectId} label="Copy object id" />
244+
</span>
218245
</div>
219-
<div>
220-
<p className="tiny-label">Version</p>
221-
<span className="mono">{readString(object, "version", "-")}</span>
222-
</div>
223-
<div>
224-
<p className="tiny-label">Status</p>
225-
<StatusPill label={readString(object, "status", "owned")} />
246+
247+
<div className="object-detail-grid">
248+
<div className="object-detail-field object-detail-field--wide">
249+
<p className="tiny-label">Type</p>
250+
<span className="mono muted-text break-anywhere">{objectType}</span>
251+
</div>
252+
<div className="object-detail-field object-detail-field--wide">
253+
<p className="tiny-label">Owner</p>
254+
<span className="copy-row copy-row--inline">
255+
<span className="mono muted-text break-anywhere">{owner}</span>
256+
<CopyButton value={owner} label="Copy owner address" />
257+
</span>
258+
</div>
259+
<div className="object-detail-field">
260+
<p className="tiny-label">Version</p>
261+
<span className="mono">{readString(object, "version", "-")}</span>
262+
</div>
263+
<div className="object-detail-field">
264+
<p className="tiny-label">Data Bytes</p>
265+
<span className="mono">{dataBytes.length.toLocaleString()}</span>
266+
</div>
267+
<div className="object-detail-field">
268+
<p className="tiny-label">Status</p>
269+
<StatusPill label={readString(object, "status", "owned")} />
270+
</div>
226271
</div>
272+
273+
<details className="object-json-details">
274+
<summary>Object JSON</summary>
275+
<pre className="custom-scrollbar">{JSON.stringify(objectJson, null, 2)}</pre>
276+
</details>
227277
</div>
228278
);
229279
})}

kanariexplorer/app/coins/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ function CoinsContent() {
7575
<SearchForm value={address} onChange={setAddress} onSubmit={() => loadBalances(address)} placeholder="Filter by address" buttonLabel="View" />
7676
</PageHeader>
7777

78-
<section className="panel" style={{ marginTop: 18 }}>
78+
<section className="panel">
7979
<div className="panel-head">
8080
<div>
8181
<h2 className="panel-title">{address ? "Address Balances" : "Token Registry"}</h2>

kanariexplorer/app/components/ExplorerUI.tsx

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Link from "next/link";
2-
import type { ReactNode } from "react";
2+
import type { MouseEvent, ReactNode } from "react";
33

44
export type DataRecord = Record<string, unknown>;
55

@@ -22,6 +22,12 @@ export function readString(value: unknown, key: string, fallback = "-") {
2222
return fallback;
2323
}
2424

25+
export function readAddress(value: unknown, primaryKey: string, fallbackKey?: string, fallback = "-") {
26+
const primary = readString(value, primaryKey, "");
27+
if (primary) return primary;
28+
return fallbackKey ? readString(value, fallbackKey, fallback) : fallback;
29+
}
30+
2531
export function readNumber(value: unknown, key: string) {
2632
const item = asRecord(value)[key];
2733
if (typeof item === "number") return item;
@@ -73,21 +79,19 @@ export function PageHeader({
7379
children?: ReactNode;
7480
}) {
7581
return (
76-
<section className="page-head">
77-
<div className="page-head__content">
78-
<p className="eyebrow">{eyebrow}</p>
79-
<h1 className="page-title">
80-
{title}
81-
{accent ? (
82-
<>
83-
<br />
84-
<span>{accent}</span>
85-
</>
86-
) : null}
87-
</h1>
88-
<p className="page-description">{description}</p>
89-
{children ? <div className="toolbar">{children}</div> : null}
90-
</div>
82+
<section className="subpage-hero explorer-page-hero">
83+
<p className="section-kicker">{eyebrow}</p>
84+
<h1>
85+
{title}
86+
{accent ? (
87+
<>
88+
<br />
89+
<span>{accent}</span>
90+
</>
91+
) : null}
92+
</h1>
93+
<p className="subpage-hero__description">{description}</p>
94+
{children ? <div className="hero-actions explorer-page-actions">{children}</div> : null}
9195
</section>
9296
);
9397
}
@@ -128,9 +132,9 @@ export function SearchForm({
128132

129133
export function StatCard({ label, value, detail }: { label: string; value: string; detail: string }) {
130134
return (
131-
<article className="panel stat-card">
132-
<span className="stat-card__label">{label}</span>
133-
<strong className="stat-card__value">{value}</strong>
135+
<article className="stat-card">
136+
<strong>{value}</strong>
137+
<span>{label}</span>
134138
<p className="stat-card__detail">{detail}</p>
135139
</article>
136140
);
@@ -165,7 +169,7 @@ export function StatusPill({ label, state = "ok" }: { label: string; state?: "ok
165169
return (
166170
<span className="status-pill">
167171
<span className={`dot ${state === "warn" ? "dot--warn" : state === "down" ? "dot--down" : ""}`} />
168-
{label}
172+
<span className="status-pill__label">{label}</span>
169173
</span>
170174
);
171175
}
@@ -179,6 +183,28 @@ export function EmptyState({ loading, label }: { loading?: boolean; label: strin
179183
);
180184
}
181185

186+
export function CopyButton({ value, label = "Copy" }: { value: string; label?: string }) {
187+
async function copyValue(event: MouseEvent<HTMLButtonElement>) {
188+
event.preventDefault();
189+
event.stopPropagation();
190+
191+
try {
192+
await navigator.clipboard.writeText(value);
193+
} catch {
194+
// Clipboard permissions can be blocked in non-secure contexts.
195+
}
196+
}
197+
198+
return (
199+
<button className="copy-button" type="button" onClick={copyValue} aria-label={label} title={label}>
200+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
201+
<path d="M8 8h10v12H8z" />
202+
<path d="M6 16H4V4h12v2" />
203+
</svg>
204+
</button>
205+
);
206+
}
207+
182208
export function RawDetails({ label, value }: { label: string; value: unknown }) {
183209
return (
184210
<details className="raw-details">

kanariexplorer/app/components/TransactionDetailsModal.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EmptyState, RawDetails, readString, shortHash } from "./ExplorerUI";
1+
import { EmptyState, RawDetails, readAddress, readString, shortHash } from "./ExplorerUI";
22

33
function readFirstString(value: unknown, keys: string[], fallback = "-") {
44
for (const key of keys) {
@@ -41,6 +41,7 @@ export default function TransactionDetailsModal({
4141

4242
const hash = readFirstString(transaction, ["hash", "tx_hash"]);
4343
const status = readFirstString(transaction, ["status"], "unknown");
44+
const senderAddress = readAddress(transaction, "sender_address", "sender");
4445
const moduleFunctions = readOptionalArray(transaction, "module_functions");
4546

4647
return (
@@ -78,8 +79,8 @@ export default function TransactionDetailsModal({
7879
<section className="tx-detail-grid" aria-label="Transaction fields">
7980
<DetailItem label="Type" value={readFirstString(transaction, ["tx_type", "type"], "operation").replace(/_/g, " ")} />
8081
<DetailItem label="Block Height" value={readFirstString(transaction, ["block_height", "height"])} mono />
81-
<DetailItem label="Sender" value={readFirstString(transaction, ["sender", "from"])} mono wide />
82-
<DetailItem label="Recipient / Target" value={readFirstString(transaction, ["recipient", "to", "module"])} mono wide />
82+
<DetailItem label="Sender" value={senderAddress} mono wide />
83+
<DetailItem label="Recipient / Target" value={shortHash(readFirstString(transaction, ["recipient", "to", "module"]))} mono wide />
8384
<DetailItem label="Function" value={readFirstString(transaction, ["function"])} mono />
8485
<DetailItem label="Sequence" value={readFirstString(transaction, ["sequence_number", "sequence", "nonce"])} mono />
8586
<DetailItem label="Gas Limit" value={readFirstString(transaction, ["gas_limit", "gas"])} mono />

0 commit comments

Comments
 (0)