mirror of
https://github.com/luxfi/explore.git
synced 2026-07-27 05:54:15 +00:00
MUD
This commit is contained in:
@@ -13,6 +13,7 @@ on:
|
||||
- arbitrum
|
||||
- base
|
||||
- celo_alfajores
|
||||
- garnet
|
||||
- gnosis
|
||||
- eth
|
||||
- eth_sepolia
|
||||
|
||||
Vendored
+1
@@ -362,6 +362,7 @@
|
||||
"arbitrum",
|
||||
"base",
|
||||
"celo_alfajores",
|
||||
"garnet",
|
||||
"gnosis",
|
||||
"eth",
|
||||
"eth_goerli",
|
||||
|
||||
@@ -17,6 +17,7 @@ export { default as growthBook } from './growthBook';
|
||||
export { default as marketplace } from './marketplace';
|
||||
export { default as metasuites } from './metasuites';
|
||||
export { default as mixpanel } from './mixpanel';
|
||||
export { default as mudFramework } from './mudFramework';
|
||||
export { default as multichainButton } from './multichainButton';
|
||||
export { default as nameService } from './nameService';
|
||||
export { default as publicTagsSubmission } from './publicTagsSubmission';
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import { getEnvValue } from '../utils';
|
||||
import rollup from './rollup';
|
||||
|
||||
const title = 'MUD framework';
|
||||
|
||||
const config: Feature<{ isEnabled: true }> = (() => {
|
||||
if (rollup.isEnabled && rollup.type === 'optimistic' && getEnvValue('NEXT_PUBLIC_HAS_MUD_FRAMEWORK') === 'true') {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: false,
|
||||
});
|
||||
})();
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,50 @@
|
||||
# Set of ENVs for Garnet (dev only)
|
||||
# https://https://explorer.garnetchain.com//
|
||||
|
||||
# app configuration
|
||||
NEXT_PUBLIC_APP_PROTOCOL=http
|
||||
NEXT_PUBLIC_APP_HOST=localhost
|
||||
NEXT_PUBLIC_APP_PORT=3000
|
||||
|
||||
# blockchain parameters
|
||||
NEXT_PUBLIC_NETWORK_NAME="Garnet Testnet"
|
||||
NEXT_PUBLIC_NETWORK_ID=17069
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
|
||||
NEXT_PUBLIC_NETWORK_RPC_URL=https://partner-rpc.garnetchain.com/tireless-strand-dreamt-overcome
|
||||
|
||||
# api configuration
|
||||
NEXT_PUBLIC_API_HOST=explorer.garnetchain.com
|
||||
NEXT_PUBLIC_API_BASE_PATH=/
|
||||
|
||||
# ui config
|
||||
## homepage
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
|
||||
## views
|
||||
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
|
||||
# app features
|
||||
NEXT_PUBLIC_APP_INSTANCE=local
|
||||
NEXT_PUBLIC_APP_ENV=development
|
||||
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
|
||||
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
|
||||
NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login
|
||||
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
|
||||
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/redstone-testnet.json
|
||||
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/redstone.json
|
||||
NEXT_PUBLIC_AD_BANNER_PROVIDER=none
|
||||
## sidebar
|
||||
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/garnet.svg
|
||||
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/garnet.svg
|
||||
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/garnet-dark.svg
|
||||
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/garnet-dark.svg
|
||||
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND="rgb(169, 31, 47)"
|
||||
NEXT_PUBLIC_OG_DESCRIPTION="Redstone is the home for onchain games, worlds, and other MUD applications"
|
||||
# rollup
|
||||
NEXT_PUBLIC_ROLLUP_TYPE=optimistic
|
||||
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-holesky.blockscout.com/
|
||||
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://garnet.qry.live/withdraw
|
||||
NEXT_PUBLIC_HAS_MUD_FRAMEWORK=true
|
||||
@@ -675,6 +675,16 @@ const schema = yup
|
||||
value => value === undefined,
|
||||
),
|
||||
}),
|
||||
NEXT_PUBLIC_HAS_MUD_FRAMEWORK: yup.boolean()
|
||||
.when('NEXT_PUBLIC_ROLLUP_TYPE', {
|
||||
is: 'optimistic',
|
||||
then: (schema) => schema,
|
||||
otherwise: (schema) => schema.test(
|
||||
'not-exist',
|
||||
'NEXT_PUBLIC_HAS_MUD_FRAMEWORK can only be used with NEXT_PUBLIC_ROLLUP_TYPE=optimistic',
|
||||
value => value === undefined,
|
||||
),
|
||||
}),
|
||||
|
||||
// 6. External services envs
|
||||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
|
||||
|
||||
@@ -406,6 +406,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
|
||||
| NEXT_PUBLIC_ROLLUP_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | v1.24.0+ |
|
||||
| NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals (Optimistic stack only) | Required for `optimistic` rollups | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0+ |
|
||||
| NEXT_PUBLIC_FAULT_PROOF_ENABLED | `boolean` | Set to `true` for chains with fault proof system enabled (Optimistic stack only) | - | - | `true` | v1.31.0+ |
|
||||
| NEXT_PUBLIC_HAS_MUD_FRAMEWORK | `boolean` | Set to `true` for instances that use MUD framework (Optimistic stack only) | - | - | `true` | - |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path fill="currentColor" d="M3.933 17.138a3.98 3.98 0 0 1-1.03-.136 4.001 4.001 0 0 1-2.771-4.866L1.796 5.82a3.958 3.958 0 0 1 1.85-2.45 3.88 3.88 0 0 1 2.966-.386A3.944 3.944 0 0 1 9.07 4.992h1.857a3.922 3.922 0 0 1 2.458-2.008 3.88 3.88 0 0 1 2.965.386 3.977 3.977 0 0 1 1.85 2.436l1.665 6.33a4 4 0 0 1-2.779 4.88 3.879 3.879 0 0 1-2.965-.386 3.959 3.959 0 0 1-1.85-2.436l-.13-.629H7.856l-.164.615a3.958 3.958 0 0 1-1.85 2.436 3.9 3.9 0 0 1-1.908.522ZM5.59 4.278a2.494 2.494 0 0 0-1.229.328A2.572 2.572 0 0 0 3.175 6.17l-1.664 6.316a2.572 2.572 0 0 0 1.771 3.137 2.45 2.45 0 0 0 1.872-.243 2.53 2.53 0 0 0 1.186-1.565l.443-1.679h6.43l.436 1.68a2.558 2.558 0 0 0 1.187 1.564 2.47 2.47 0 0 0 1.879.243 2.558 2.558 0 0 0 1.764-3.137L16.814 6.17a2.535 2.535 0 0 0-1.178-1.564 2.472 2.472 0 0 0-1.88-.243 2.536 2.536 0 0 0-1.693 1.586l-.171.472H8.105l-.171-.472a2.544 2.544 0 0 0-1.7-1.586 2.486 2.486 0 0 0-.644-.086Z"/>
|
||||
<path fill="currentColor" d="M5.712 9.993a1.429 1.429 0 1 1 0-2.858 1.429 1.429 0 0 1 0 2.858Zm8.573-2.143a.714.714 0 1 0 0-1.43.714.714 0 0 0 0 1.43Zm0 2.857a.714.714 0 1 0 0-1.429.714.714 0 0 0 0 1.43Zm-1.428-1.429a.714.714 0 1 0 0-1.428.714.714 0 0 0 0 1.428Zm2.857 0a.714.714 0 1 0 0-1.428.714.714 0 0 0 0 1.428Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -32,6 +32,12 @@ import type {
|
||||
AddressCollectionsResponse,
|
||||
AddressNFTTokensFilter,
|
||||
AddressCoinBalanceHistoryChartOld,
|
||||
AddressMudTables,
|
||||
AddressMudTablesFilter,
|
||||
AddressMudRecords,
|
||||
AddressMudRecordsFilter,
|
||||
AddressMudRecordsSorting,
|
||||
AddressMudRecord,
|
||||
} from 'types/api/address';
|
||||
import type { AddressesResponse } from 'types/api/addresses';
|
||||
import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata';
|
||||
@@ -61,6 +67,7 @@ import type {
|
||||
import type { IndexingStatus } from 'types/api/indexingStatus';
|
||||
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
|
||||
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
|
||||
import type { MudWorldsResponse } from 'types/api/mudWorlds';
|
||||
import type { NovesAccountHistoryResponse, NovesDescribeTxsResponse, NovesResponseData } from 'types/api/noves';
|
||||
import type {
|
||||
OptimisticL2DepositsResponse,
|
||||
@@ -654,6 +661,34 @@ export const RESOURCES = {
|
||||
path: '/api/v2/optimism/games/count',
|
||||
},
|
||||
|
||||
// MUD worlds on optimism
|
||||
mud_worlds: {
|
||||
path: '/api/v2/mud/worlds',
|
||||
filterFields: [],
|
||||
},
|
||||
|
||||
address_mud_tables: {
|
||||
path: '/api/v2/mud/worlds/:hash/tables',
|
||||
pathParams: [ 'hash' as const ],
|
||||
filterFields: [ 'q' as const ],
|
||||
},
|
||||
|
||||
address_mud_tables_count: {
|
||||
path: '/api/v2/mud/worlds/:hash/tables/count',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
|
||||
address_mud_records: {
|
||||
path: '/api/v2/mud/worlds/:hash/tables/:table_id/records',
|
||||
pathParams: [ 'hash' as const, 'table_id' as const ],
|
||||
filterFields: [ 'filter_key0' as const, 'filter_key1' as const ],
|
||||
},
|
||||
|
||||
address_mud_record: {
|
||||
path: '/api/v2/mud/worlds/:hash/tables/:table_id/records/:record_id',
|
||||
pathParams: [ 'hash' as const, 'table_id' as const, 'record_id' as const ],
|
||||
},
|
||||
|
||||
// arbitrum L2
|
||||
arbitrum_l2_messages: {
|
||||
path: '/api/v2/arbitrum/messages/:direction',
|
||||
@@ -899,6 +934,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
|
||||
'verified_contracts' |
|
||||
'optimistic_l2_output_roots' | 'optimistic_l2_withdrawals' | 'optimistic_l2_txn_batches' | 'optimistic_l2_deposits' |
|
||||
'optimistic_l2_dispute_games' |
|
||||
'mud_worlds'| 'address_mud_tables' | 'address_mud_records' |
|
||||
'shibarium_deposits' | 'shibarium_withdrawals' |
|
||||
'arbitrum_l2_messages' | 'arbitrum_l2_txn_batches' | 'arbitrum_l2_txn_batch_txs' | 'arbitrum_l2_txn_batch_blocks' |
|
||||
'zkevm_l2_deposits' | 'zkevm_l2_withdrawals' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
|
||||
@@ -1056,6 +1092,11 @@ Q extends 'user_op_interpretation'? TxInterpretationResponse :
|
||||
Q extends 'noves_transaction' ? NovesResponseData :
|
||||
Q extends 'noves_address_history' ? NovesAccountHistoryResponse :
|
||||
Q extends 'noves_describe_txs' ? NovesDescribeTxsResponse :
|
||||
Q extends 'mud_worlds' ? MudWorldsResponse :
|
||||
Q extends 'address_mud_tables' ? AddressMudTables :
|
||||
Q extends 'address_mud_tables_count' ? number :
|
||||
Q extends 'address_mud_records' ? AddressMudRecords :
|
||||
Q extends 'address_mud_record' ? AddressMudRecord :
|
||||
never;
|
||||
/* eslint-enable @typescript-eslint/indent */
|
||||
|
||||
@@ -1087,6 +1128,8 @@ Q extends 'addresses_lookup' ? EnsAddressLookupFilters :
|
||||
Q extends 'domains_lookup' ? EnsDomainLookupFilters :
|
||||
Q extends 'user_ops' ? UserOpsFilters :
|
||||
Q extends 'validators' ? ValidatorsFilters :
|
||||
Q extends 'address_mud_tables' ? AddressMudTablesFilter :
|
||||
Q extends 'address_mud_records' ? AddressMudRecordsFilter :
|
||||
never;
|
||||
/* eslint-enable @typescript-eslint/indent */
|
||||
|
||||
@@ -1099,5 +1142,6 @@ Q extends 'address_txs' ? TransactionsSorting :
|
||||
Q extends 'addresses_lookup' ? EnsLookupSorting :
|
||||
Q extends 'domains_lookup' ? EnsLookupSorting :
|
||||
Q extends 'validators' ? ValidatorsSorting :
|
||||
Q extends 'address_mud_records' ? AddressMudRecordsSorting :
|
||||
never;
|
||||
/* eslint-enable @typescript-eslint/indent */
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function capitalizeFirstLetter(text: string) {
|
||||
if (!text || !text.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
}
|
||||
@@ -102,6 +102,12 @@ export default function useNavItems(): ReturnType {
|
||||
icon: 'games',
|
||||
isActive: pathname === '/dispute-games',
|
||||
} : null;
|
||||
const mudWorlds = config.features.mudFramework.isEnabled ? {
|
||||
text: 'MUD worlds',
|
||||
nextRoute: { pathname: '/mud-worlds' as const },
|
||||
icon: 'MUD',
|
||||
isActive: pathname === '/mud-worlds',
|
||||
} : null;
|
||||
|
||||
const rollupFeature = config.features.rollup;
|
||||
|
||||
@@ -121,6 +127,7 @@ export default function useNavItems(): ReturnType {
|
||||
[
|
||||
userOps,
|
||||
topAccounts,
|
||||
mudWorlds,
|
||||
validators,
|
||||
verifiedContracts,
|
||||
ensLookup,
|
||||
|
||||
@@ -48,6 +48,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
|
||||
'/name-domains/[name]': 'Regular page',
|
||||
'/validators': 'Root page',
|
||||
'/gas-tracker': 'Root page',
|
||||
'/mud-worlds': 'Root page',
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': 'Regular page',
|
||||
|
||||
@@ -52,6 +52,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/name-domains/[name]': DEFAULT_TEMPLATE,
|
||||
'/validators': DEFAULT_TEMPLATE,
|
||||
'/gas-tracker': DEFAULT_TEMPLATE,
|
||||
'/mud-worlds': DEFAULT_TEMPLATE,
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': DEFAULT_TEMPLATE,
|
||||
|
||||
@@ -48,6 +48,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/name-domains/[name]': '%network_name% %name% domain details',
|
||||
'/validators': '%network_name% validators list',
|
||||
'/gas-tracker': '%network_name% gas tracker - Current gas fees',
|
||||
'/mud-worlds': '%network_name% MUD worlds list',
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': '%network_name% login',
|
||||
|
||||
@@ -46,6 +46,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
|
||||
'/name-domains/[name]': 'Domain details',
|
||||
'/validators': 'Validators list',
|
||||
'/gas-tracker': 'Gas tracker',
|
||||
'/mud-worlds': 'MUD worlds',
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': 'Login',
|
||||
|
||||
@@ -275,3 +275,13 @@ export const disputeGames: GetServerSideProps<Props> = async(context) => {
|
||||
|
||||
return base(context);
|
||||
};
|
||||
|
||||
export const mud: GetServerSideProps<Props> = async(context) => {
|
||||
if (!config.features.mudFramework.isEnabled) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
return base(context);
|
||||
};
|
||||
|
||||
Vendored
+1
@@ -44,6 +44,7 @@ declare module "nextjs-routes" {
|
||||
| StaticRoute<"/graphiql">
|
||||
| StaticRoute<"/">
|
||||
| StaticRoute<"/login">
|
||||
| StaticRoute<"/mud-worlds">
|
||||
| DynamicRoute<"/name-domains/[name]", { "name": string }>
|
||||
| StaticRoute<"/name-domains">
|
||||
| DynamicRoute<"/op/[hash]", { "hash": string }>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { NextPage } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
const MudWorlds = dynamic(() => import('ui/pages/MudWorlds'), { ssr: false });
|
||||
|
||||
const Page: NextPage = () => {
|
||||
return (
|
||||
<PageNextJs pathname="/mud-worlds">
|
||||
<MudWorlds/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { mud as getServerSideProps } from 'nextjs/getServerSideProps';
|
||||
Vendored
+1
@@ -91,6 +91,7 @@
|
||||
| "monaco/vyper"
|
||||
| "moon-with-star"
|
||||
| "moon"
|
||||
| "MUD"
|
||||
| "networks"
|
||||
| "networks/icon-placeholder"
|
||||
| "networks/logo-placeholder"
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
AddressCoinBalanceHistoryItem,
|
||||
AddressCollection,
|
||||
AddressCounters,
|
||||
AddressMudTableItem,
|
||||
AddressNFT,
|
||||
AddressTabsCounters,
|
||||
AddressTokenBalance,
|
||||
@@ -10,6 +11,7 @@ import type {
|
||||
import type { AddressesItem } from 'types/api/addresses';
|
||||
|
||||
import { ADDRESS_HASH } from './addressParams';
|
||||
import { MUD_SCHEMA, MUD_TABLE } from './mud';
|
||||
import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_721, TOKEN_INFO_ERC_404, TOKEN_INSTANCE } from './token';
|
||||
import { TX_HASH } from './tx';
|
||||
|
||||
@@ -109,3 +111,8 @@ export const ADDRESS_COLLECTION: AddressCollection = {
|
||||
amount: '4',
|
||||
token_instances: Array(4).fill(TOKEN_INSTANCE),
|
||||
};
|
||||
|
||||
export const ADDRESS_MUD_TABLE_ITEM: AddressMudTableItem = {
|
||||
schema: MUD_SCHEMA,
|
||||
table: MUD_TABLE,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { MudWorldItem, MudWorldSchema, MudWorldTable } from 'types/api/mudWorlds';
|
||||
|
||||
import { ADDRESS_PARAMS } from './addressParams';
|
||||
|
||||
export const MUD_TABLE: MudWorldTable = {
|
||||
table_full_name: 'ot.Match',
|
||||
table_id: '0x6f7400000000000000000000000000004d617463680000000000000000000000',
|
||||
table_name: 'Match',
|
||||
table_namespace: '',
|
||||
table_type: 'offchain',
|
||||
};
|
||||
|
||||
export const MUD_SCHEMA: MudWorldSchema = {
|
||||
key_names: [ 'matchEntityKey', 'entity' ],
|
||||
key_types: [ 'bytes32', 'bytes32' ],
|
||||
value_names: [ 'matchEntity' ],
|
||||
value_types: [ 'bytes32' ],
|
||||
};
|
||||
|
||||
export const MUD_WORLD: MudWorldItem = {
|
||||
address: ADDRESS_PARAMS,
|
||||
coin_balance: '7072643779453701031672',
|
||||
tx_count: 442,
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import type { Transaction } from 'types/api/transaction';
|
||||
import type { UserTags, AddressImplementation } from './addressParams';
|
||||
import type { Block } from './block';
|
||||
import type { InternalTransaction } from './internalTransaction';
|
||||
import type { MudWorldSchema, MudWorldTable } from './mudWorlds';
|
||||
import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token';
|
||||
import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer';
|
||||
|
||||
@@ -197,3 +198,56 @@ export type AddressTabsCounters = {
|
||||
validations_count: number | null;
|
||||
withdrawals_count: number | null;
|
||||
}
|
||||
|
||||
// MUD framework
|
||||
export type AddressMudTableItem = {
|
||||
schema: MudWorldSchema;
|
||||
table: MudWorldTable;
|
||||
}
|
||||
|
||||
export type AddressMudTables = {
|
||||
items: Array<AddressMudTableItem>;
|
||||
next_page_params: {
|
||||
items_count: number;
|
||||
table_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type AddressMudTablesFilter = {
|
||||
q?: string;
|
||||
}
|
||||
|
||||
export type AddressMudRecords = {
|
||||
items: Array<AddressMudRecordsItem>;
|
||||
schema: MudWorldSchema;
|
||||
table: MudWorldTable;
|
||||
next_page_params: {
|
||||
items_count: number;
|
||||
key0: string;
|
||||
key1: string;
|
||||
key_bytes: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type AddressMudRecordsItem = {
|
||||
decoded: Record<string, string>;
|
||||
id: string;
|
||||
is_deleted: boolean;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export type AddressMudRecordsFilter = {
|
||||
filter_key0?: string;
|
||||
filter_key1?: string;
|
||||
}
|
||||
|
||||
export type AddressMudRecordsSorting = {
|
||||
sort: 'key0' | 'key1';
|
||||
order: 'asc' | 'desc' | undefined;
|
||||
}
|
||||
|
||||
export type AddressMudRecord = {
|
||||
record: AddressMudRecordsItem;
|
||||
schema: MudWorldSchema;
|
||||
table: MudWorldTable;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { AddressParam } from './addressParams';
|
||||
|
||||
export type MudWorldsResponse = {
|
||||
items: Array<MudWorldItem>;
|
||||
next_page_params: {
|
||||
items_count: number;
|
||||
world: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type MudWorldItem = {
|
||||
address: AddressParam;
|
||||
coin_balance: string;
|
||||
tx_count: number | null;
|
||||
}
|
||||
|
||||
export type MudWorldSchema = {
|
||||
key_names: Array<string>;
|
||||
key_types: Array<string>;
|
||||
value_names: Array<string>;
|
||||
value_types: Array<string>;
|
||||
};
|
||||
|
||||
export type MudWorldTable = {
|
||||
table_full_name: string;
|
||||
table_id: string;
|
||||
table_name: string;
|
||||
table_namespace: string;
|
||||
table_type: string;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import useIsMounted from 'lib/hooks/useIsMounted';
|
||||
|
||||
import AddressMudRecord from './mud/AddressMudRecord';
|
||||
import AddressMudTable from './mud/AddressMudTable';
|
||||
import AddressMudTables from './mud/AddressMudTables';
|
||||
|
||||
type Props ={
|
||||
scrollRef?: React.RefObject<HTMLDivElement>;
|
||||
shouldRender?: boolean;
|
||||
isQueryEnabled?: boolean;
|
||||
}
|
||||
|
||||
const AddressMud = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => {
|
||||
const isMounted = useIsMounted();
|
||||
const router = useRouter();
|
||||
const tableId = router.query.table_id?.toString();
|
||||
const recordId = router.query.record_id?.toString();
|
||||
|
||||
if (!isMounted || !shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tableId && recordId) {
|
||||
return <AddressMudRecord tableId={ tableId } recordId={ recordId } isQueryEnabled={ isQueryEnabled } scrollRef={ scrollRef }/>;
|
||||
}
|
||||
|
||||
if (tableId) {
|
||||
return <AddressMudTable tableId={ tableId } scrollRef={ scrollRef } isQueryEnabled={ isQueryEnabled }/>;
|
||||
}
|
||||
|
||||
return <AddressMudTables scrollRef={ scrollRef } isQueryEnabled={ isQueryEnabled }/>;
|
||||
};
|
||||
|
||||
export default AddressMud;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Alert, Box, Tooltip, useClipboard, useDisclosure } from '@chakra-ui/react';
|
||||
import { AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Alert, Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { Element } from 'react-scroll';
|
||||
|
||||
@@ -10,7 +10,6 @@ import config from 'configs/app';
|
||||
import Tag from 'ui/shared/chakra/Tag';
|
||||
import CopyToClipboard from 'ui/shared/CopyToClipboard';
|
||||
import Hint from 'ui/shared/Hint';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import ContractMethodForm from './form/ContractMethodForm';
|
||||
import { getElementName } from './useScrollToMethod';
|
||||
@@ -42,13 +41,9 @@ const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit }: Props)
|
||||
});
|
||||
}, [ addressHash, data, tab ]);
|
||||
|
||||
const { hasCopied, onCopy } = useClipboard(url, 1000);
|
||||
const methodIdTooltip = useDisclosure();
|
||||
|
||||
const handleCopyLinkClick = React.useCallback((event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
onCopy();
|
||||
}, [ onCopy ]);
|
||||
}, []);
|
||||
|
||||
const handleCopyMethodIdClick = React.useCallback((event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
@@ -64,21 +59,7 @@ const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit }: Props)
|
||||
<>
|
||||
<Element as="h2" name={ 'method_id' in data ? getElementName(data.method_id) : '' }>
|
||||
<AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }} wordBreak="break-all" textAlign="left" as="div" cursor="pointer">
|
||||
{ 'method_id' in data && (
|
||||
<Tooltip label={ hasCopied ? 'Copied!' : 'Copy link' } isOpen={ methodIdTooltip.isOpen || hasCopied } onClose={ methodIdTooltip.onClose }>
|
||||
<Box
|
||||
boxSize={ 5 }
|
||||
color="text_secondary"
|
||||
_hover={{ color: 'link_hovered' }}
|
||||
mr={ 2 }
|
||||
onClick={ handleCopyLinkClick }
|
||||
onMouseEnter={ methodIdTooltip.onOpen }
|
||||
onMouseLeave={ methodIdTooltip.onClose }
|
||||
>
|
||||
<IconSvg name="link" boxSize={ 5 }/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
) }
|
||||
{ 'method_id' in data && <CopyToClipboard text={ url } onClick={ handleCopyLinkClick } type="link" mr={ 2 } color="text_secondary"/> }
|
||||
<Box as="span" fontWeight={ 500 } mr={ 1 }>
|
||||
{ index + 1 }. { data.type === 'fallback' || data.type === 'receive' ? data.type : data.name }
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { HStack, useColorModeValue, chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import CopyToClipboard from 'ui/shared/CopyToClipboard';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import LinkInternal from 'ui/shared/links/LinkInternal';
|
||||
|
||||
type TableViewProps = {
|
||||
scrollRef?: React.RefObject<HTMLDivElement>;
|
||||
className?: string;
|
||||
hash: string;
|
||||
tableId: string;
|
||||
tableName: string;
|
||||
}
|
||||
|
||||
type RecordViewProps = TableViewProps & {
|
||||
recordId: string;
|
||||
recordName: string;
|
||||
}
|
||||
|
||||
type BreadcrumbItemProps = {
|
||||
scrollRef?: React.RefObject<HTMLDivElement>;
|
||||
text: string;
|
||||
href: string;
|
||||
isLast?: boolean;
|
||||
}
|
||||
|
||||
const BreadcrumbItem = ({ text, href, isLast, scrollRef }: BreadcrumbItemProps) => {
|
||||
const iconColor = useColorModeValue('gray.300', 'gray.600');
|
||||
|
||||
const onLinkClick = React.useCallback(() => {
|
||||
window.setTimeout(() => {
|
||||
// cannot do scroll instantly, have to wait a little
|
||||
scrollRef?.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, 500);
|
||||
}, [ scrollRef ]);
|
||||
|
||||
if (isLast) {
|
||||
return (
|
||||
<>
|
||||
{ text }
|
||||
<CopyToClipboard text={ href } type="link" mx={ 0 } color="text_secondary"/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<LinkInternal href={ href } onClick={ onLinkClick }>{ text }</LinkInternal>
|
||||
{ !isLast && <IconSvg name="arrows/east" boxSize={ 6 } color={ iconColor }/> }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AddressMudBreadcrumbs = (props: TableViewProps | RecordViewProps) => {
|
||||
const queryParams = { tab: 'mud', hash: props.hash };
|
||||
return (
|
||||
<HStack gap={ 2 } className={ props.className }>
|
||||
<IconSvg name="MUD" boxSize={ 5 } color="green.500"/>
|
||||
<BreadcrumbItem
|
||||
text="MUD World"
|
||||
href={ route({ pathname: '/address/[hash]', query: queryParams }) }
|
||||
scrollRef={ props.scrollRef }
|
||||
/>
|
||||
<BreadcrumbItem
|
||||
text={ props.tableName }
|
||||
href={ route({ pathname: '/address/[hash]', query: { ...queryParams, table_id: props.tableId } }) }
|
||||
isLast={ !('recordId' in props) }
|
||||
scrollRef={ props.scrollRef }
|
||||
/>
|
||||
{ ('recordId' in props) && (
|
||||
<BreadcrumbItem
|
||||
text={ props.tableName }
|
||||
href={ route({ pathname: '/address/[hash]', query: { ...queryParams, table_id: props.tableId, record_id: props.recordId } }) }
|
||||
isLast
|
||||
scrollRef={ props.scrollRef }
|
||||
|
||||
/>
|
||||
) }
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(chakra(AddressMudBreadcrumbs));
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Box, Td, Tr, Flex, useColorModeValue, Table } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import ContentLoader from 'ui/shared/ContentLoader';
|
||||
import TruncatedValue from 'ui/shared/TruncatedValue';
|
||||
|
||||
import AddressMudBreadcrumbs from './AddressMudBreadcrumbs';
|
||||
|
||||
type Props ={
|
||||
scrollRef?: React.RefObject<HTMLDivElement>;
|
||||
isQueryEnabled?: boolean;
|
||||
tableId: string;
|
||||
recordId: string;
|
||||
}
|
||||
|
||||
const AddressMudRecord = ({ tableId, recordId, isQueryEnabled = true, scrollRef }: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
const hash = getQueryParamString(router.query.hash);
|
||||
|
||||
const valuesBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
|
||||
|
||||
const { data, isLoading, isError } = useApiQuery('address_mud_record', {
|
||||
pathParams: { hash, table_id: tableId, record_id: recordId },
|
||||
queryOptions: {
|
||||
enabled: isQueryEnabled,
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoader/>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <Box>error message</Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ data && (
|
||||
<AddressMudBreadcrumbs
|
||||
hash={ hash }
|
||||
tableId={ tableId }
|
||||
tableName={ data?.table.table_name }
|
||||
recordId={ recordId }
|
||||
recordName={ data.record.id }
|
||||
mb={ 6 }
|
||||
scrollRef={ scrollRef }
|
||||
/>
|
||||
) }
|
||||
|
||||
<Table borderRadius="8px">
|
||||
{ data?.schema.key_names.length && data?.schema.key_names.map((keyName, index) => (
|
||||
<Tr key={ keyName }>
|
||||
<Td fontWeight={ 600 }>
|
||||
{ keyName } ({ data.schema.key_types[index] })
|
||||
</Td>
|
||||
<Td colSpan={ 2 }>
|
||||
<Flex justifyContent="space-between">
|
||||
<TruncatedValue value={ data.record.decoded[keyName] } mr={ 2 }/>
|
||||
{ index === 0 && <Box color="text_secondary">{ dayjs(data.record.timestamp).format('llll') }</Box> }
|
||||
</Flex>
|
||||
</Td>
|
||||
</Tr>
|
||||
)) }
|
||||
{ data?.schema.value_names.length && (
|
||||
<>
|
||||
<Tr backgroundColor={ valuesBgColor } borderBottomStyle="hidden">
|
||||
<Td fontWeight={ 600 }>Field</Td>
|
||||
<Td fontWeight={ 600 }>Type</Td>
|
||||
<Td fontWeight={ 600 }>Value</Td>
|
||||
</Tr>
|
||||
{ data?.schema.value_names.map((valName, index) => (
|
||||
<Tr key={ valName } backgroundColor={ valuesBgColor } borderBottomStyle="hidden">
|
||||
<Td>{ valName }</Td>
|
||||
<Td>{ data.schema.value_types[index] }</Td>
|
||||
<Td>{ data.record.decoded[valName] }</Td>
|
||||
</Tr>
|
||||
)) }
|
||||
</>
|
||||
) }
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddressMudRecord;
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
|
||||
import FilterInput from 'ui/shared/filters/FilterInput';
|
||||
import TableColumnFilter from 'ui/shared/filters/TableColumnFilter';
|
||||
|
||||
type Props = {
|
||||
value?: string;
|
||||
handleFilterChange: (val: string) => void;
|
||||
title: string;
|
||||
columnName: string;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const AddressMudRecordsKeyFilter = ({ value = '', handleFilterChange, columnName, title, isLoading }: Props) => {
|
||||
const [ filterValue, setFilterValue ] = React.useState<string>(value);
|
||||
|
||||
const onFilter = React.useCallback(() => {
|
||||
handleFilterChange(filterValue);
|
||||
}, [ handleFilterChange, filterValue ]);
|
||||
|
||||
return (
|
||||
<TableColumnFilter
|
||||
columnName={ columnName }
|
||||
title={ title }
|
||||
isActive={ Boolean(value) }
|
||||
isFilled={ filterValue !== value }
|
||||
onFilter={ onFilter }
|
||||
isLoading={ isLoading }
|
||||
w="350px"
|
||||
>
|
||||
<FilterInput
|
||||
initialValue={ value }
|
||||
size="xs"
|
||||
onChange={ setFilterValue }
|
||||
placeholder={ columnName }
|
||||
/>
|
||||
</TableColumnFilter>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddressMudRecordsKeyFilter;
|
||||
@@ -0,0 +1,152 @@
|
||||
import type { StyleProps } from '@chakra-ui/react';
|
||||
import { Box, Link, Table, Tbody, Td, Th, Tr, Flex, useColorModeValue, useBoolean } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressMudRecords, AddressMudRecordsFilter, AddressMudRecordsSorting } from 'types/api/address';
|
||||
|
||||
import capitalizeFirstLetter from 'lib/capitalizeFirstLetter';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import { default as Thead } from 'ui/shared/TheadSticky';
|
||||
|
||||
import AddressMudRecordsKeyFilter from './AddressMudRecordsKeyFilter';
|
||||
import { getNameTypeText } from './utils';
|
||||
|
||||
const COL_MIN_WIDTH = 180;
|
||||
const CUT_COL_WIDTH = 36;
|
||||
|
||||
type Props = {
|
||||
data: AddressMudRecords;
|
||||
top: number;
|
||||
sorting?: AddressMudRecordsSorting;
|
||||
toggleSorting: (key: AddressMudRecordsSorting['sort']) => void;
|
||||
setFilters: React.Dispatch<React.SetStateAction<AddressMudRecordsFilter>>;
|
||||
filters: AddressMudRecordsFilter;
|
||||
}
|
||||
|
||||
const AddressMudRecordsTable = ({ data, top, sorting, toggleSorting, filters, setFilters }: Props) => {
|
||||
const [ colsCutCount, setColsCutCount ] = React.useState<number>(0);
|
||||
const [ isOpened, setIsOpened ] = useBoolean(false);
|
||||
const [ hasCut, setHasCut ] = useBoolean(true);
|
||||
|
||||
const tableRef = React.useRef<HTMLTableElement>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const onRecordClick = React.useCallback((e: React.MouseEvent) => {
|
||||
const newQuery = {
|
||||
...router.query,
|
||||
record_id: e.currentTarget.getAttribute('data-id') as string,
|
||||
};
|
||||
router.push({ pathname: router.pathname, query: newQuery }, undefined, { shallow: true });
|
||||
}, [ router ]);
|
||||
|
||||
const handleFilterChange = React.useCallback((field: keyof AddressMudRecordsFilter) => (val: string) => {
|
||||
setFilters(prev => {
|
||||
const newVal = { ...prev };
|
||||
newVal[field] = val;
|
||||
return newVal;
|
||||
});
|
||||
}, [ setFilters ]);
|
||||
|
||||
const onKeySortClick = React.useCallback(
|
||||
(e: React.MouseEvent) => toggleSorting('key' + e.currentTarget.getAttribute('data-id') as AddressMudRecordsSorting['sort']),
|
||||
[ toggleSorting ],
|
||||
);
|
||||
|
||||
const keyBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasCut && !colsCutCount && tableRef.current) {
|
||||
const count = Math.floor((tableRef.current.getBoundingClientRect().width - CUT_COL_WIDTH) / COL_MIN_WIDTH);
|
||||
const total = data.schema.key_names.length + data.schema.value_names.length;
|
||||
if (total > 2 && count - 1 < total) {
|
||||
setColsCutCount(count - 1);
|
||||
} else {
|
||||
setHasCut.off();
|
||||
}
|
||||
}
|
||||
}, [ colsCutCount, data.schema, hasCut, setHasCut ]);
|
||||
|
||||
const cutWidth = `${ CUT_COL_WIDTH }px `;
|
||||
|
||||
const tdStyles: StyleProps = {
|
||||
wordBreak: 'break-all',
|
||||
whiteSpace: 'normal',
|
||||
minW: `${ COL_MIN_WIDTH }px`,
|
||||
w: `${ COL_MIN_WIDTH }px`,
|
||||
};
|
||||
|
||||
const thStyles: StyleProps = {
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
minW: `${ COL_MIN_WIDTH }px`,
|
||||
w: `${ COL_MIN_WIDTH }px`,
|
||||
};
|
||||
|
||||
const keys = (isOpened || !hasCut) ? data.schema.key_names : data.schema.key_names.slice(0, colsCutCount);
|
||||
const values = (isOpened || !hasCut) ? data.schema.value_names : data.schema.value_names.slice(0, colsCutCount - data.schema.key_names.length);
|
||||
|
||||
return (
|
||||
<Box maxW="100%" overflowX="scroll" whiteSpace="nowrap">
|
||||
<Table variant="simple" size="sm" style={{ tableLayout: 'fixed' }} ref={ tableRef }>
|
||||
<Thead top={ top } display="table" w="100%">
|
||||
<Tr >
|
||||
{ keys.map((keyName, index) => {
|
||||
const text = getNameTypeText(keyName, data.schema.key_types[index]);
|
||||
return (
|
||||
<Th key={ keyName } { ...thStyles }>
|
||||
{ index < 2 ? (
|
||||
<Flex>
|
||||
<Link onClick={ onKeySortClick } data-id={ index } display="flex" alignItems="start" lineHeight="20px" mr={ 2 }>
|
||||
{ sorting?.sort === `key${ index }` && sorting.order &&
|
||||
<IconSvg name="arrows/east" boxSize={ 5 } mr={ 2 } transform={ sorting.order === 'asc' ? 'rotate(-90deg)' : 'rotate(90deg)' }/>
|
||||
}
|
||||
{ text }
|
||||
</Link>
|
||||
<AddressMudRecordsKeyFilter
|
||||
value={ filters[index === 0 ? 'filter_key0' : 'filter_key1'] }
|
||||
title={ text }
|
||||
columnName={ keyName }
|
||||
handleFilterChange={ handleFilterChange(index === 0 ? 'filter_key0' : 'filter_key1') }
|
||||
/>
|
||||
</Flex>
|
||||
) : text }
|
||||
</Th>
|
||||
);
|
||||
}) }
|
||||
{ values.map((valName, index) => (
|
||||
<Th key={ valName } { ...thStyles }>
|
||||
{ capitalizeFirstLetter(valName) } ({ data.schema.value_types[index] })
|
||||
</Th>
|
||||
)) }
|
||||
{ hasCut && !isOpened && <Th width={ cutWidth }><Link onClick={ setIsOpened.on }>...</Link></Th> }
|
||||
<Th { ...thStyles }>Modified</Th>
|
||||
{ hasCut && isOpened && <Th width={ cutWidth }><Link onClick={ setIsOpened.off }>...</Link></Th> }
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody display="table" w="100%">
|
||||
{ data.items.map((item) => (
|
||||
<Tr key={ item.id }>
|
||||
{ keys.map((keyName, index) => (
|
||||
<Td key={ keyName } backgroundColor={ keyBgColor } { ...tdStyles }>
|
||||
{ index === 0 ?
|
||||
<Link onClick={ onRecordClick } data-id={ item.id }>{ item.decoded[keyName].toString() }</Link> :
|
||||
item.decoded[keyName].toString()
|
||||
}
|
||||
</Td>
|
||||
)) }
|
||||
{ values.map((valName) =>
|
||||
<Td key={ valName } { ...tdStyles }>{ item.decoded[valName].toString() }</Td>) }
|
||||
{ hasCut && !isOpened && <Td width={ cutWidth }></Td> }
|
||||
<Td { ...tdStyles }>{ dayjs(item.timestamp).format('llll') }</Td>
|
||||
{ hasCut && isOpened && <Td width={ cutWidth }></Td> }
|
||||
</Tr>
|
||||
)) }
|
||||
</Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddressMudRecordsTable;
|
||||
@@ -0,0 +1,149 @@
|
||||
import { Box, HStack, Hide, Show, Tag, TagCloseButton, chakra } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressMudRecordsFilter, AddressMudRecordsSorting } from 'types/api/address';
|
||||
|
||||
import { apos, nbsp } from 'lib/html-entities';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import ActionBar from 'ui/shared/ActionBar';
|
||||
import ContentLoader from 'ui/shared/ContentLoader';
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
import Pagination from 'ui/shared/pagination/Pagination';
|
||||
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
|
||||
import { getNextOrderValue } from 'ui/shared/sort/getNextSortValue';
|
||||
import getSortParamsFromQuery from 'ui/shared/sort/getSortParamsFromQuery';
|
||||
|
||||
import AddressMudBreadcrumbs from './AddressMudBreadcrumbs';
|
||||
import AddressMudRecordsTable from './AddressMudRecordsTable';
|
||||
import { getNameTypeText, SORT_SEQUENCE } from './utils';
|
||||
|
||||
type Props ={
|
||||
scrollRef?: React.RefObject<HTMLDivElement>;
|
||||
isQueryEnabled?: boolean;
|
||||
tableId: string;
|
||||
}
|
||||
|
||||
type FilterKeys = keyof AddressMudRecordsFilter;
|
||||
|
||||
const AddressMudTable = ({ scrollRef, tableId, isQueryEnabled = true }: Props) => {
|
||||
const router = useRouter();
|
||||
const [ sorting, setSorting ] =
|
||||
React.useState<AddressMudRecordsSorting | undefined>(getSortParamsFromQuery<AddressMudRecordsSorting>(router.query, SORT_SEQUENCE));
|
||||
const [ filters, setFilters ] = React.useState<AddressMudRecordsFilter>({});
|
||||
|
||||
const hash = getQueryParamString(router.query.hash);
|
||||
|
||||
const { data, isLoading, isError, pagination, onSortingChange } = useQueryWithPages({
|
||||
resourceName: 'address_mud_records',
|
||||
pathParams: { hash, table_id: tableId },
|
||||
filters,
|
||||
sorting,
|
||||
scrollRef,
|
||||
options: {
|
||||
// no placeholder data because the structure of a table is unpredictable
|
||||
enabled: isQueryEnabled,
|
||||
},
|
||||
});
|
||||
|
||||
const toggleSorting = React.useCallback((val: AddressMudRecordsSorting['sort']) => {
|
||||
const newSorting = { sort: val, order: getNextOrderValue(sorting?.sort === val ? sorting.order : undefined) };
|
||||
setSorting(newSorting);
|
||||
onSortingChange(newSorting);
|
||||
}, [ onSortingChange, sorting ]);
|
||||
|
||||
const onRemoveFilterClick = React.useCallback((key: FilterKeys) => () => {
|
||||
setFilters(prev => {
|
||||
const newFilters = { ...prev };
|
||||
delete newFilters[key];
|
||||
return newFilters;
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoader/>;
|
||||
}
|
||||
|
||||
const hasActiveFilters = Object.values(filters).some(Boolean);
|
||||
|
||||
const filtersTags = hasActiveFilters ? (
|
||||
<HStack gap={ 3 }>
|
||||
{ Object.entries(filters).map(([ key, value ]) => {
|
||||
const index = key as FilterKeys === 'filter_key0' ? 0 : 1;
|
||||
return (
|
||||
<Tag display="inline-flex" key={ key } maxW="360px" colorScheme="blue">
|
||||
<chakra.span color="text_secondary" >{
|
||||
getNameTypeText(data?.schema.key_names[index] || '', data?.schema.key_types[index] || '') }
|
||||
</chakra.span>
|
||||
<chakra.span color="text" overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
|
||||
{ nbsp }
|
||||
{ value }
|
||||
</chakra.span>
|
||||
<TagCloseButton onClick={ onRemoveFilterClick(key as FilterKeys) }/>
|
||||
</Tag>
|
||||
);
|
||||
}) }
|
||||
</HStack>
|
||||
) : null;
|
||||
|
||||
const actionBar = (
|
||||
<ActionBar mt={ -6 } showShadow justifyContent="space-between">
|
||||
<Box>
|
||||
{ data && (
|
||||
<AddressMudBreadcrumbs
|
||||
hash={ hash }
|
||||
tableId={ tableId }
|
||||
tableName={ data?.table.table_full_name }
|
||||
scrollRef={ scrollRef }
|
||||
mb={ 3 }
|
||||
/>
|
||||
) }
|
||||
{ filtersTags }
|
||||
</Box>
|
||||
<Pagination ml={{ base: 0, lg: 8 }} { ...pagination }/>
|
||||
</ActionBar>
|
||||
);
|
||||
|
||||
const content = data?.items ? (
|
||||
<>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<AddressMudRecordsTable
|
||||
data={ data }
|
||||
// can't implement both horisontal table scroll and sticky header
|
||||
// top={ pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 60 }
|
||||
top={ 0 }
|
||||
sorting={ sorting }
|
||||
toggleSorting={ toggleSorting }
|
||||
setFilters={ setFilters }
|
||||
filters={ filters }
|
||||
/>
|
||||
</Hide>
|
||||
<Show below="lg" ssr={ false }>
|
||||
waiting for mobile mockup
|
||||
{ /* { data.items.map((item, index) => (
|
||||
<AddressMudListItem
|
||||
key={ item.table.table_id + (isPlaceholderData ? String(index) : '') }
|
||||
item={ item }
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
)) } */ }
|
||||
</Show>
|
||||
</>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<DataListDisplay
|
||||
isError={ isError }
|
||||
items={ data?.items }
|
||||
emptyText="There are no records for this table."
|
||||
filterProps={{
|
||||
emptyFilteredText: `Couldn${ apos }t find records that match your filter query.`,
|
||||
hasActiveFilters: Object.values(filters).some(Boolean),
|
||||
}}
|
||||
content={ content }
|
||||
actionBar={ actionBar }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddressMudTable;
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Hide, Show } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import useIsInitialLoading from 'lib/hooks/useIsInitialLoading';
|
||||
import { apos } from 'lib/html-entities';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import { ADDRESS_MUD_TABLE_ITEM } from 'stubs/address';
|
||||
import { generateListStub } from 'stubs/utils';
|
||||
import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
import FilterInput from 'ui/shared/filters/FilterInput';
|
||||
import Pagination from 'ui/shared/pagination/Pagination';
|
||||
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
|
||||
|
||||
import AddressMudTablesTable from './AddressMudTablesTable';
|
||||
|
||||
type Props ={
|
||||
scrollRef?: React.RefObject<HTMLDivElement>;
|
||||
isQueryEnabled?: boolean;
|
||||
}
|
||||
|
||||
const AddressMudTables = ({ scrollRef, isQueryEnabled = true }: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
const hash = getQueryParamString(router.query.hash);
|
||||
const q = getQueryParamString(router.query.q);
|
||||
const [ searchTerm, setSearchTerm ] = React.useState<string>(q || '');
|
||||
|
||||
const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({
|
||||
resourceName: 'address_mud_tables',
|
||||
pathParams: { hash },
|
||||
filters: { q: searchTerm },
|
||||
scrollRef,
|
||||
options: {
|
||||
enabled: isQueryEnabled,
|
||||
placeholderData: generateListStub<'address_mud_tables'>(ADDRESS_MUD_TABLE_ITEM, 3, { next_page_params: {
|
||||
items_count: 50,
|
||||
table_id: '1',
|
||||
} }),
|
||||
},
|
||||
});
|
||||
|
||||
const isInitialLoading = useIsInitialLoading(isPlaceholderData);
|
||||
|
||||
const searchInput = (
|
||||
<FilterInput
|
||||
w={{ base: '100%', lg: '360px' }}
|
||||
minW={{ base: 'auto', lg: '250px' }}
|
||||
size="xs"
|
||||
onChange={ setSearchTerm }
|
||||
placeholder="Search by name, namespace or table ID..."
|
||||
initialValue={ searchTerm }
|
||||
isLoading={ isInitialLoading }
|
||||
/>
|
||||
);
|
||||
|
||||
const actionBar = (
|
||||
<ActionBar mt={ -6 } showShadow justifyContent="space-between">
|
||||
{ searchInput }
|
||||
<Pagination ml={{ base: 0, lg: 8 }} { ...pagination }/>
|
||||
</ActionBar>
|
||||
);
|
||||
|
||||
const content = data?.items ? (
|
||||
<>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<AddressMudTablesTable
|
||||
items={ data.items }
|
||||
isLoading={ isPlaceholderData }
|
||||
top={ ACTION_BAR_HEIGHT_DESKTOP }
|
||||
/>
|
||||
</Hide>
|
||||
<Show below="lg" ssr={ false }>
|
||||
waiting for mobile mockup
|
||||
{ /* { data.items.map((item, index) => (
|
||||
<AddressMudListItem
|
||||
key={ item.table.table_id + (isPlaceholderData ? String(index) : '') }
|
||||
item={ item }
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
)) } */ }
|
||||
</Show>
|
||||
</>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<DataListDisplay
|
||||
isError={ isError }
|
||||
items={ data?.items }
|
||||
emptyText="There are no tables for this address."
|
||||
filterProps={{
|
||||
emptyFilteredText: `Couldn${ apos }t find tables that match your filter query.`,
|
||||
hasActiveFilters: Boolean(searchTerm),
|
||||
}}
|
||||
content={ content }
|
||||
actionBar={ actionBar }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddressMudTables;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressMudTables } from 'types/api/address';
|
||||
|
||||
import { default as Thead } from 'ui/shared/TheadSticky';
|
||||
|
||||
import AddressMudTablesTableItem from './AddressMudTablesTableItem';
|
||||
|
||||
type Props = {
|
||||
items: AddressMudTables['items'];
|
||||
isLoading: boolean;
|
||||
top: number;
|
||||
}
|
||||
|
||||
//sorry for the naming
|
||||
const AddressMudTablesTable = ({ items, isLoading, top }: Props) => {
|
||||
return (
|
||||
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }}>
|
||||
<Thead top={ top }>
|
||||
<Tr>
|
||||
<Th width="24px"></Th>
|
||||
<Th>Full name</Th>
|
||||
<Th>Table ID</Th>
|
||||
<Th>Type</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{ items.map((item, index) => (
|
||||
<AddressMudTablesTableItem
|
||||
key={ item.table.table_id + (isLoading ? String(index) : '') }
|
||||
item={ item }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
)) }
|
||||
</Tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddressMudTablesTable;
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Td, Tr, Text, Skeleton, useBoolean, Link, Table, VStack, chakra } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressMudTableItem } from 'types/api/address';
|
||||
|
||||
import Tag from 'ui/shared/chakra/Tag';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
type Props = {
|
||||
item: AddressMudTableItem;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
const AddressMudTablesTableItem = ({ item, isLoading }: Props) => {
|
||||
const [ isOpened, setIsOpened ] = useBoolean(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const onTableClick = React.useCallback((e: React.MouseEvent) => {
|
||||
const newQuery = {
|
||||
...router.query,
|
||||
table_id: e.currentTarget.getAttribute('data-id') as string,
|
||||
};
|
||||
router.push({ pathname: router.pathname, query: newQuery }, undefined, { shallow: true });
|
||||
}, [ router ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr borderStyle={ isOpened ? 'hidden' : 'unset' }>
|
||||
<Td verticalAlign="middle">
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
<Link display="block">
|
||||
<IconSvg
|
||||
name="arrows/east-mini"
|
||||
transform={ isOpened ? 'rotate(270deg)' : 'rotate(180deg)' }
|
||||
boxSize={ 6 }
|
||||
cursor="pointer"
|
||||
onClick={ setIsOpened.toggle }
|
||||
transitionDuration="faster"
|
||||
/>
|
||||
</Link>
|
||||
</Skeleton>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
<Link onClick={ onTableClick } data-id={ item.table.table_id }>
|
||||
{ item.table.table_full_name }
|
||||
</Link>
|
||||
</Skeleton>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
{ item.table.table_id }
|
||||
</Skeleton>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
{ item.table.table_type }
|
||||
</Skeleton>
|
||||
</Td>
|
||||
</Tr>
|
||||
{ isOpened && (
|
||||
<Tr>
|
||||
<Td></Td>
|
||||
<Td colSpan={ 3 }>
|
||||
<Table>
|
||||
{ Boolean(item.schema.key_names.length) && (
|
||||
<Tr>
|
||||
<Td width="80px" fontSize="sm" fontWeight={ 600 }>Key</Td>
|
||||
<Td>
|
||||
<VStack gap={ 1 } alignItems="start">
|
||||
{ item.schema.key_names.map((name, index) => (
|
||||
<Tag key={ name }>
|
||||
<chakra.span fontWeight={ 700 }>{ item.schema.key_types[index] }</chakra.span> { name }
|
||||
</Tag>
|
||||
)) }
|
||||
</VStack>
|
||||
</Td>
|
||||
</Tr>
|
||||
) }
|
||||
<Tr borderBottomStyle="hidden">
|
||||
<Td width="80px" fontSize="sm" fontWeight={ 600 }>Value</Td>
|
||||
<Td fontSize="sm">
|
||||
<VStack gap={ 1 } alignItems="start">
|
||||
{ item.schema.value_names.map((name, index) => (
|
||||
<Text key={ name }>
|
||||
<chakra.span fontWeight={ 700 }>{ item.schema.value_types[index] }</chakra.span> { name }
|
||||
</Text>
|
||||
)) }
|
||||
</VStack>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Table>
|
||||
</Td>
|
||||
</Tr>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AddressMudTablesTableItem);
|
||||
@@ -0,0 +1,10 @@
|
||||
import capitalizeFirstLetter from 'lib/capitalizeFirstLetter';
|
||||
|
||||
export const SORT_SEQUENCE: Record<'key0' | 'key1', Array<'desc' | 'asc' | undefined>> = {
|
||||
key0: [ 'desc', 'asc', undefined ],
|
||||
key1: [ 'desc', 'asc', undefined ],
|
||||
};
|
||||
|
||||
export const getNameTypeText = (name: string, type: string) => {
|
||||
return capitalizeFirstLetter(name) + ' (' + type + ')';
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { HStack, Skeleton } from '@chakra-ui/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import React from 'react';
|
||||
|
||||
import type { MudWorldItem } from 'types/api/mudWorlds';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { currencyUnits } from 'lib/units';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
|
||||
|
||||
type Props = {
|
||||
item: MudWorldItem;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const MudWorldsListItem = ({
|
||||
item,
|
||||
isLoading,
|
||||
}: Props) => {
|
||||
|
||||
const addressBalance = BigNumber(item.coin_balance).div(BigNumber(10 ** config.chain.currency.decimals));
|
||||
|
||||
return (
|
||||
<ListItemMobile rowGap={ 3 }>
|
||||
<AddressEntity
|
||||
address={ item.address }
|
||||
isLoading={ isLoading }
|
||||
fontWeight={ 700 }
|
||||
mr={ 2 }
|
||||
truncation="constant_long"
|
||||
/>
|
||||
<HStack spacing={ 3 } maxW="100%" alignItems="flex-start">
|
||||
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 } flexShrink={ 0 }>{ `Balance ${ currencyUnits.ether }` }</Skeleton>
|
||||
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary" minW="0" whiteSpace="pre-wrap">
|
||||
<span>{ addressBalance.dp(8).toFormat() }</span>
|
||||
</Skeleton>
|
||||
</HStack>
|
||||
<HStack spacing={ 3 }>
|
||||
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Txn count</Skeleton>
|
||||
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
|
||||
<span>{ Number(item.tx_count).toLocaleString() }</span>
|
||||
</Skeleton>
|
||||
</HStack>
|
||||
</ListItemMobile>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(MudWorldsListItem);
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { MudWorldItem } from 'types/api/mudWorlds';
|
||||
|
||||
import { currencyUnits } from 'lib/units';
|
||||
import { default as Thead } from 'ui/shared/TheadSticky';
|
||||
|
||||
import MudWorldsTableItem from './MudWorldsTableItem';
|
||||
|
||||
type Props = {
|
||||
items: Array<MudWorldItem>;
|
||||
top: number;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const MudWorldsTable = ({ items, top, isLoading }: Props) => {
|
||||
return (
|
||||
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }}>
|
||||
<Thead top={ top }>
|
||||
<Tr>
|
||||
<Th>Address</Th>
|
||||
<Th isNumeric>{ `Balance ${ currencyUnits.ether }` }</Th>
|
||||
<Th isNumeric>Txn count</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{ items.map((item, index) => (
|
||||
<MudWorldsTableItem
|
||||
key={ String(item.address.hash) + (isLoading ? index : '') }
|
||||
item={ item }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
)) }
|
||||
</Tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default MudWorldsTable;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Text, Td, Tr, Skeleton } from '@chakra-ui/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import React from 'react';
|
||||
|
||||
import type { MudWorldItem } from 'types/api/mudWorlds';
|
||||
|
||||
import config from 'configs/app';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
|
||||
const mudFrameworkFeature = config.features.mudFramework;
|
||||
|
||||
type Props = { item: MudWorldItem; isLoading?: boolean };
|
||||
|
||||
const MudWorldsTableItem = ({ item, isLoading }: Props) => {
|
||||
if (!mudFrameworkFeature.isEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const addressBalance = BigNumber(item.coin_balance).div(BigNumber(10 ** config.chain.currency.decimals));
|
||||
const addressBalanceChunks = addressBalance.dp(8).toFormat().split('.');
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td verticalAlign="middle">
|
||||
<AddressEntity address={ item.address } isLoading={ isLoading } fontWeight={ 700 }/>
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block" maxW="100%">
|
||||
<Text lineHeight="24px" as="span">{ addressBalanceChunks[0] }</Text>
|
||||
{ addressBalanceChunks[1] && <Text lineHeight="24px" as="span">.</Text> }
|
||||
<Text lineHeight="24px" variant="secondary" as="span">{ addressBalanceChunks[1] }</Text>
|
||||
</Skeleton>
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block" lineHeight="24px">
|
||||
{ Number(item.tx_count).toLocaleString() }
|
||||
</Skeleton>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default MudWorldsTableItem;
|
||||
+29
-3
@@ -23,6 +23,7 @@ import AddressContract from 'ui/address/AddressContract';
|
||||
import AddressDetails from 'ui/address/AddressDetails';
|
||||
import AddressInternalTxs from 'ui/address/AddressInternalTxs';
|
||||
import AddressLogs from 'ui/address/AddressLogs';
|
||||
import AddressMud from 'ui/address/AddressMud';
|
||||
import AddressTokens from 'ui/address/AddressTokens';
|
||||
import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
|
||||
import AddressTxs from 'ui/address/AddressTxs';
|
||||
@@ -77,6 +78,14 @@ const AddressPageContent = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const mudTablesCountQuery = useApiQuery('address_mud_tables_count', {
|
||||
pathParams: { hash },
|
||||
queryOptions: {
|
||||
enabled: config.features.mudFramework.isEnabled && areQueriesEnabled && Boolean(hash),
|
||||
placeholderData: 10,
|
||||
},
|
||||
});
|
||||
|
||||
const addressesForMetadataQuery = React.useMemo(() => ([ hash ].filter(Boolean)), [ hash ]);
|
||||
const addressMetadataQuery = useAddressMetadataInfoQuery(addressesForMetadataQuery, areQueriesEnabled);
|
||||
|
||||
@@ -98,7 +107,7 @@ const AddressPageContent = () => {
|
||||
undefined;
|
||||
|
||||
const isLoading = addressQuery.isPlaceholderData || (config.features.userOps.isEnabled && userOpsAccountQuery.isPlaceholderData);
|
||||
const isTabsLoading = isLoading || addressTabsCountersQuery.isPlaceholderData;
|
||||
const isTabsLoading = isLoading || addressTabsCountersQuery.isPlaceholderData || mudTablesCountQuery.isPlaceholderData;
|
||||
|
||||
const handleFetchedBytecodeMessage = React.useCallback(() => {
|
||||
addressQuery.refetch();
|
||||
@@ -121,6 +130,12 @@ const AddressPageContent = () => {
|
||||
|
||||
const tabs: Array<RoutedTab> = React.useMemo(() => {
|
||||
return [
|
||||
config.features.mudFramework.isEnabled && mudTablesCountQuery.data && mudTablesCountQuery.data > 0 && {
|
||||
id: 'mud',
|
||||
title: 'MUD',
|
||||
count: mudTablesCountQuery.data,
|
||||
component: <AddressMud scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
|
||||
},
|
||||
{
|
||||
id: 'txs',
|
||||
title: 'Transactions',
|
||||
@@ -215,7 +230,15 @@ const AddressPageContent = () => {
|
||||
subTabs: contractTabs.tabs.map(tab => tab.id),
|
||||
} : undefined,
|
||||
].filter(Boolean);
|
||||
}, [ addressQuery.data, contractTabs, addressTabsCountersQuery.data, userOpsAccountQuery.data, isTabsLoading, areQueriesEnabled ]);
|
||||
}, [
|
||||
addressQuery.data,
|
||||
contractTabs,
|
||||
addressTabsCountersQuery.data,
|
||||
userOpsAccountQuery.data,
|
||||
isTabsLoading,
|
||||
areQueriesEnabled,
|
||||
mudTablesCountQuery.data,
|
||||
]);
|
||||
|
||||
const tags: Array<EntityTag> = React.useMemo(() => {
|
||||
return [
|
||||
@@ -229,10 +252,13 @@ const AddressPageContent = () => {
|
||||
config.features.userOps.isEnabled && userOpsAccountQuery.data ?
|
||||
{ slug: 'user_ops_acc', name: 'Smart contract wallet', tagType: 'custom' as const, ordinal: -10 } :
|
||||
undefined,
|
||||
config.features.mudFramework.isEnabled && mudTablesCountQuery.data ?
|
||||
{ slug: 'mud', name: 'MUD World', tagType: 'custom' as const, ordinal: -10 } :
|
||||
undefined,
|
||||
...formatUserTags(addressQuery.data),
|
||||
...(addressMetadataQuery.data?.addresses?.[hash.toLowerCase()]?.tags || []),
|
||||
].filter(Boolean).sort(sortEntityTags);
|
||||
}, [ addressMetadataQuery.data, addressQuery.data, hash, isSafeAddress, userOpsAccountQuery.data ]);
|
||||
}, [ addressMetadataQuery.data, addressQuery.data, hash, isSafeAddress, userOpsAccountQuery.data, mudTablesCountQuery.data ]);
|
||||
|
||||
const titleContentAfter = (
|
||||
<EntityTags
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Hide, Show } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { MUD_WORLD } from 'stubs/mud';
|
||||
import { generateListStub } from 'stubs/utils';
|
||||
import MudWorldsListItem from 'ui/mudWorlds/MudWorldsListItem';
|
||||
import MudWorldsTable from 'ui/mudWorlds/MudWorldsTable';
|
||||
import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import Pagination from 'ui/shared/pagination/Pagination';
|
||||
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
|
||||
|
||||
const MudWorlds = () => {
|
||||
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
|
||||
resourceName: 'mud_worlds',
|
||||
options: {
|
||||
placeholderData: generateListStub<'mud_worlds'>(
|
||||
MUD_WORLD,
|
||||
50,
|
||||
{
|
||||
next_page_params: {
|
||||
items_count: 50,
|
||||
world: '1',
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
const content = data?.items ? (
|
||||
<>
|
||||
<Show below="lg" ssr={ false }>
|
||||
{ data.items.map(((item, index) => (
|
||||
<MudWorldsListItem
|
||||
key={ item.address.hash + (isPlaceholderData ? String(index) : '') }
|
||||
item={ item }
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
))) }
|
||||
</Show>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<MudWorldsTable items={ data.items } top={ pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 } isLoading={ isPlaceholderData }/>
|
||||
</Hide>
|
||||
</>
|
||||
) : null;
|
||||
|
||||
const actionBar = pagination.isVisible ? (
|
||||
<ActionBar mt={ -6 }>
|
||||
<Pagination ml="auto" { ...pagination }/>
|
||||
</ActionBar>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle title="MUD worlds" withTextAd/>
|
||||
<DataListDisplay
|
||||
isError={ isError }
|
||||
items={ data?.items }
|
||||
emptyText="There are no MUD worlds."
|
||||
content={ content }
|
||||
actionBar={ actionBar }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MudWorlds;
|
||||
@@ -9,9 +9,10 @@ export interface Props {
|
||||
isLoading?: boolean;
|
||||
onClick?: (event: React.MouseEvent) => void;
|
||||
size?: number;
|
||||
type?: 'link';
|
||||
}
|
||||
|
||||
const CopyToClipboard = ({ text, className, isLoading, onClick, size = 5 }: Props) => {
|
||||
const CopyToClipboard = ({ text, className, isLoading, onClick, size = 5, type }: Props) => {
|
||||
const { hasCopied, onCopy } = useClipboard(text, 1000);
|
||||
const [ copied, setCopied ] = useState(false);
|
||||
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
|
||||
@@ -36,10 +37,10 @@ const CopyToClipboard = ({ text, className, isLoading, onClick, size = 5 }: Prop
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } isOpen={ isOpen || copied }>
|
||||
<Tooltip label={ copied ? 'Copied' : `Copy${ type === 'link' ? ' link ' : ' ' }to clipboard` } isOpen={ isOpen || copied }>
|
||||
<IconButton
|
||||
aria-label="copy"
|
||||
icon={ <IconSvg name="copy" boxSize={ size }/> }
|
||||
icon={ <IconSvg name={ type === 'link' ? 'link' : 'copy' } boxSize={ size }/> }
|
||||
boxSize={ size }
|
||||
color={ iconColor }
|
||||
variant="simple"
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
chakra,
|
||||
Flex,
|
||||
Text,
|
||||
Link,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import TableColumnFilterWrapper from './TableColumnFilterWrapper';
|
||||
|
||||
type Props = {
|
||||
columnName: string;
|
||||
title: string;
|
||||
isActive?: boolean;
|
||||
isFilled?: boolean;
|
||||
onFilter: () => void;
|
||||
onReset?: () => void;
|
||||
onClose?: () => void;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
type ContentProps = {
|
||||
title: string;
|
||||
isFilled?: boolean;
|
||||
hasReset?: boolean;
|
||||
onFilter: () => void;
|
||||
onReset?: () => void;
|
||||
onClose?: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const TableColumnFilterContent = ({ title, isFilled, hasReset, onFilter, onReset, onClose, children }: ContentProps) => {
|
||||
const onFilterClick = React.useCallback(() => {
|
||||
onClose && onClose();
|
||||
onFilter();
|
||||
}, [ onClose, onFilter ]);
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<Text color="text_secondary" fontWeight="600">{ title }</Text>
|
||||
{ hasReset && (
|
||||
<Link
|
||||
onClick={ onReset }
|
||||
cursor={ isFilled ? 'pointer' : 'unset' }
|
||||
opacity={ isFilled ? 1 : 0.2 }
|
||||
_hover={{
|
||||
color: isFilled ? 'link_hovered' : 'none',
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Link>
|
||||
) }
|
||||
</Flex>
|
||||
{ children }
|
||||
<Button
|
||||
isDisabled={ !isFilled }
|
||||
onClick={ onFilterClick }
|
||||
w="fit-content"
|
||||
>
|
||||
Filter
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const TableColumnFilter = ({ columnName, isActive, className, isLoading, ...props }: Props) => {
|
||||
return (
|
||||
<TableColumnFilterWrapper
|
||||
isActive={ isActive }
|
||||
columnName={ columnName }
|
||||
className={ className }
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
<TableColumnFilterContent { ...props }/>
|
||||
</TableColumnFilterWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default chakra(TableColumnFilter);
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
PopoverBody,
|
||||
useDisclosure,
|
||||
IconButton,
|
||||
chakra,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import Popover from 'ui/shared/chakra/Popover';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
interface Props {
|
||||
columnName: string;
|
||||
isActive?: boolean;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const TableColumnFilterWrapper = ({ columnName, isActive, className, children, isLoading }: Props) => {
|
||||
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||
|
||||
const child = React.Children.only(children) as React.ReactElement & {
|
||||
ref?: React.Ref<React.ReactNode>;
|
||||
};
|
||||
|
||||
const modifiedChildren = React.cloneElement(
|
||||
child,
|
||||
{ onClose },
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy lazyBehavior="unmount" strategy="fixed">
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
onClick={ onToggle }
|
||||
aria-label={ `filter by ${ columnName }` }
|
||||
variant="ghost"
|
||||
w="20px"
|
||||
h="20px"
|
||||
icon={ <IconSvg name="filter" w="20px" h="20px"/> }
|
||||
isActive={ isActive }
|
||||
isDisabled={ isLoading }
|
||||
borderRadius="4px"
|
||||
color="text_secondary"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className={ className }>
|
||||
<PopoverBody px={ 4 } py={ 6 } display="flex" flexDir="column" rowGap={ 3 }>
|
||||
{ modifiedChildren }
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default chakra(TableColumnFilterWrapper);
|
||||
@@ -8,7 +8,7 @@ interface Props {
|
||||
|
||||
const Container = ({ children, className }: Props) => {
|
||||
return (
|
||||
<Box className={ className } minWidth={{ base: '100vw', lg: 'fit-content' }}>
|
||||
<Box className={ className } minWidth={{ base: '100vw', lg: 'auto' }}>
|
||||
{ children }
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -3,8 +3,20 @@ export default function getNextSortValue<SortField extends string, Sort extends
|
||||
) {
|
||||
return (prevValue: Sort | undefined) => {
|
||||
const sequence = sortSequence[field];
|
||||
getNextValueFromSequence(sequence, prevValue);
|
||||
const curIndex = sequence.findIndex((sort) => sort === prevValue);
|
||||
const nextIndex = curIndex + 1 > sequence.length - 1 ? 0 : curIndex + 1;
|
||||
return sequence[nextIndex];
|
||||
};
|
||||
}
|
||||
|
||||
export function getNextValueFromSequence<T>(sequence: Array<T>, prevValue: T) {
|
||||
const curIndex = sequence.findIndex((val) => val === prevValue);
|
||||
const nextIndex = curIndex + 1 > sequence.length - 1 ? 0 : curIndex + 1;
|
||||
return sequence[nextIndex];
|
||||
}
|
||||
|
||||
// asc desc undefined
|
||||
type Order = 'asc' | 'desc' | undefined;
|
||||
const sequence: Array<Order> = [ 'desc', 'asc', undefined ];
|
||||
export const getNextOrderValue = (getNextValueFromSequence<Order>).bind(undefined, sequence);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Query } from 'nextjs-routes';
|
||||
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
|
||||
export default function getSortParamsFromQuery<T>(query: Query, sortOptions: Record<string, Array<string | undefined>>) {
|
||||
if (!query.sort || !query.order) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sortStr = getQueryParamString(query.sort);
|
||||
|
||||
if (!Object.keys(sortOptions).includes(sortStr)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const orderStr = getQueryParamString(query.order);
|
||||
|
||||
if (!sortOptions[sortStr].includes(orderStr)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return ({ sort: sortStr, order: orderStr } as T);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TagLabel, Tooltip, chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import capitalizeFirstLetter from 'lib/capitalizeFirstLetter';
|
||||
import Tag from 'ui/shared/chakra/Tag';
|
||||
import type { IconName } from 'ui/shared/IconSvg';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
@@ -19,7 +20,7 @@ const StatusTag = ({ type, text, errorText, isLoading, className }: Props) => {
|
||||
let icon: IconName;
|
||||
let colorScheme;
|
||||
|
||||
const capitalizedText = text.charAt(0).toUpperCase() + text.slice(1);
|
||||
const capitalizedText = capitalizeFirstLetter(text);
|
||||
|
||||
switch (type) {
|
||||
case 'ok':
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import capitalizeFirstLetter from 'lib/capitalizeFirstLetter';
|
||||
|
||||
export function camelCaseToSentence(camelCaseString: string | undefined) {
|
||||
if (!camelCaseString) {
|
||||
return '';
|
||||
@@ -5,7 +7,7 @@ export function camelCaseToSentence(camelCaseString: string | undefined) {
|
||||
|
||||
let sentence = camelCaseString.replace(/([a-z])([A-Z])/g, '$1 $2');
|
||||
sentence = sentence.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2');
|
||||
sentence = sentence.charAt(0).toUpperCase() + sentence.slice(1);
|
||||
sentence = capitalizeFirstLetter(sentence);
|
||||
|
||||
return sentence;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user