Commit a0b15f53 authored by isstuev's avatar isstuev

MUD

parent 7e11f56f
...@@ -13,6 +13,7 @@ on: ...@@ -13,6 +13,7 @@ on:
- arbitrum - arbitrum
- base - base
- celo_alfajores - celo_alfajores
- garnet
- gnosis - gnosis
- eth - eth
- eth_sepolia - eth_sepolia
......
...@@ -362,6 +362,7 @@ ...@@ -362,6 +362,7 @@
"arbitrum", "arbitrum",
"base", "base",
"celo_alfajores", "celo_alfajores",
"garnet",
"gnosis", "gnosis",
"eth", "eth",
"eth_goerli", "eth_goerli",
......
...@@ -17,6 +17,7 @@ export { default as growthBook } from './growthBook'; ...@@ -17,6 +17,7 @@ export { default as growthBook } from './growthBook';
export { default as marketplace } from './marketplace'; export { default as marketplace } from './marketplace';
export { default as metasuites } from './metasuites'; export { default as metasuites } from './metasuites';
export { default as mixpanel } from './mixpanel'; export { default as mixpanel } from './mixpanel';
export { default as mudFramework } from './mudFramework';
export { default as multichainButton } from './multichainButton'; export { default as multichainButton } from './multichainButton';
export { default as nameService } from './nameService'; export { default as nameService } from './nameService';
export { default as publicTagsSubmission } from './publicTagsSubmission'; export { default as publicTagsSubmission } from './publicTagsSubmission';
......
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;
# 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
\ No newline at end of file
...@@ -675,6 +675,16 @@ const schema = yup ...@@ -675,6 +675,16 @@ const schema = yup
value => value === undefined, 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 // 6. External services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(), 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 ...@@ -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_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_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_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` | - |
&nbsp; &nbsp;
......
<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>
...@@ -32,6 +32,12 @@ import type { ...@@ -32,6 +32,12 @@ import type {
AddressCollectionsResponse, AddressCollectionsResponse,
AddressNFTTokensFilter, AddressNFTTokensFilter,
AddressCoinBalanceHistoryChartOld, AddressCoinBalanceHistoryChartOld,
AddressMudTables,
AddressMudTablesFilter,
AddressMudRecords,
AddressMudRecordsFilter,
AddressMudRecordsSorting,
AddressMudRecord,
} from 'types/api/address'; } from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses'; import type { AddressesResponse } from 'types/api/addresses';
import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata'; import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata';
...@@ -61,6 +67,7 @@ import type { ...@@ -61,6 +67,7 @@ import type {
import type { IndexingStatus } from 'types/api/indexingStatus'; import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log'; 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 { NovesAccountHistoryResponse, NovesDescribeTxsResponse, NovesResponseData } from 'types/api/noves';
import type { import type {
OptimisticL2DepositsResponse, OptimisticL2DepositsResponse,
...@@ -654,6 +661,34 @@ export const RESOURCES = { ...@@ -654,6 +661,34 @@ export const RESOURCES = {
path: '/api/v2/optimism/games/count', 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
arbitrum_l2_messages: { arbitrum_l2_messages: {
path: '/api/v2/arbitrum/messages/:direction', path: '/api/v2/arbitrum/messages/:direction',
...@@ -899,6 +934,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -899,6 +934,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'verified_contracts' | 'verified_contracts' |
'optimistic_l2_output_roots' | 'optimistic_l2_withdrawals' | 'optimistic_l2_txn_batches' | 'optimistic_l2_deposits' | 'optimistic_l2_output_roots' | 'optimistic_l2_withdrawals' | 'optimistic_l2_txn_batches' | 'optimistic_l2_deposits' |
'optimistic_l2_dispute_games' | 'optimistic_l2_dispute_games' |
'mud_worlds'| 'address_mud_tables' | 'address_mud_records' |
'shibarium_deposits' | 'shibarium_withdrawals' | 'shibarium_deposits' | 'shibarium_withdrawals' |
'arbitrum_l2_messages' | 'arbitrum_l2_txn_batches' | 'arbitrum_l2_txn_batch_txs' | 'arbitrum_l2_txn_batch_blocks' | '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' | '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 : ...@@ -1056,6 +1092,11 @@ Q extends 'user_op_interpretation'? TxInterpretationResponse :
Q extends 'noves_transaction' ? NovesResponseData : Q extends 'noves_transaction' ? NovesResponseData :
Q extends 'noves_address_history' ? NovesAccountHistoryResponse : Q extends 'noves_address_history' ? NovesAccountHistoryResponse :
Q extends 'noves_describe_txs' ? NovesDescribeTxsResponse : 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; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -1087,6 +1128,8 @@ Q extends 'addresses_lookup' ? EnsAddressLookupFilters : ...@@ -1087,6 +1128,8 @@ Q extends 'addresses_lookup' ? EnsAddressLookupFilters :
Q extends 'domains_lookup' ? EnsDomainLookupFilters : Q extends 'domains_lookup' ? EnsDomainLookupFilters :
Q extends 'user_ops' ? UserOpsFilters : Q extends 'user_ops' ? UserOpsFilters :
Q extends 'validators' ? ValidatorsFilters : Q extends 'validators' ? ValidatorsFilters :
Q extends 'address_mud_tables' ? AddressMudTablesFilter :
Q extends 'address_mud_records' ? AddressMudRecordsFilter :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -1099,5 +1142,6 @@ Q extends 'address_txs' ? TransactionsSorting : ...@@ -1099,5 +1142,6 @@ Q extends 'address_txs' ? TransactionsSorting :
Q extends 'addresses_lookup' ? EnsLookupSorting : Q extends 'addresses_lookup' ? EnsLookupSorting :
Q extends 'domains_lookup' ? EnsLookupSorting : Q extends 'domains_lookup' ? EnsLookupSorting :
Q extends 'validators' ? ValidatorsSorting : Q extends 'validators' ? ValidatorsSorting :
Q extends 'address_mud_records' ? AddressMudRecordsSorting :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
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 { ...@@ -102,6 +102,12 @@ export default function useNavItems(): ReturnType {
icon: 'games', icon: 'games',
isActive: pathname === '/dispute-games', isActive: pathname === '/dispute-games',
} : null; } : 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; const rollupFeature = config.features.rollup;
...@@ -121,6 +127,7 @@ export default function useNavItems(): ReturnType { ...@@ -121,6 +127,7 @@ export default function useNavItems(): ReturnType {
[ [
userOps, userOps,
topAccounts, topAccounts,
mudWorlds,
validators, validators,
verifiedContracts, verifiedContracts,
ensLookup, ensLookup,
......
...@@ -48,6 +48,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -48,6 +48,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/name-domains/[name]': 'Regular page', '/name-domains/[name]': 'Regular page',
'/validators': 'Root page', '/validators': 'Root page',
'/gas-tracker': 'Root page', '/gas-tracker': 'Root page',
'/mud-worlds': 'Root page',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': 'Regular page', '/login': 'Regular page',
......
...@@ -52,6 +52,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -52,6 +52,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/name-domains/[name]': DEFAULT_TEMPLATE, '/name-domains/[name]': DEFAULT_TEMPLATE,
'/validators': DEFAULT_TEMPLATE, '/validators': DEFAULT_TEMPLATE,
'/gas-tracker': DEFAULT_TEMPLATE, '/gas-tracker': DEFAULT_TEMPLATE,
'/mud-worlds': DEFAULT_TEMPLATE,
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': DEFAULT_TEMPLATE, '/login': DEFAULT_TEMPLATE,
......
...@@ -48,6 +48,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -48,6 +48,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/name-domains/[name]': '%network_name% %name% domain details', '/name-domains/[name]': '%network_name% %name% domain details',
'/validators': '%network_name% validators list', '/validators': '%network_name% validators list',
'/gas-tracker': '%network_name% gas tracker - Current gas fees', '/gas-tracker': '%network_name% gas tracker - Current gas fees',
'/mud-worlds': '%network_name% MUD worlds list',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': '%network_name% login', '/login': '%network_name% login',
......
...@@ -46,6 +46,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -46,6 +46,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/name-domains/[name]': 'Domain details', '/name-domains/[name]': 'Domain details',
'/validators': 'Validators list', '/validators': 'Validators list',
'/gas-tracker': 'Gas tracker', '/gas-tracker': 'Gas tracker',
'/mud-worlds': 'MUD worlds',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': 'Login', '/login': 'Login',
......
...@@ -275,3 +275,13 @@ export const disputeGames: GetServerSideProps<Props> = async(context) => { ...@@ -275,3 +275,13 @@ export const disputeGames: GetServerSideProps<Props> = async(context) => {
return base(context); return base(context);
}; };
export const mud: GetServerSideProps<Props> = async(context) => {
if (!config.features.mudFramework.isEnabled) {
return {
notFound: true,
};
}
return base(context);
};
...@@ -44,6 +44,7 @@ declare module "nextjs-routes" { ...@@ -44,6 +44,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/graphiql"> | StaticRoute<"/graphiql">
| StaticRoute<"/"> | StaticRoute<"/">
| StaticRoute<"/login"> | StaticRoute<"/login">
| StaticRoute<"/mud-worlds">
| DynamicRoute<"/name-domains/[name]", { "name": string }> | DynamicRoute<"/name-domains/[name]", { "name": string }>
| StaticRoute<"/name-domains"> | StaticRoute<"/name-domains">
| DynamicRoute<"/op/[hash]", { "hash": string }> | DynamicRoute<"/op/[hash]", { "hash": string }>
......
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';
...@@ -91,6 +91,7 @@ ...@@ -91,6 +91,7 @@
| "monaco/vyper" | "monaco/vyper"
| "moon-with-star" | "moon-with-star"
| "moon" | "moon"
| "MUD"
| "networks" | "networks"
| "networks/icon-placeholder" | "networks/icon-placeholder"
| "networks/logo-placeholder" | "networks/logo-placeholder"
......
...@@ -3,6 +3,7 @@ import type { ...@@ -3,6 +3,7 @@ import type {
AddressCoinBalanceHistoryItem, AddressCoinBalanceHistoryItem,
AddressCollection, AddressCollection,
AddressCounters, AddressCounters,
AddressMudTableItem,
AddressNFT, AddressNFT,
AddressTabsCounters, AddressTabsCounters,
AddressTokenBalance, AddressTokenBalance,
...@@ -10,6 +11,7 @@ import type { ...@@ -10,6 +11,7 @@ import type {
import type { AddressesItem } from 'types/api/addresses'; import type { AddressesItem } from 'types/api/addresses';
import { ADDRESS_HASH } from './addressParams'; 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 { 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'; import { TX_HASH } from './tx';
...@@ -109,3 +111,8 @@ export const ADDRESS_COLLECTION: AddressCollection = { ...@@ -109,3 +111,8 @@ export const ADDRESS_COLLECTION: AddressCollection = {
amount: '4', amount: '4',
token_instances: Array(4).fill(TOKEN_INSTANCE), token_instances: Array(4).fill(TOKEN_INSTANCE),
}; };
export const ADDRESS_MUD_TABLE_ITEM: AddressMudTableItem = {
schema: MUD_SCHEMA,
table: MUD_TABLE,
};
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'; ...@@ -3,6 +3,7 @@ import type { Transaction } from 'types/api/transaction';
import type { UserTags, AddressImplementation } from './addressParams'; import type { UserTags, AddressImplementation } from './addressParams';
import type { Block } from './block'; import type { Block } from './block';
import type { InternalTransaction } from './internalTransaction'; import type { InternalTransaction } from './internalTransaction';
import type { MudWorldSchema, MudWorldTable } from './mudWorlds';
import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token'; import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token';
import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer'; import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer';
...@@ -197,3 +198,56 @@ export type AddressTabsCounters = { ...@@ -197,3 +198,56 @@ export type AddressTabsCounters = {
validations_count: number | null; validations_count: number | null;
withdrawals_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;
}
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;
}
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;
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 React from 'react';
import { Element } from 'react-scroll'; import { Element } from 'react-scroll';
...@@ -10,7 +10,6 @@ import config from 'configs/app'; ...@@ -10,7 +10,6 @@ import config from 'configs/app';
import Tag from 'ui/shared/chakra/Tag'; import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import Hint from 'ui/shared/Hint'; import Hint from 'ui/shared/Hint';
import IconSvg from 'ui/shared/IconSvg';
import ContractMethodForm from './form/ContractMethodForm'; import ContractMethodForm from './form/ContractMethodForm';
import { getElementName } from './useScrollToMethod'; import { getElementName } from './useScrollToMethod';
...@@ -42,13 +41,9 @@ const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit }: Props) ...@@ -42,13 +41,9 @@ const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit }: Props)
}); });
}, [ addressHash, data, tab ]); }, [ addressHash, data, tab ]);
const { hasCopied, onCopy } = useClipboard(url, 1000);
const methodIdTooltip = useDisclosure();
const handleCopyLinkClick = React.useCallback((event: React.MouseEvent) => { const handleCopyLinkClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation(); event.stopPropagation();
onCopy(); }, []);
}, [ onCopy ]);
const handleCopyMethodIdClick = React.useCallback((event: React.MouseEvent) => { const handleCopyMethodIdClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation(); event.stopPropagation();
...@@ -64,21 +59,7 @@ const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit }: Props) ...@@ -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) : '' }> <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"> <AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }} wordBreak="break-all" textAlign="left" as="div" cursor="pointer">
{ 'method_id' in data && ( { 'method_id' in data && <CopyToClipboard text={ url } onClick={ handleCopyLinkClick } type="link" mr={ 2 } color="text_secondary"/> }
<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>
) }
<Box as="span" fontWeight={ 500 } mr={ 1 }> <Box as="span" fontWeight={ 500 } mr={ 1 }>
{ index + 1 }. { data.type === 'fallback' || data.type === 'receive' ? data.type : data.name } { index + 1 }. { data.type === 'fallback' || data.type === 'receive' ? data.type : data.name }
</Box> </Box>
......
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));
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;
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;
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;
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;
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;
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;
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);
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 + ')';
};
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);
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;
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;
...@@ -23,6 +23,7 @@ import AddressContract from 'ui/address/AddressContract'; ...@@ -23,6 +23,7 @@ import AddressContract from 'ui/address/AddressContract';
import AddressDetails from 'ui/address/AddressDetails'; import AddressDetails from 'ui/address/AddressDetails';
import AddressInternalTxs from 'ui/address/AddressInternalTxs'; import AddressInternalTxs from 'ui/address/AddressInternalTxs';
import AddressLogs from 'ui/address/AddressLogs'; import AddressLogs from 'ui/address/AddressLogs';
import AddressMud from 'ui/address/AddressMud';
import AddressTokens from 'ui/address/AddressTokens'; import AddressTokens from 'ui/address/AddressTokens';
import AddressTokenTransfers from 'ui/address/AddressTokenTransfers'; import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
import AddressTxs from 'ui/address/AddressTxs'; import AddressTxs from 'ui/address/AddressTxs';
...@@ -77,6 +78,14 @@ const AddressPageContent = () => { ...@@ -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 addressesForMetadataQuery = React.useMemo(() => ([ hash ].filter(Boolean)), [ hash ]);
const addressMetadataQuery = useAddressMetadataInfoQuery(addressesForMetadataQuery, areQueriesEnabled); const addressMetadataQuery = useAddressMetadataInfoQuery(addressesForMetadataQuery, areQueriesEnabled);
...@@ -98,7 +107,7 @@ const AddressPageContent = () => { ...@@ -98,7 +107,7 @@ const AddressPageContent = () => {
undefined; undefined;
const isLoading = addressQuery.isPlaceholderData || (config.features.userOps.isEnabled && userOpsAccountQuery.isPlaceholderData); 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(() => { const handleFetchedBytecodeMessage = React.useCallback(() => {
addressQuery.refetch(); addressQuery.refetch();
...@@ -121,6 +130,12 @@ const AddressPageContent = () => { ...@@ -121,6 +130,12 @@ const AddressPageContent = () => {
const tabs: Array<RoutedTab> = React.useMemo(() => { const tabs: Array<RoutedTab> = React.useMemo(() => {
return [ 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', id: 'txs',
title: 'Transactions', title: 'Transactions',
...@@ -215,7 +230,15 @@ const AddressPageContent = () => { ...@@ -215,7 +230,15 @@ const AddressPageContent = () => {
subTabs: contractTabs.tabs.map(tab => tab.id), subTabs: contractTabs.tabs.map(tab => tab.id),
} : undefined, } : undefined,
].filter(Boolean); ].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(() => { const tags: Array<EntityTag> = React.useMemo(() => {
return [ return [
...@@ -229,10 +252,13 @@ const AddressPageContent = () => { ...@@ -229,10 +252,13 @@ const AddressPageContent = () => {
config.features.userOps.isEnabled && userOpsAccountQuery.data ? config.features.userOps.isEnabled && userOpsAccountQuery.data ?
{ slug: 'user_ops_acc', name: 'Smart contract wallet', tagType: 'custom' as const, ordinal: -10 } : { slug: 'user_ops_acc', name: 'Smart contract wallet', tagType: 'custom' as const, ordinal: -10 } :
undefined, undefined,
config.features.mudFramework.isEnabled && mudTablesCountQuery.data ?
{ slug: 'mud', name: 'MUD World', tagType: 'custom' as const, ordinal: -10 } :
undefined,
...formatUserTags(addressQuery.data), ...formatUserTags(addressQuery.data),
...(addressMetadataQuery.data?.addresses?.[hash.toLowerCase()]?.tags || []), ...(addressMetadataQuery.data?.addresses?.[hash.toLowerCase()]?.tags || []),
].filter(Boolean).sort(sortEntityTags); ].filter(Boolean).sort(sortEntityTags);
}, [ addressMetadataQuery.data, addressQuery.data, hash, isSafeAddress, userOpsAccountQuery.data ]); }, [ addressMetadataQuery.data, addressQuery.data, hash, isSafeAddress, userOpsAccountQuery.data, mudTablesCountQuery.data ]);
const titleContentAfter = ( const titleContentAfter = (
<EntityTags <EntityTags
......
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 { ...@@ -9,9 +9,10 @@ export interface Props {
isLoading?: boolean; isLoading?: boolean;
onClick?: (event: React.MouseEvent) => void; onClick?: (event: React.MouseEvent) => void;
size?: number; 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 { hasCopied, onCopy } = useClipboard(text, 1000);
const [ copied, setCopied ] = useState(false); const [ copied, setCopied ] = useState(false);
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107 // 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 ...@@ -36,10 +37,10 @@ const CopyToClipboard = ({ text, className, isLoading, onClick, size = 5 }: Prop
} }
return ( 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 <IconButton
aria-label="copy" aria-label="copy"
icon={ <IconSvg name="copy" boxSize={ size }/> } icon={ <IconSvg name={ type === 'link' ? 'link' : 'copy' } boxSize={ size }/> }
boxSize={ size } boxSize={ size }
color={ iconColor } color={ iconColor }
variant="simple" variant="simple"
......
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);
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 { ...@@ -8,7 +8,7 @@ interface Props {
const Container = ({ children, className }: Props) => { const Container = ({ children, className }: Props) => {
return ( return (
<Box className={ className } minWidth={{ base: '100vw', lg: 'fit-content' }}> <Box className={ className } minWidth={{ base: '100vw', lg: 'auto' }}>
{ children } { children }
</Box> </Box>
); );
......
...@@ -3,8 +3,20 @@ export default function getNextSortValue<SortField extends string, Sort extends ...@@ -3,8 +3,20 @@ export default function getNextSortValue<SortField extends string, Sort extends
) { ) {
return (prevValue: Sort | undefined) => { return (prevValue: Sort | undefined) => {
const sequence = sortSequence[field]; const sequence = sortSequence[field];
getNextValueFromSequence(sequence, prevValue);
const curIndex = sequence.findIndex((sort) => sort === prevValue); const curIndex = sequence.findIndex((sort) => sort === prevValue);
const nextIndex = curIndex + 1 > sequence.length - 1 ? 0 : curIndex + 1; const nextIndex = curIndex + 1 > sequence.length - 1 ? 0 : curIndex + 1;
return sequence[nextIndex]; 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);
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);
}
import { TagLabel, Tooltip, chakra } from '@chakra-ui/react'; import { TagLabel, Tooltip, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import capitalizeFirstLetter from 'lib/capitalizeFirstLetter';
import Tag from 'ui/shared/chakra/Tag'; import Tag from 'ui/shared/chakra/Tag';
import type { IconName } from 'ui/shared/IconSvg'; import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
...@@ -19,7 +20,7 @@ const StatusTag = ({ type, text, errorText, isLoading, className }: Props) => { ...@@ -19,7 +20,7 @@ const StatusTag = ({ type, text, errorText, isLoading, className }: Props) => {
let icon: IconName; let icon: IconName;
let colorScheme; let colorScheme;
const capitalizedText = text.charAt(0).toUpperCase() + text.slice(1); const capitalizedText = capitalizeFirstLetter(text);
switch (type) { switch (type) {
case 'ok': case 'ok':
......
import capitalizeFirstLetter from 'lib/capitalizeFirstLetter';
export function camelCaseToSentence(camelCaseString: string | undefined) { export function camelCaseToSentence(camelCaseString: string | undefined) {
if (!camelCaseString) { if (!camelCaseString) {
return ''; return '';
...@@ -5,7 +7,7 @@ export function camelCaseToSentence(camelCaseString: string | undefined) { ...@@ -5,7 +7,7 @@ export function camelCaseToSentence(camelCaseString: string | undefined) {
let sentence = camelCaseString.replace(/([a-z])([A-Z])/g, '$1 $2'); 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.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2');
sentence = sentence.charAt(0).toUpperCase() + sentence.slice(1); sentence = capitalizeFirstLetter(sentence);
return sentence; return sentence;
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment