Commit 0845994e authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into search-bar

parents a057416a 1fb04b96
......@@ -76,7 +76,7 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT | `string` *(optional)* | Gradient value for hero plate on the homepage | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 72% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` *(optional)* | Set to false if network doesn't have gas tracker | `true` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` *(optional)* | Set to false if average block time is useless for the network | `true` |
| NEXT_PUBLIC_DOMAIN_WITH_AD | `string` *(optional)* | The domain on which we display ads | `blockscout.com` |
| NEXT_PUBLIC_DOMAIN_WITH_AD | `string` *(optional)* | The domain on which we display ads | `blockscout.com` |
| NEXT_PUBLIC_AD_ADBUTLER_ON | `boolean` *(optional)* | Set to true to show Adbutler banner instead of Coinzilla banner | `false` |
### App configuration
......@@ -132,7 +132,7 @@ The app instance could be customized by passing following variables to NodeJS en
| id | `string` | Used as slug for the app. Must be unique in the app list. | `'app'` |
| external | `boolean` | If true means that the application opens in a new window, but not in an iframe. | `true` |
| title | `string` | Displayed title of the app. | `'The App'` |
| logo | `string` | URL to logo file. Should be at least 144x144. | `'https://foo.app/icon.png'` |
| logo | `string` | URL to logo file. Should be at least 288x288. | `'https://foo.app/icon.png'` |
| shortDescription | `string` | Displayed only in the app list. | `'Awesome app'` |
| categories | `Array<MarketplaceCategoryId>` | Displayed category. Select one of the following bellow. | `['security', 'tools']` |
| author | `string` | Displayed author of the app | `'Bob'` |
......
......@@ -14,6 +14,7 @@ import type {
} from 'types/api/address';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { SmartContract } from 'types/api/contract';
import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl';
......@@ -21,7 +22,7 @@ import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search';
import type { Stats, Charts, HomeStats } from 'types/api/stats';
import type { TokenCounters, TokenInfo } from 'types/api/tokenInfo';
import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
......@@ -164,6 +165,11 @@ export const RESOURCES = {
filterFields: [ ],
},
// CONTRACT
contract: {
path: '/api/v2/smart-contracts/:id',
},
// TOKEN
token: {
path: '/api/v2/tokens/:hash',
......@@ -171,6 +177,11 @@ export const RESOURCES = {
token_counters: {
path: '/api/v2/tokens/:hash/counters',
},
token_holders: {
path: '/api/v2/tokens/:hash/holders',
paginationFields: [ 'items_count' as const, 'value' as const ],
filterFields: [],
},
// HOMEPAGE
homepage_stats: {
......@@ -245,7 +256,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'address_logs' |
'search';
'search' |
'token_holders';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
......@@ -288,8 +300,10 @@ Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'token' ? TokenInfo :
Q extends 'token_counters' ? TokenCounters :
Q extends 'token_holders' ? TokenHolders :
Q extends 'config_json_rpc' ? JsonRpcUrlResponse :
Q extends 'search' ? SearchResult :
Q extends 'contract' ? SmartContract :
never;
/* eslint-enable @typescript-eslint/indent */
......
......@@ -5,7 +5,7 @@ import omit from 'lodash/omit';
import pick from 'lodash/pick';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { animateScroll, scroller } from 'react-scroll';
import { animateScroll } from 'react-scroll';
import type { PaginatedResources, PaginatedResponse, PaginationFilters } from 'lib/api/resources';
import { RESOURCES } from 'lib/api/resources';
......@@ -17,7 +17,7 @@ interface Params<Resource extends PaginatedResources> {
options?: UseApiQueryParams<Resource>['queryOptions'];
pathParams?: UseApiQueryParams<Resource>['pathParams'];
filters?: PaginationFilters<Resource>;
scroll?: { elem: string; offset: number };
scrollRef?: React.RefObject<HTMLDivElement>;
}
export default function useQueryWithPages<Resource extends PaginatedResources>({
......@@ -25,7 +25,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
filters,
options,
pathParams,
scroll,
scrollRef,
}: Params<Resource>) {
const resource = RESOURCES[resourceName];
const queryClient = useQueryClient();
......@@ -47,8 +47,8 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const queryParams = { ...pageParams[page], ...filters };
const scrollToTop = useCallback(() => {
scroll ? scroller.scrollTo(scroll.elem, { offset: scroll.offset }) : animateScroll.scrollToTop({ duration: 0 });
}, [ scroll ]);
scrollRef?.current ? scrollRef.current.scrollIntoView(true) : animateScroll.scrollToTop({ duration: 0 });
}, [ scrollRef ]);
const queryResult = useApiQuery(resourceName, {
pathParams,
......@@ -78,10 +78,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
nextPageQuery.page = String(page + 1);
setHasPagination(true);
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => {
scrollToTop();
});
scrollToTop();
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true });
}, [ data?.next_page_params, page, router, scrollToTop ]);
const onPrevPageClick = useCallback(() => {
......@@ -97,9 +96,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
nextPageQuery.page = String(page - 1);
}
scrollToTop();
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => {
scrollToTop();
setPage(prev => prev - 1);
page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] });
});
......@@ -109,10 +108,10 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const resetPage = useCallback(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] });
scrollToTop();
const nextRouterQuery = omit(router.query, difference(resource.paginationFields, resource.filterFields), 'page');
router.push({ pathname: router.pathname, query: nextRouterQuery }, undefined, { shallow: true }).then(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] });
scrollToTop();
setPage(1);
setPageParams({});
canGoBackwards.current = true;
......@@ -136,7 +135,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
});
}
setHasPagination(false);
scrollToTop();
router.push(
{
pathname: router.pathname,
......@@ -147,7 +146,6 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
).then(() => {
setPage(1);
setPageParams({});
scrollToTop();
});
}, [ router, resource.paginationFields, resource.filterFields, scrollToTop ]);
......
......@@ -18,7 +18,7 @@
"apps": "/apps",
"app_index": "/apps/:id",
"search_results": "/search-results",
"other": "/search-results",
"auth": "/auth/auth0",
"stats": "/stats"
"stats": "/stats",
"visualize_sol2uml": "/visualize/sol2uml"
}
......@@ -94,9 +94,9 @@ export const ROUTES = {
pattern: PATHS.search_results,
},
// ??? what URL will be here
other: {
pattern: PATHS.other,
// VISUALIZE
visualize_sol2uml: {
pattern: PATHS.visualize_sol2uml,
},
// AUTH
......
import type { TokenHolders } from 'types/api/tokenInfo';
import { withName, withoutName } from 'mocks/address/address';
export const tokenHolders: TokenHolders = {
items: [
{
address: withName,
value: '107014805905725000000',
},
{
address: withoutName,
value: '207014805905725000000',
},
],
next_page_params: {
value: '50',
items_count: 50,
},
};
......@@ -6,6 +6,8 @@ import { mode } from '@chakra-ui/theme-tools';
const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys);
import Button from './Button/Button';
const variantSoftRounded = definePartsStyle((props) => {
return {
tab: {
......@@ -26,11 +28,32 @@ const variantSoftRounded = definePartsStyle((props) => {
};
});
const variantOutline = definePartsStyle((props) => {
return {
tab: {
...Button.variants?.outline(props),
...Button.baseStyle,
_selected: Button.variants?.outline(props)._active,
},
};
});
const sizes = {
sm: definePartsStyle({
tab: Button.sizes?.sm,
}),
md: definePartsStyle({
tab: Button.sizes?.md,
}),
};
const variants = {
'soft-rounded': variantSoftRounded,
outline: variantOutline,
};
const Tabs = defineMultiStyleConfig({
sizes,
variants,
});
......
export interface SmartContract {
deployed_bytecode: string | null;
creation_bytecode: string | null;
is_self_destructed: boolean;
abi: Array<Record<string, unknown>> | null;
compiler_version: string | null;
evm_version: string | null;
optimization_enabled: boolean | null;
optimization_runs: number | null;
name: string | null;
verified_at: string | null;
is_verified: boolean | null;
source_code: string | null;
can_be_visualized_via_sol2uml: boolean | null;
}
import type { AddressParam } from './addressParams';
export type TokenType = 'ERC-20' | 'ERC-721' | 'ERC-1155';
export interface TokenInfo {
......@@ -17,3 +19,18 @@ export interface TokenCounters {
}
export type TokenInfoGeneric<Type extends TokenType> = Omit<TokenInfo, 'type'> & { type: Type };
export interface TokenHolders {
items: Array<TokenHolder>;
next_page_params: TokenHoldersPagination;
}
export type TokenHolder = {
address: AddressParam;
value: string;
}
export type TokenHoldersPagination = {
items_count: number;
value: string;
}
import React from 'react';
import type { RoutedSubTab } from 'ui/shared/RoutedTabs/types';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
interface Props {
tabs: Array<RoutedSubTab>;
}
const AddressContract = ({ tabs }: Props) => {
return <RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={{ columnGap: 3 }}/>;
};
export default React.memo(AddressContract);
......@@ -2,7 +2,6 @@ import { Text, Show, Hide } from '@chakra-ui/react';
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router';
import React from 'react';
import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
......@@ -21,12 +20,9 @@ import Pagination from 'ui/shared/Pagination';
import AddressTxsFilter from './AddressTxsFilter';
import AddressIntTxsList from './internals/AddressIntTxsList';
const SCROLL_ELEM = 'address-internas-txs';
const SCROLL_OFFSET = -100;
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const AddressInternalTxs = () => {
const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter();
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter));
......@@ -38,7 +34,7 @@ const AddressInternalTxs = () => {
resourceName: 'address_internal_txs',
pathParams: { id: queryIdStr },
filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
scrollRef,
});
const handleFilterChange = React.useCallback((val: string | Array<string>) => {
......@@ -83,7 +79,7 @@ const AddressInternalTxs = () => {
}
return (
<Element name={ SCROLL_ELEM }>
<>
<ActionBar mt={ -6 }>
<AddressTxsFilter
defaultFilter={ filterValue }
......@@ -93,7 +89,7 @@ const AddressInternalTxs = () => {
{ isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
</ActionBar>
{ content }
</Element>
</>
);
};
......
import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import { Element } from 'react-scroll';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import ActionBar from 'ui/shared/ActionBar';
......@@ -10,19 +9,14 @@ import LogItem from 'ui/shared/logs/LogItem';
import LogSkeleton from 'ui/shared/logs/LogSkeleton';
import Pagination from 'ui/shared/Pagination';
const SCROLL_PARAMS = {
elem: 'address-logs',
offset: -100,
};
const AddressLogs = () => {
const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter();
const addressHash = String(router.query?.id);
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_logs',
pathParams: { id: addressHash },
scroll: SCROLL_PARAMS,
scrollRef,
});
if (isError) {
......@@ -50,10 +44,10 @@ const AddressLogs = () => {
}
return (
<Element name={ SCROLL_PARAMS.elem }>
<>
{ bar }
{ data.items.map((item, index) => <LogItem key={ index } { ...item } type="address"/>) }
</Element>
</>
);
};
......
......@@ -3,7 +3,7 @@ import React from 'react';
import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer';
const AddressTokenTransfers = () => {
const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter();
const hash = router.query.id;
......@@ -13,6 +13,7 @@ const AddressTokenTransfers = () => {
pathParams={{ id: hash?.toString() }}
baseAddress={ typeof hash === 'string' ? hash : undefined }
enableTimeIncrement
scrollRef={ scrollRef }
/>
);
};
......
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router';
import React from 'react';
import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
......@@ -17,10 +16,7 @@ import AddressTxsFilter from './AddressTxsFilter';
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const SCROLL_ELEM = 'address-txs';
const SCROLL_OFFSET = -100;
const AddressTxs = () => {
const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter();
const isMobile = useIsMobile();
......@@ -31,7 +27,7 @@ const AddressTxs = () => {
resourceName: 'address_txs',
pathParams: { id: castArray(router.query.id)[0] },
filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
scrollRef,
});
const handleFilterChange = React.useCallback((val: string | Array<string>) => {
......@@ -50,7 +46,7 @@ const AddressTxs = () => {
);
return (
<Element name={ SCROLL_ELEM }>
<>
{ !isMobile && (
<ActionBar mt={ -6 }>
{ filter }
......@@ -64,7 +60,7 @@ const AddressTxs = () => {
currentAddress={ typeof router.query.id === 'string' ? router.query.id : undefined }
enableTimeIncrement
/>
</Element>
</>
);
};
......
......@@ -24,6 +24,10 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
return <DataFetchAlert/>;
}
if (!items?.length) {
return null;
}
return (
<ChartWidget
chartHeight="200px"
......
import { Flex, Skeleton, Button, Grid, GridItem, Text } from '@chakra-ui/react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import link from 'lib/link/link';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
const DynamicContractSourceCode = dynamic(
() => import('./ContractSourceCode'),
{ ssr: false },
);
const InfoItem = ({ label, value }: { label: string; value: string }) => (
<GridItem display="flex" columnGap={ 6 }>
<Text w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Text>
<Text wordBreak="break-all">{ value }</Text>
</GridItem>
);
const ContractCode = () => {
const router = useRouter();
const { data, isLoading, isError } = useApiQuery('contract', {
pathParams: { id: router.query.id?.toString() },
queryOptions: {
enabled: Boolean(router.query.id),
},
});
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Flex justifyContent="space-between" mb={ 2 }>
<Skeleton w="180px" h={ 5 } borderRadius="full"/>
<Skeleton w={ 5 } h={ 5 }/>
</Flex>
<Skeleton w="100%" h="250px" borderRadius="md"/>
<Flex justifyContent="space-between" mb={ 2 } mt={ 6 }>
<Skeleton w="180px" h={ 5 } borderRadius="full"/>
<Skeleton w={ 5 } h={ 5 }/>
</Flex>
<Skeleton w="100%" h="400px" borderRadius="md"/>
</>
);
}
const verificationButton = (
<Button
size="sm"
ml="auto"
mr={ 3 }
as="a"
href={ link('address_contract_verification', { id: router.query.id?.toString() }) }
>
Verify & publish
</Button>
);
return (
<>
{ data.is_verified && (
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
{ data.name && <InfoItem label="Contract name" value={ data.name }/> }
{ data.compiler_version && <InfoItem label="Compiler version" value={ data.compiler_version }/> }
{ data.evm_version && <InfoItem label="EVM version" value={ data.evm_version }/> }
{ typeof data.optimization_enabled === 'boolean' && <InfoItem label="Optimization enabled" value={ data.optimization_enabled ? 'true' : 'false' }/> }
{ data.optimization_runs && <InfoItem label="Optimization runs" value={ String(data.optimization_runs) }/> }
{ data.verified_at && <InfoItem label="Verified at" value={ data.verified_at }/> }
</Grid>
) }
<Flex flexDir="column" rowGap={ 6 }>
{ data.source_code && (
<DynamicContractSourceCode
data={ data.source_code }
hasSol2Yml={ Boolean(data.can_be_visualized_via_sol2uml) }
address={ router.query.id?.toString() }
/>
) }
{ data.abi && (
<RawDataSnippet
data={ JSON.stringify(data.abi) }
title="Contract ABI"
textareaMinHeight="200px"
/>
) }
{ data.creation_bytecode && (
<RawDataSnippet
data={ data.creation_bytecode }
title="Contract creation code"
rightSlot={ data.is_verified ? null : verificationButton }
/>
) }
{ data.deployed_bytecode && (
<RawDataSnippet
data={ data.deployed_bytecode }
title="Deployed ByteCode"
/>
) }
</Flex>
</>
);
};
export default ContractCode;
import { Box, Flex, Link, Text, Tooltip } from '@chakra-ui/react';
import React from 'react';
import link from 'lib/link/link';
import CodeEditor from 'ui/shared/CodeEditor';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props {
data: string;
hasSol2Yml: boolean;
address?: string;
}
const ContractSourceCode = ({ data, hasSol2Yml, address }: Props) => {
return (
<Box>
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
<Text fontWeight={ 500 }>Contract source code</Text>
{ hasSol2Yml && address && (
<Tooltip label="Visualize contract code using Sol2Uml JS library">
<Link
href={ link('visualize_sol2uml', undefined, { address }) }
ml="auto"
mr={ 3 }
>
View Sol2uml
</Link>
</Tooltip>
) }
<CopyToClipboard text={ data }/>
</Flex>
<CodeEditor value={ data } id="source_code"/>
</Box>
);
};
export default React.memo(ContractSourceCode);
import { Flex, Skeleton, Tag } from '@chakra-ui/react';
import { Flex, Skeleton, Tag, Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -8,20 +8,33 @@ import useApiQuery from 'lib/api/useApiQuery';
import notEmpty from 'lib/notEmpty';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance';
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 AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
import AddressTxs from 'ui/address/AddressTxs';
import AddressLogs from 'ui/address/logs/AddressLogs';
import ContractCode from 'ui/address/contract/ContractCode';
import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
const CONTRACT_TABS = [
{ id: 'contact_code', title: 'Code', component: <ContractCode/> },
{ id: 'contact_decompiled_code', title: 'Decompiled code', component: <div>Decompiled code</div> },
{ id: 'read_contract', title: 'Read contract', component: <div>Read contract</div> },
{ id: 'read_proxy', title: 'Read proxy', component: <div>Read proxy</div> },
{ id: 'write_contract', title: 'Write contract', component: <div>Write contract</div> },
{ id: 'write_proxy', title: 'Write proxy', component: <div>Write proxy</div> },
];
const AddressPageContent = () => {
const router = useRouter();
const tabsScrollRef = React.useRef<HTMLDivElement>(null);
const addressQuery = useApiQuery('address', {
pathParams: { id: router.query.id?.toString() },
queryOptions: { enabled: Boolean(router.query.id) },
......@@ -37,15 +50,21 @@ const AddressPageContent = () => {
const tabs: Array<RoutedTab> = React.useMemo(() => {
return [
{ id: 'txs', title: 'Transactions', component: <AddressTxs/> },
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers/> },
{ id: 'txs', title: 'Transactions', component: <AddressTxs scrollRef={ tabsScrollRef }/> },
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> },
{ id: 'tokens', title: 'Tokens', component: null },
{ id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs/> },
{ id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs scrollRef={ tabsScrollRef }/> },
{ id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance/> },
// temporary show this tab in all address
// later api will return info about available tabs
{ id: 'blocks_validated', title: 'Blocks validated', component: <AddressBlocksValidated/> },
isContract ? { id: 'logs', title: 'Logs', component: <AddressLogs/> } : undefined,
isContract ? { id: 'logs', title: 'Logs', component: <AddressLogs scrollRef={ tabsScrollRef }/> } : undefined,
isContract ? {
id: 'contract',
title: 'Contract',
component: <AddressContract tabs={ CONTRACT_TABS }/>,
subTabs: CONTRACT_TABS,
} : undefined,
].filter(notEmpty);
}, [ isContract ]);
......@@ -63,6 +82,8 @@ const AddressPageContent = () => {
/>
) }
<AddressDetails addressQuery={ addressQuery }/>
{ /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/> }
</Page>
);
......
import { Skeleton } from '@chakra-ui/react';
import { Skeleton, Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import { Element } from 'react-scroll';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TokenContractInfo from 'ui/token/TokenContractInfo';
import TokenDetails from 'ui/token/TokenDetails';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
export type TokenTabs = 'token_transfers' | 'holders'
const TokenPageContent = () => {
const router = useRouter();
const isMobile = useIsMobile();
const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenQuery = useApiQuery('token', {
pathParams: { hash: router.query.hash?.toString() },
queryOptions: { enabled: Boolean(router.query.hash) },
});
// const transfersQuery = useQueryWithPages({
// resourceName: 'token_transfers',
// pathParams: { hash: router.query.hash?.toString() },
// options: {
// enabled: Boolean(router.query.hash && router.query.tab === 'holders' && tokenQuery.data),
// },
// });
const holdersQuery = useQueryWithPages({
resourceName: 'token_holders',
pathParams: { hash: router.query.hash?.toString() },
scrollRef,
options: {
enabled: Boolean(router.query.hash && router.query.tab === 'holders' && tokenQuery.data),
},
});
const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: null },
{ id: 'holders', title: 'Holders', component: null },
{ id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> },
];
let hasPagination;
let pagination;
// if (router.query.tab === 'token_transfers') {
// hasPagination = transfersQuery.isPaginationVisible;
// pagination = transfersQuery.pagination;
// }
if (router.query.tab === 'holders') {
hasPagination = holdersQuery.isPaginationVisible;
pagination = holdersQuery.pagination;
}
return (
<Page>
{ tokenQuery.isLoading ?
......@@ -34,7 +69,16 @@ const TokenPageContent = () => {
<PageTitle text={ `${ tokenQuery.data?.name } (${ tokenQuery.data?.symbol }) token` }/> }
<TokenContractInfo tokenQuery={ tokenQuery }/>
<TokenDetails tokenQuery={ tokenQuery }/>
<Element name="token-tabs"><RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/></Element>
{ /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ scrollRef }></Box>
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null }
stickyEnabled={ !isMobile }
/>
</Page>
);
};
......
import { chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import AceEditor from 'react-ace';
import 'ace-builds/src-noconflict/mode-javascript';
import 'ace-builds/src-noconflict/theme-tomorrow';
import 'ace-builds/src-noconflict/theme-tomorrow_night';
import 'ace-builds/src-noconflict/ext-language_tools';
interface Props {
id: string;
value: string;
className?: string;
}
const CodeEditorBase = chakra(({ id, value, className }: Props) => {
const theme = useColorModeValue('tomorrow', 'tomorrow_night');
return (
<AceEditor
className={ className }
mode="javascript" // TODO need to find mode for solidity
theme={ theme }
value={ value }
name={ id }
editorProps={{ $blockScrolling: true }}
readOnly
width="100%"
showPrintMargin={ false }
maxLines={ 25 }
/>
);
});
const CodeEditor = ({ id, value }: Props) => {
// see theme/components/Textarea.ts variantFilledInactive
const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b');
const gutterBgColor = useColorModeValue('gray.100', '#25282c');
return (
<CodeEditorBase
id={ id }
value={ value }
bgColor={ bgColor }
borderRadius="md"
overflow="hidden"
sx={{
'.ace_gutter': {
backgroundColor: gutterBgColor,
},
}}
/>
);
};
export default React.memo(CodeEditor);
import { Box, Flex, Text, Textarea, chakra } from '@chakra-ui/react';
import React from 'react';
import CopyToClipboard from './CopyToClipboard';
interface Props {
data: string;
title?: string;
className?: string;
rightSlot?: React.ReactNode;
textareaMinHeight?: string;
}
const RawDataSnippet = ({ data, className, title, rightSlot, textareaMinHeight }: Props) => {
return (
<Box className={ className }>
<Flex justifyContent={ title ? 'space-between' : 'flex-end' } alignItems="center" mb={ 3 }>
{ title && <Text fontWeight={ 500 }>{ title }</Text> }
{ rightSlot }
<CopyToClipboard text={ data }/>
</Flex>
<Textarea
variant="filledInactive"
p={ 4 }
minHeight={ textareaMinHeight || '400px' }
value={ data }
fontSize="sm"
borderRadius="md"
readOnly
/>
</Box>
);
};
export default React.memo(chakra(RawDataSnippet));
import type { ChakraProps } from '@chakra-ui/react';
import type { ChakraProps, ThemingProps } from '@chakra-ui/react';
import {
Tab,
Tabs,
......@@ -7,6 +7,7 @@ import {
TabPanels,
Box,
useColorModeValue,
chakra,
} from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system';
import { useRouter } from 'next/router';
......@@ -28,14 +29,15 @@ const hiddenItemStyles: StyleProps = {
visibility: 'hidden',
};
interface Props {
interface Props extends ThemingProps<'Tabs'> {
tabs: Array<RoutedTab>;
tabListProps?: ChakraProps;
rightSlot?: React.ReactNode;
stickyEnabled?: boolean;
className?: string;
}
const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled }: Props) => {
const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, ...themeProps }: Props) => {
const router = useRouter();
const scrollDirection = useScrollDirection();
const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1);
......@@ -58,8 +60,9 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled }: Props) =>
useEffect(() => {
if (router.isReady) {
let tabIndex = 0;
if (router.query.tab) {
tabIndex = tabs.findIndex(({ id }) => id === router.query.tab);
const tabFromRoute = router.query.tab;
if (tabFromRoute) {
tabIndex = tabs.findIndex(({ id, subTabs }) => id === tabFromRoute || subTabs?.some(({ id }) => id === tabFromRoute));
if (tabIndex < 0) {
tabIndex = 0;
}
......@@ -89,12 +92,14 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled }: Props) =>
return (
<Tabs
variant="soft-rounded"
colorScheme="blue"
className={ className }
variant={ themeProps.variant || 'soft-rounded' }
colorScheme={ themeProps.colorScheme || 'blue' }
isLazy
onChange={ handleTabChange }
index={ activeTabIndex }
position="relative"
size={ themeProps.size || 'md' }
>
<TabList
marginBottom={{ base: 6, lg: 8 }}
......@@ -155,6 +160,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled }: Props) =>
ref={ tabsRefs[index] }
{ ...(index < tabsCut ? {} : hiddenItemStyles) }
scrollSnapAlign="start"
flexShrink={ 0 }
>
{ tab.title }
</Tab>
......@@ -169,4 +175,4 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled }: Props) =>
);
};
export default React.memo(RoutedTabs);
export default React.memo(chakra(RoutedTabs));
......@@ -2,8 +2,11 @@ export interface RoutedTab {
id: string;
title: string;
component: React.ReactNode;
subTabs?: Array<RoutedSubTab>;
}
export type RoutedSubTab = Omit<RoutedTab, 'subTabs'>;
export interface MenuButton {
id: null;
title: string;
......
import { Hide, Show, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
......@@ -28,9 +27,6 @@ import { TOKEN_TYPE } from './helpers';
const TOKEN_TYPES = TOKEN_TYPE.map(i => i.id);
const SCROLL_ELEM = 'token-transfers';
const SCROLL_OFFSET = -100;
const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPES);
const getAddressFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
......@@ -43,6 +39,7 @@ interface Props<Resource extends 'tx_token_transfers' | 'address_token_transfers
txHash?: string;
enableTimeIncrement?: boolean;
pathParams?: UseApiQueryParams<Resource>['pathParams'];
scrollRef?: React.RefObject<HTMLDivElement>;
}
type State = {
......@@ -58,6 +55,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr
showTxInfo = true,
enableTimeIncrement,
pathParams,
scrollRef,
}: Props<Resource>) => {
const router = useRouter();
const [ filters, setFilters ] = React.useState<State>(
......@@ -69,7 +67,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr
pathParams,
options: { enabled: !isDisabled },
filters: filters as PaginationFilters<Resource>,
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
scrollRef,
});
const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => {
......@@ -129,7 +127,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr
})();
return (
<Element name={ SCROLL_ELEM }>
<>
{ !isActionBarHidden && (
<ActionBar mt={ -6 }>
<TokenTransferFilter
......@@ -144,7 +142,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr
</ActionBar>
) }
{ content }
</Element>
</>
);
};
......
import { Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { TokenHolders, TokenInfo } from 'types/api/tokenInfo';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import TokenHoldersList from './TokenHoldersList';
import TokenHoldersTable from './TokenHoldersTable';
type Props = {
tokenQuery: UseQueryResult<TokenInfo>;
holdersQuery: UseQueryResult<TokenHolders> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
}
const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => {
const isMobile = useIsMobile();
if (holdersQuery.isError || tokenQuery.isError) {
return <DataFetchAlert/>;
}
const bar = isMobile && holdersQuery.isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...holdersQuery.pagination }/>
</ActionBar>
);
if (holdersQuery.isLoading || tokenQuery.isLoading) {
return (
<>
{ bar }
{ isMobile && <SkeletonList/> }
{ !isMobile && (
<SkeletonTable columns={ [ '100%', '300px', '175px' ] }/>
) }
</>
);
}
const items = holdersQuery.data.items;
if (!items?.length) {
return <Text as="span">There are no holders for this token.</Text>;
}
return (
<>
{ bar }
{ !isMobile && <TokenHoldersTable data={ items } token={ tokenQuery.data }/> }
{ isMobile && <TokenHoldersList data={ items } token={ tokenQuery.data }/> }
</>
);
};
export default TokenHoldersContent;
import { test, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react';
import { tokenHolders } from 'mocks/tokens/tokenHolders';
import { tokenInfo } from 'mocks/tokens/tokenInfo';
import TestApp from 'playwright/TestApp';
import TokenHoldersList from './TokenHoldersList';
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ mount }) => {
const component = await mount(
<TestApp>
<TokenHoldersList data={ tokenHolders.items } token={ tokenInfo }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo';
import TokenHoldersListItem from './TokenHoldersListItem';
interface Props {
data: Array<TokenHolder>;
token: TokenInfo;
}
const TokenHoldersList = ({ data, token }: Props) => {
return (
<Box>
{ data.map((item) => (
<TokenHoldersListItem
key={ item.address.hash }
token={ token }
holder={ item }
/>
)) }
</Box>
);
};
export default TokenHoldersList;
import { Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import ListItemMobile from 'ui/shared/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization';
interface Props {
holder: TokenHolder;
token: TokenInfo;
}
const TokenHoldersListItem = ({ holder, token }: Props) => {
const quantity = BigNumber(holder.value).div(BigNumber(10 ** Number(token.decimals))).dp(6).toFormat();
return (
<ListItemMobile rowGap={ 3 }>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ holder.address }/>
<AddressLink ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/>
</Address>
<Flex justifyContent="space-between" alignItems="center" width="100%">
{ quantity }
{ token.total_supply && (
<Utilization
value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() }
colorScheme="green"
ml={ 6 }
/>
) }
</Flex>
</ListItemMobile>
);
};
export default TokenHoldersListItem;
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { tokenHolders } from 'mocks/tokens/tokenHolders';
import { tokenInfo } from 'mocks/tokens/tokenInfo';
import TestApp from 'playwright/TestApp';
import TokenHoldersTable from './TokenHoldersTable';
test('base view', async({ mount }) => {
const component = await mount(
<TestApp>
<Box h="128px"/>
<TokenHoldersTable data={ tokenHolders.items } token={ tokenInfo }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo';
import { default as Thead } from 'ui/shared/TheadSticky';
import TokenHoldersTableItem from 'ui/token/TokenHolders/TokenHoldersTableItem';
interface Props {
data: Array<TokenHolder>;
token: TokenInfo;
}
const TokenHoldersTable = ({ data, token }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ 80 }>
<Tr>
<Th>Holder</Th>
<Th isNumeric width="300px">Quantity</Th>
{ token.total_supply && <Th isNumeric width="175px">Percentage</Th> }
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<TokenHoldersTableItem key={ item.address.hash } holder={ item } token={ token }/>
)) }
</Tbody>
</Table>
);
};
export default React.memo(TokenHoldersTable);
import { Tr, Td } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import Utilization from 'ui/shared/Utilization/Utilization';
type Props = {
holder: TokenHolder;
token: TokenInfo;
}
const TokenTransferTableItem = ({ holder, token }: Props) => {
const quantity = BigNumber(holder.value).div(BigNumber(10 ** Number(token.decimals))).toFormat();
return (
<Tr>
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ holder.address }/>
<AddressLink ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/>
</Address>
</Td>
<Td isNumeric>
{ quantity }
</Td>
{ token.total_supply && (
<Td isNumeric>
<Utilization
value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() }
colorScheme="green"
display="inline-flex"
/>
</Td>
) }
</Tr>
);
};
export default React.memo(TokenTransferTableItem);
import { Flex, Textarea, Skeleton } from '@chakra-ui/react';
import { Flex, Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { SECOND } from 'lib/consts';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
......@@ -41,24 +41,12 @@ const TxRawTrace = () => {
}
if (data.length === 0) {
return <span>There is no raw trace for this transaction.</span>;
return <span>No trace entries found.</span>;
}
const text = JSON.stringify(data, undefined, 4);
return (
<>
<Flex justifyContent="end" mb={ 2 }>
<CopyToClipboard text={ text }/>
</Flex>
<Textarea
variant="filledInactive"
minHeight="500px"
p={ 4 }
value={ text }
/>
</>
);
return <RawDataSnippet data={ text }/>;
};
export default TxRawTrace;
......@@ -3864,6 +3864,11 @@ abort-controller@^3.0.0:
dependencies:
event-target-shim "^5.0.0"
ace-builds@^1.14.0, ace-builds@^1.4.14:
version "1.14.0"
resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.14.0.tgz#85a6733b4fa17b0abc3dbfe38cd8d823cad79716"
integrity sha512-3q8LvawomApRCt4cC0OzxVjDsZ609lDbm8l0Xl9uqG06dKEq4RT0YXLUyk7J2SxmqIp5YXzZNw767Dr8GKUruw==
acorn-globals@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-7.0.1.tgz#0dbf05c44fa7c94332914c02066d5beff62c40c3"
......@@ -5012,6 +5017,11 @@ detect-node-es@^1.1.0:
resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==
diff-match-patch@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37"
integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==
diff-sequences@^29.3.1:
version "29.3.1"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.3.1.tgz#104b5b95fe725932421a9c6e5b4bef84c3f2249e"
......@@ -7244,6 +7254,16 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
lodash.memoize@4.x:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
......@@ -8035,6 +8055,17 @@ quick-format-unescaped@^4.0.3:
resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
react-ace@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-10.1.0.tgz#d348eac2b16475231779070b6cd16768deed565f"
integrity sha512-VkvUjZNhdYTuKOKQpMIZi7uzZZVgzCjM7cLYu6F64V0mejY8a2XTyPUIMszC6A4trbeMIHbK5fYFcT/wkP/8VA==
dependencies:
ace-builds "^1.4.14"
diff-match-patch "^1.0.5"
lodash.get "^4.4.2"
lodash.isequal "^4.5.0"
prop-types "^15.7.2"
react-clientside-effect@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a"
......
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