Skip to content

Commit

Permalink
Contract Explorer: Contract storage (#1227)
Browse files Browse the repository at this point in the history
* Contract storage basics

* Version History as default tab

* Table pagination

* Format sc key and value

* Data type legend with colors

* Handle React keys

* Add UI tests
  • Loading branch information
quietbits authored Jan 24, 2025
1 parent 05893b0 commit 92b4b69
Show file tree
Hide file tree
Showing 17 changed files with 1,446 additions and 246 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tslib": "^2.7.0",
"uuid": "^11.0.5",
"zustand": "^4.5.5",
"zustand-querystring": "^0.0.19"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { useState } from "react";
import { Avatar, Badge, Card, Icon, Logo, Text } from "@stellar/design-system";
import {
Avatar,
Badge,
Card,
Icon,
Link,
Logo,
Text,
} from "@stellar/design-system";

import { Box } from "@/components/layout/Box";
import { SdsLink } from "@/components/SdsLink";
Expand All @@ -10,6 +18,7 @@ import { stellarExpertAccountLink } from "@/helpers/stellarExpertAccountLink";

import { ContractInfoApiResponse, NetworkType } from "@/types/types";

import { ContractStorage } from "./ContractStorage";
import { VersionHistory } from "./VersionHistory";

export const ContractInfo = ({
Expand All @@ -19,7 +28,16 @@ export const ContractInfo = ({
infoData: ContractInfoApiResponse;
networkId: NetworkType;
}) => {
const [activeTab, setActiveTab] = useState("contract-version-history");
type ContractTabId =
| "contract-bindings"
| "contract-contract-info"
| "contract-source-code"
| "contract-contract-storage"
| "contract-version-history";

const [activeTab, setActiveTab] = useState<ContractTabId>(
"contract-version-history",
);

type ContractExplorerInfoField = {
id: string;
Expand Down Expand Up @@ -158,11 +176,16 @@ export const ContractInfo = ({
<InfoFieldItem
key={field.id}
label={field.label}
// TODO: link to Contract Storage tab when ready
value={
infoData.storage_entries
? formatEntriesText(infoData.storage_entries)
: null
infoData.storage_entries ? (
<Link
onClick={() => {
setActiveTab("contract-contract-storage");
}}
>
{formatEntriesText(infoData.storage_entries)}
</Link>
) : null
}
/>
);
Expand Down Expand Up @@ -226,21 +249,29 @@ export const ContractInfo = ({
tab4={{
id: "contract-contract-storage",
label: "Contract Storage",
content: <ComingSoonText />,
content: (
<ContractStorage
isActive={activeTab === "contract-contract-storage"}
contractId={infoData.contract}
networkId={networkId}
totalEntriesCount={infoData.storage_entries}
/>
),
}}
tab5={{
id: "contract-version-history",
label: "Version History",
content: (
<VersionHistory
isActive={activeTab === "contract-version-history"}
contractId={infoData.contract}
networkId={networkId}
/>
),
}}
activeTabId={activeTab}
onTabChange={(tabId) => {
setActiveTab(tabId);
setActiveTab(tabId as ContractTabId);
}}
/>
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Loader, Text } from "@stellar/design-system";

import { ErrorText } from "@/components/ErrorText";
import { Box } from "@/components/layout/Box";
import { DataTable } from "@/components/DataTable";
import { ScValPrettyJson } from "@/components/ScValPrettyJson";

import { useSEContractStorage } from "@/query/external/useSEContracStorage";
import { formatEpochToDate } from "@/helpers/formatEpochToDate";
import { formatNumber } from "@/helpers/formatNumber";
import { capitalizeString } from "@/helpers/capitalizeString";

import { useIsXdrInit } from "@/hooks/useIsXdrInit";

import { ContractStorageResponseItem, NetworkType } from "@/types/types";

export const ContractStorage = ({
isActive,
contractId,
networkId,
totalEntriesCount,
}: {
isActive: boolean;
contractId: string;
networkId: NetworkType;
totalEntriesCount: number | undefined;
}) => {
const isXdrInit = useIsXdrInit();

const {
data: storageData,
error: storageError,
isLoading: isStorageLoading,
isFetching: isStorageFetching,
} = useSEContractStorage({
isActive,
networkId,
contractId,
totalEntriesCount,
});

// Loading, error, and no data states
if (isStorageLoading || isStorageFetching) {
return (
<Box gap="sm" direction="row" justify="center">
<Loader />
</Box>
);
}

if (storageError) {
return <ErrorText errorMessage={storageError.toString()} size="sm" />;
}

if (!storageData || storageData.length === 0) {
return (
<Text as="div" size="sm">
No contract storage
</Text>
);
}

return (
<DataTable
tableId="contract-storage"
tableData={storageData}
tableHeaders={[
{ id: "key", value: "Key", isSortable: false },
{ id: "value", value: "Value", isSortable: false },
{ id: "durability", value: "Durability", isSortable: true },
{ id: "ttl", value: "TTL", isSortable: true },
{ id: "updated", value: "Updated", isSortable: true },
]}
formatDataRow={(vh: ContractStorageResponseItem) => [
{
value: (
<div className="CodeBox">
<ScValPrettyJson xdrString={vh.key} isReady={isXdrInit} />
</div>
),
},
{
value: (
<div className="CodeBox">
<ScValPrettyJson xdrString={vh.value} isReady={isXdrInit} />
</div>
),
},
{ value: capitalizeString(vh.durability) },
{ value: formatNumber(vh.ttl) },
{ value: formatEpochToDate(vh.updated, "short") || "-" },
]}
cssGridTemplateColumns="minmax(210px, 2fr) minmax(210px, 2fr) minmax(130px, 1fr) minmax(130px, 1fr) minmax(210px, 1fr)"
customFooterEl={
<Box gap="sm" direction="row" align="center">
{["sym", "i128", "u32", "bool"].map((t) => (
<div
className="DataTypeLegend"
data-type={t}
key={`legend-type-${t}`}
></div>
))}
</Box>
}
/>
);
};
Loading

0 comments on commit 92b4b69

Please sign in to comment.