feat(explorer): asset details
This commit is contained in:
parent
1952cb0e78
commit
02773bdae3
@ -9,6 +9,23 @@ import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-web
|
||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||
import { Footer } from './components/footer/footer';
|
||||
import { AnnouncementBanner, ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
AssetDetailsDialog,
|
||||
useAssetDetailsDialogStore,
|
||||
} from '@vegaprotocol/assets';
|
||||
|
||||
const DialogsContainer = () => {
|
||||
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
|
||||
return (
|
||||
<AssetDetailsDialog
|
||||
assetId={id}
|
||||
trigger={trigger || null}
|
||||
asJson={asJson}
|
||||
open={isOpen}
|
||||
onChange={setOpen}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function App() {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
@ -56,6 +73,8 @@ function App() {
|
||||
<Main />
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
<DialogsContainer />
|
||||
</NetworkLoader>
|
||||
</TendermintWebsocketProvider>
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useAssetDataProvider } from '@vegaprotocol/assets';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
|
||||
import { AssetLink } from '../links';
|
||||
import { useExplorerAssetQuery } from '../links/asset-link/__generated__/Asset';
|
||||
|
||||
export type AssetBalanceProps = {
|
||||
assetId: string;
|
||||
@ -17,21 +17,17 @@ const AssetBalance = ({
|
||||
price,
|
||||
showAssetLink = true,
|
||||
}: AssetBalanceProps) => {
|
||||
const { data } = useExplorerAssetQuery({
|
||||
variables: { id: assetId },
|
||||
});
|
||||
const { data: asset } = useAssetDataProvider(assetId);
|
||||
|
||||
const label =
|
||||
data && data.asset?.decimals
|
||||
? addDecimalsFormatNumber(price, data.asset.decimals)
|
||||
asset && asset.decimals
|
||||
? addDecimalsFormatNumber(price, asset.decimals)
|
||||
: price;
|
||||
|
||||
return (
|
||||
<div className="inline-block">
|
||||
<span>{label}</span>{' '}
|
||||
{showAssetLink && data?.asset?.id ? (
|
||||
<AssetLink id={data.asset.id} />
|
||||
) : null}
|
||||
{showAssetLink && asset?.id ? <AssetLink assetId={assetId} /> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,8 +0,0 @@
|
||||
query ExplorerAsset($id: ID!) {
|
||||
asset(id: $id) {
|
||||
id
|
||||
name
|
||||
status
|
||||
decimals
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ExplorerAssetQueryVariables = Types.Exact<{
|
||||
id: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerAssetQuery = { __typename?: 'Query', asset?: { __typename?: 'Asset', id: string, name: string, status: Types.AssetStatus, decimals: number } | null };
|
||||
|
||||
|
||||
export const ExplorerAssetDocument = gql`
|
||||
query ExplorerAsset($id: ID!) {
|
||||
asset(id: $id) {
|
||||
id
|
||||
name
|
||||
status
|
||||
decimals
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useExplorerAssetQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useExplorerAssetQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useExplorerAssetQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useExplorerAssetQuery({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useExplorerAssetQuery(baseOptions: Apollo.QueryHookOptions<ExplorerAssetQuery, ExplorerAssetQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ExplorerAssetQuery, ExplorerAssetQueryVariables>(ExplorerAssetDocument, options);
|
||||
}
|
||||
export function useExplorerAssetLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerAssetQuery, ExplorerAssetQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ExplorerAssetQuery, ExplorerAssetQueryVariables>(ExplorerAssetDocument, options);
|
||||
}
|
||||
export type ExplorerAssetQueryHookResult = ReturnType<typeof useExplorerAssetQuery>;
|
||||
export type ExplorerAssetLazyQueryHookResult = ReturnType<typeof useExplorerAssetLazyQuery>;
|
||||
export type ExplorerAssetQueryResult = Apollo.QueryResult<ExplorerAssetQuery, ExplorerAssetQueryVariables>;
|
@ -1,63 +1,99 @@
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { render } from '@testing-library/react';
|
||||
import AssetLink from './asset-link';
|
||||
import { ExplorerAssetDocument } from './__generated__/Asset';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { AssetLink } from './asset-link';
|
||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import { AssetDocument } from '@vegaprotocol/assets';
|
||||
import { AssetStatus } from '@vegaprotocol/types';
|
||||
|
||||
function renderComponent(id: string, mock: MockedResponse[]) {
|
||||
return (
|
||||
<MockedProvider mocks={mock}>
|
||||
<MockedProvider mocks={mock} addTypename={false}>
|
||||
<MemoryRouter>
|
||||
<AssetLink id={id} />
|
||||
<AssetLink assetId={id} />
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('Asset link component', () => {
|
||||
it('Renders the ID at first', () => {
|
||||
const A1: AssetFieldsFragment = {
|
||||
__typename: 'Asset',
|
||||
id: '123',
|
||||
name: 'A ONE',
|
||||
symbol: 'A1',
|
||||
decimals: 0,
|
||||
quantum: '',
|
||||
status: AssetStatus.STATUS_ENABLED,
|
||||
source: {
|
||||
__typename: 'BuiltinAsset',
|
||||
maxFaucetAmountMint: '',
|
||||
},
|
||||
infrastructureFeeAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
globalRewardPoolAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
lpFeeRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
makerFeeRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
marketProposerRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
takerFeeRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
};
|
||||
|
||||
const mock = {
|
||||
request: {
|
||||
query: AssetDocument,
|
||||
variables: {
|
||||
assetId: '123',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
assetsConnection: {
|
||||
__typename: 'AssetsConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'AssetEdge',
|
||||
node: A1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('AssetLink', () => {
|
||||
it('renders the asset id when not found and makes the button disabled', async () => {
|
||||
const res = render(renderComponent('123', []));
|
||||
expect(res.getByText('123')).toBeInTheDocument();
|
||||
expect(await res.findByTestId('asset-link')).toBeDisabled();
|
||||
await waitFor(async () => {
|
||||
expect(await res.queryByText('A ONE')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('Renders the asset name when the query returns a result', async () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerAssetDocument,
|
||||
variables: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
asset: {
|
||||
id: '123',
|
||||
name: 'test-label',
|
||||
status: 'irrelevant-test-data',
|
||||
decimals: 18,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('renders the asset name when found and make the button enabled', async () => {
|
||||
const res = render(renderComponent('123', [mock]));
|
||||
expect(res.getByText('123')).toBeInTheDocument();
|
||||
expect(await res.findByText('test-label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Leaves the asset id when the asset is not found', async () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerAssetDocument,
|
||||
variables: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
error: new Error('No such asset'),
|
||||
};
|
||||
|
||||
const res = render(renderComponent('123', [mock]));
|
||||
expect(await res.findByText('123')).toBeInTheDocument();
|
||||
await waitFor(async () => {
|
||||
expect(await res.findByText('A ONE')).toBeInTheDocument();
|
||||
expect(await res.findByTestId('asset-link')).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Routes } from '../../../routes/route-names';
|
||||
import { useExplorerAssetQuery } from './__generated__/Asset';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import Hash from '../hash';
|
||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
useAssetDataProvider,
|
||||
useAssetDetailsDialogStore,
|
||||
} from '@vegaprotocol/assets';
|
||||
|
||||
export type AssetLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||
id: string;
|
||||
export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & {
|
||||
assetId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -15,22 +15,21 @@ export type AssetLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||
* with a link to the assets list. If the name does not come back
|
||||
* it will use the ID instead.
|
||||
*/
|
||||
const AssetLink = ({ id, ...props }: AssetLinkProps) => {
|
||||
const { data } = useExplorerAssetQuery({
|
||||
variables: { id },
|
||||
});
|
||||
|
||||
let label: string = id;
|
||||
|
||||
if (data?.asset?.name) {
|
||||
label = data.asset.name;
|
||||
}
|
||||
export const AssetLink = ({ assetId, ...props }: AssetLinkProps) => {
|
||||
const { data: asset } = useAssetDataProvider(assetId);
|
||||
|
||||
const open = useAssetDetailsDialogStore((state) => state.open);
|
||||
const label = asset?.name ? asset.name : assetId;
|
||||
return (
|
||||
<Link className="underline" {...props} to={`/${Routes.ASSETS}#${id}`}>
|
||||
<ButtonLink
|
||||
data-testid="asset-link"
|
||||
disabled={!asset}
|
||||
onClick={(e) => {
|
||||
open(assetId, e.target as HTMLElement);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Hash text={label} />
|
||||
</Link>
|
||||
</ButtonLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssetLink;
|
||||
|
@ -2,4 +2,4 @@ export { default as BlockLink } from './block-link/block-link';
|
||||
export { default as PartyLink } from './party-link/party-link';
|
||||
export { default as NodeLink } from './node-link/node-link';
|
||||
export { default as MarketLink } from './market-link/market-link';
|
||||
export { default as AssetLink } from './asset-link/asset-link';
|
||||
export * from './asset-link/asset-link';
|
||||
|
@ -76,9 +76,7 @@ describe('Chain Event: Builtin asset deposit', () => {
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
|
||||
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
|
||||
});
|
||||
});
|
||||
|
@ -34,7 +34,7 @@ export const TxDetailsChainEventBuiltinDeposit = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Asset')}</TableCell>
|
||||
<TableCell>
|
||||
<AssetLink id={deposit.vegaAssetId} /> ({t('built in asset')})
|
||||
<AssetLink assetId={deposit.vegaAssetId} /> ({t('built in asset')})
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
|
@ -82,9 +82,7 @@ describe('Chain Event: Builtin asset withdrawal', () => {
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
|
||||
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
|
||||
});
|
||||
});
|
||||
|
@ -39,8 +39,8 @@ export const TxDetailsChainEventBuiltinWithdrawal = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Asset')}</TableCell>
|
||||
<TableCell>
|
||||
<AssetLink id={withdrawal.vegaAssetId || ''} /> ({t('built in asset')}
|
||||
)
|
||||
<AssetLink assetId={withdrawal.vegaAssetId || ''} /> (
|
||||
{t('built in asset')})
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
|
@ -63,9 +63,7 @@ describe('Chain Event: ERC20 Asset Delist', () => {
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
|
||||
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
|
||||
});
|
||||
});
|
||||
|
@ -29,7 +29,7 @@ export const TxDetailsChainEventErc20AssetDelist = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Removed Vega asset')}</TableCell>
|
||||
<TableCell>
|
||||
<AssetLink id={assetDelist.vegaAssetId || ''} />
|
||||
<AssetLink assetId={assetDelist.vegaAssetId || ''} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
|
@ -79,10 +79,8 @@ describe('Chain Event: ERC20 Asset limits updated', () => {
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
|
||||
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
|
||||
|
||||
expect(screen.getByText(t('ERC20 asset'))).toBeInTheDocument();
|
||||
const ethLink = screen.getByText(`${fullMock.sourceEthereumAddress}`);
|
||||
|
@ -51,7 +51,7 @@ export const TxDetailsChainEventErc20AssetLimitsUpdated = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Vega asset')}</TableCell>
|
||||
<TableCell>
|
||||
<AssetLink id={assetLimitsUpdated.vegaAssetId} />
|
||||
<AssetLink assetId={assetLimitsUpdated.vegaAssetId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
|
@ -65,10 +65,8 @@ describe('Chain Event: ERC20 Asset List', () => {
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
|
||||
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
|
||||
|
||||
expect(screen.getByText(t('Source'))).toBeInTheDocument();
|
||||
const ethLink = screen.getByText(`${fullMock.assetSource}`);
|
||||
|
@ -41,7 +41,7 @@ export const TxDetailsChainEventErc20AssetList = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Added Vega asset')}</TableCell>
|
||||
<TableCell>
|
||||
<AssetLink id={assetList.vegaAssetId} />
|
||||
<AssetLink assetId={assetList.vegaAssetId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
|
@ -75,10 +75,8 @@ describe('Chain Event: ERC20 asset deposit', () => {
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
|
||||
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
|
||||
|
||||
expect(screen.getByText(t('Source'))).toBeInTheDocument();
|
||||
const ethLink = screen.getByText(`${fullMock.sourceEthereumAddress}`);
|
||||
|
@ -51,7 +51,7 @@ export const TxDetailsChainEventDeposit = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Asset')}</TableCell>
|
||||
<TableCell>
|
||||
<AssetLink id={deposit.vegaAssetId} />
|
||||
<AssetLink assetId={deposit.vegaAssetId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
|
@ -60,10 +60,8 @@ describe('Chain Event: ERC20 asset deposit', () => {
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
|
||||
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
|
||||
|
||||
expect(screen.getByText(t('Recipient'))).toBeInTheDocument();
|
||||
const ethLink = screen.getByText(`${fullMock.targetEthereumAddress}`);
|
||||
|
@ -45,7 +45,7 @@ export const TxDetailsChainEventWithdrawal = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Asset')}</TableCell>
|
||||
<TableCell>
|
||||
<AssetLink id={withdrawal.vegaAssetId} />
|
||||
<AssetLink assetId={withdrawal.vegaAssetId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
|
@ -1,32 +0,0 @@
|
||||
fragment AssetsFields on Asset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
source {
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
}
|
||||
... on BuiltinAsset {
|
||||
maxFaucetAmountMint
|
||||
}
|
||||
}
|
||||
|
||||
infrastructureFeeAccount {
|
||||
type
|
||||
balance
|
||||
market {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query ExplorerAssets {
|
||||
assetsConnection {
|
||||
edges {
|
||||
node {
|
||||
...AssetsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type AssetsFieldsFragment = { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string }, infrastructureFeeAccount?: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, market?: { __typename?: 'Market', id: string } | null } | null };
|
||||
|
||||
export type ExplorerAssetsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ExplorerAssetsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string }, infrastructureFeeAccount?: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, market?: { __typename?: 'Market', id: string } | null } | null } } | null> | null } | null };
|
||||
|
||||
export const AssetsFieldsFragmentDoc = gql`
|
||||
fragment AssetsFields on Asset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
source {
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
}
|
||||
... on BuiltinAsset {
|
||||
maxFaucetAmountMint
|
||||
}
|
||||
}
|
||||
infrastructureFeeAccount {
|
||||
type
|
||||
balance
|
||||
market {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const ExplorerAssetsDocument = gql`
|
||||
query ExplorerAssets {
|
||||
assetsConnection {
|
||||
edges {
|
||||
node {
|
||||
...AssetsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${AssetsFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useExplorerAssetsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useExplorerAssetsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useExplorerAssetsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useExplorerAssetsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useExplorerAssetsQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerAssetsQuery, ExplorerAssetsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ExplorerAssetsQuery, ExplorerAssetsQueryVariables>(ExplorerAssetsDocument, options);
|
||||
}
|
||||
export function useExplorerAssetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerAssetsQuery, ExplorerAssetsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ExplorerAssetsQuery, ExplorerAssetsQueryVariables>(ExplorerAssetsDocument, options);
|
||||
}
|
||||
export type ExplorerAssetsQueryHookResult = ReturnType<typeof useExplorerAssetsQuery>;
|
||||
export type ExplorerAssetsLazyQueryHookResult = ReturnType<typeof useExplorerAssetsLazyQuery>;
|
||||
export type ExplorerAssetsQueryResult = Apollo.QueryResult<ExplorerAssetsQuery, ExplorerAssetsQueryVariables>;
|
@ -1,44 +1,149 @@
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { render } from '@testing-library/react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import { AssetsDocument } from '@vegaprotocol/assets';
|
||||
import { AssetStatus } from '@vegaprotocol/types';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import Assets from './index';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { ExplorerAssetDocument } from '../../components/links/asset-link/__generated__/Asset';
|
||||
|
||||
function renderComponent(mock: MockedResponse[]) {
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<MockedProvider mocks={mock}>
|
||||
<Assets />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
const component = (mocks: MockedResponse[]) => (
|
||||
<MemoryRouter>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<Assets />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
describe('Assets index', () => {
|
||||
it('Renders loader when loading', async () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerAssetDocument,
|
||||
const A1: AssetFieldsFragment = {
|
||||
__typename: 'Asset',
|
||||
id: '123',
|
||||
name: 'A ONE',
|
||||
symbol: 'A1',
|
||||
decimals: 0,
|
||||
quantum: '',
|
||||
status: AssetStatus.STATUS_ENABLED,
|
||||
source: {
|
||||
__typename: 'BuiltinAsset',
|
||||
maxFaucetAmountMint: '',
|
||||
},
|
||||
infrastructureFeeAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
globalRewardPoolAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
lpFeeRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
makerFeeRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
marketProposerRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
takerFeeRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
};
|
||||
|
||||
const A2: AssetFieldsFragment = {
|
||||
__typename: 'Asset',
|
||||
id: '456',
|
||||
name: 'A TWO',
|
||||
symbol: 'A2',
|
||||
decimals: 0,
|
||||
quantum: '',
|
||||
status: AssetStatus.STATUS_ENABLED,
|
||||
source: {
|
||||
__typename: 'BuiltinAsset',
|
||||
maxFaucetAmountMint: '',
|
||||
},
|
||||
infrastructureFeeAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
globalRewardPoolAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
lpFeeRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
makerFeeRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
marketProposerRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
takerFeeRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
};
|
||||
|
||||
const mock = {
|
||||
request: {
|
||||
query: AssetsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
assetsConnection: {
|
||||
__typename: 'AssetsConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'AssetEdge',
|
||||
node: A1,
|
||||
},
|
||||
{
|
||||
__typename: 'AssetEdge',
|
||||
node: A2,
|
||||
},
|
||||
],
|
||||
},
|
||||
result: {
|
||||
data: {},
|
||||
},
|
||||
};
|
||||
const res = render(renderComponent([mock]));
|
||||
expect(await res.findByTestId('loader')).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('Assets', () => {
|
||||
it('shows loading message on first render', async () => {
|
||||
const res = render(component([mock]));
|
||||
expect(await res.findByText('Loading...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders EmptyList when loading completes and there are no results', async () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerAssetDocument,
|
||||
},
|
||||
result: {
|
||||
data: {},
|
||||
},
|
||||
};
|
||||
const res = render(renderComponent([mock]));
|
||||
expect(await res.findByTestId('emptylist')).toBeInTheDocument();
|
||||
it('shows no data message if no assets found', async () => {
|
||||
const res = render(
|
||||
component([
|
||||
{
|
||||
request: {
|
||||
query: AssetsDocument,
|
||||
},
|
||||
result: { data: null },
|
||||
},
|
||||
])
|
||||
);
|
||||
expect(
|
||||
await res.findByText('This chain has no assets')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows a table/list with all the assets', async () => {
|
||||
const res = render(component([mock]));
|
||||
await waitFor(() => {
|
||||
const rowA1 = res.container.querySelector('[row-id="123"]');
|
||||
expect(rowA1).toBeInTheDocument();
|
||||
|
||||
const rowA2 = res.container.querySelector('[row-id="456"]');
|
||||
expect(rowA2).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,51 +1,108 @@
|
||||
import { getNodes, t } from '@vegaprotocol/react-helpers';
|
||||
import React from 'react';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { SubHeading } from '../../components/sub-heading';
|
||||
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { useExplorerAssetsQuery } from './__generated__/Assets';
|
||||
import type { AssetsFieldsFragment } from './__generated__/Assets';
|
||||
import type { VegaICellRendererParams } from '@vegaprotocol/ui-toolkit';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import EmptyList from '../../components/empty-list/empty-list';
|
||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import {
|
||||
AssetStatusMapping,
|
||||
AssetTypeMapping,
|
||||
useAssetDetailsDialogStore,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
||||
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import { useRef } from 'react';
|
||||
|
||||
const Assets = () => {
|
||||
const { data, loading } = useExplorerAssetsQuery();
|
||||
useDocumentTitle(['Assets']);
|
||||
|
||||
useScrollToLocation();
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
|
||||
const assets = getNodes<AssetsFieldsFragment>(data?.assetsConnection);
|
||||
|
||||
if (!assets || assets.length === 0) {
|
||||
if (!loading) {
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="assets-header">{t('Assets')}</RouteTitle>
|
||||
<EmptyList
|
||||
heading={t('This chain has no assets')}
|
||||
label={t('0 assets')}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
} else {
|
||||
return <Loader />;
|
||||
}
|
||||
}
|
||||
const { data, loading, error } = useAssetsDataProvider();
|
||||
const ref = useRef<AgGridReact>(null);
|
||||
const table = (
|
||||
<AgGrid
|
||||
ref={ref}
|
||||
rowData={data}
|
||||
getRowId={({ data }: { data: AssetFieldsFragment }) => data.id}
|
||||
overlayNoRowsTemplate={t('This chain has no assets')}
|
||||
domLayout="autoHeight"
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
filterParams: { buttons: ['reset'] },
|
||||
autoHeight: true,
|
||||
}}
|
||||
suppressCellFocus={true}
|
||||
>
|
||||
<AgGridColumn headerName={t('Symbol')} field="symbol" />
|
||||
<AgGridColumn headerName={t('Name')} field="name" />
|
||||
<AgGridColumn headerName={t('ID')} field="id" />
|
||||
<AgGridColumn
|
||||
headerName={t('Type')}
|
||||
field="source.__typename"
|
||||
valueFormatter={({ value }: { value?: string }) =>
|
||||
value && AssetTypeMapping[value].value
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Status')}
|
||||
field="status"
|
||||
valueFormatter={({ value }: { value?: string }) =>
|
||||
value && AssetStatusMapping[value].value
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName=""
|
||||
sortable={false}
|
||||
filter={false}
|
||||
field="id"
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaICellRendererParams<AssetFieldsFragment, 'id'>) =>
|
||||
value ? (
|
||||
<>
|
||||
<ButtonLink
|
||||
onClick={(e) => {
|
||||
openAssetDetailsDialog(value, e.target as HTMLElement);
|
||||
}}
|
||||
>
|
||||
{t('View details')}
|
||||
</ButtonLink>{' '}
|
||||
<ButtonLink
|
||||
onClick={(e) => {
|
||||
openAssetDetailsDialog(value, e.target as HTMLElement, true);
|
||||
}}
|
||||
>
|
||||
{t('View JSON')}
|
||||
</ButtonLink>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
/>
|
||||
</AgGrid>
|
||||
);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="assets-header">{t('Assets')}</RouteTitle>
|
||||
{assets.map((a) => {
|
||||
return (
|
||||
<React.Fragment key={a.id}>
|
||||
<SubHeading data-testid="asset-header" id={a.id}>
|
||||
{a.name} ({a.symbol})
|
||||
</SubHeading>
|
||||
<SyntaxHighlighter data={a} />
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
<AsyncRenderer
|
||||
noDataMessage={t('This chain has no assets')}
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
>
|
||||
<div className="h-full relative">{table}</div>
|
||||
</AsyncRenderer>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
@ -71,7 +71,7 @@ export const PartyAccounts = ({ accounts }: PartyAccountsProps) => {
|
||||
/>
|
||||
</td>
|
||||
<td className="text-md">
|
||||
<AssetLink id={account.asset.id} />
|
||||
<AssetLink assetId={account.asset.id} />
|
||||
</td>
|
||||
</TableRow>
|
||||
);
|
||||
|
@ -1,9 +1,6 @@
|
||||
const { join } = require('path');
|
||||
const { createGlobPatternsForDependencies } = require('@nrwl/next/tailwind');
|
||||
const theme = require('../../libs/tailwindcss-config/src/theme');
|
||||
const {
|
||||
VegaColours,
|
||||
} = require('../../libs/tailwindcss-config/src/vega-colours');
|
||||
const vegaCustomClasses = require('../../libs/tailwindcss-config/src/vega-custom-classes');
|
||||
|
||||
module.exports = {
|
||||
@ -14,12 +11,7 @@ module.exports = {
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
...theme,
|
||||
colors: {
|
||||
vega: VegaColours,
|
||||
},
|
||||
},
|
||||
extend: theme,
|
||||
},
|
||||
plugins: [vegaCustomClasses],
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ import { AssetDocument } from './__generated__/Asset';
|
||||
|
||||
export type Asset = AssetFieldsFragment;
|
||||
|
||||
const getData = (responseData: AssetQuery | null) => {
|
||||
export const getData = (responseData: AssetQuery | null | undefined) => {
|
||||
const foundAssets = responseData?.assetsConnection?.edges
|
||||
?.filter((e) => Boolean(e?.node))
|
||||
.map((e) => e?.node as Asset);
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { useAssetsDataProvider } from './assets-data-provider';
|
||||
import { Button, Dialog, Icon, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
Icon,
|
||||
Splash,
|
||||
SyntaxHighlighter,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { create } from 'zustand';
|
||||
import { AssetDetailsTable } from './asset-details-table';
|
||||
import { AssetProposalNotification } from '@vegaprotocol/governance';
|
||||
@ -9,8 +15,9 @@ export type AssetDetailsDialogStore = {
|
||||
isOpen: boolean;
|
||||
id: string;
|
||||
trigger: HTMLElement | null | undefined;
|
||||
asJson: boolean;
|
||||
setOpen: (isOpen: boolean) => void;
|
||||
open: (id: string, trigger?: HTMLElement | null) => void;
|
||||
open: (id: string, trigger?: HTMLElement | null, asJson?: boolean) => void;
|
||||
};
|
||||
|
||||
export const useAssetDetailsDialogStore = create<AssetDetailsDialogStore>(
|
||||
@ -18,14 +25,16 @@ export const useAssetDetailsDialogStore = create<AssetDetailsDialogStore>(
|
||||
isOpen: false,
|
||||
id: '',
|
||||
trigger: null,
|
||||
asJson: false,
|
||||
setOpen: (isOpen) => {
|
||||
set({ isOpen: isOpen });
|
||||
},
|
||||
open: (id, trigger?) => {
|
||||
open: (id, trigger?, asJson = false) => {
|
||||
set({
|
||||
isOpen: true,
|
||||
id,
|
||||
trigger,
|
||||
asJson,
|
||||
});
|
||||
},
|
||||
})
|
||||
@ -36,6 +45,7 @@ export interface AssetDetailsDialogProps {
|
||||
trigger?: HTMLElement | null;
|
||||
open: boolean;
|
||||
onChange: (open: boolean) => void;
|
||||
asJson?: boolean;
|
||||
}
|
||||
|
||||
export const AssetDetailsDialog = ({
|
||||
@ -43,6 +53,7 @@ export const AssetDetailsDialog = ({
|
||||
trigger,
|
||||
open,
|
||||
onChange,
|
||||
asJson = false,
|
||||
}: AssetDetailsDialogProps) => {
|
||||
const { data } = useAssetsDataProvider();
|
||||
|
||||
@ -51,7 +62,13 @@ export const AssetDetailsDialog = ({
|
||||
const content = asset ? (
|
||||
<div className="my-2">
|
||||
<AssetProposalNotification assetId={asset.id} />
|
||||
<AssetDetailsTable asset={asset} />
|
||||
{asJson ? (
|
||||
<div className="pr-8">
|
||||
<SyntaxHighlighter size="smaller" data={asset} />
|
||||
</div>
|
||||
) : (
|
||||
<AssetDetailsTable asset={asset} />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-12" data-testid="splash">
|
||||
|
@ -2,6 +2,7 @@ import { useEtherscanLink } from '@vegaprotocol/environment';
|
||||
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import type { KeyValueTableRowProps } from '@vegaprotocol/ui-toolkit';
|
||||
import { CopyWithTooltip, Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
KeyValueTable,
|
||||
@ -105,7 +106,16 @@ export const rows: Rows = [
|
||||
return;
|
||||
}
|
||||
|
||||
return <ContractAddressLink address={asset.source.contractAddress} />;
|
||||
return (
|
||||
<>
|
||||
<ContractAddressLink address={asset.source.contractAddress} />{' '}
|
||||
<CopyWithTooltip text={asset.source.contractAddress}>
|
||||
<button title={t('Copy address to clipboard')}>
|
||||
<Icon size={3} name="duplicate" />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -180,7 +190,7 @@ export const rows: Rows = [
|
||||
},
|
||||
];
|
||||
|
||||
const AssetStatusMapping: Mapping = {
|
||||
export const AssetStatusMapping: Mapping = {
|
||||
STATUS_ENABLED: {
|
||||
value: t('Enabled'),
|
||||
tooltip: t('Asset can be used on the Vega network'),
|
||||
@ -199,7 +209,7 @@ const AssetStatusMapping: Mapping = {
|
||||
},
|
||||
};
|
||||
|
||||
const AssetTypeMapping: Mapping = {
|
||||
export const AssetTypeMapping: Mapping = {
|
||||
BuiltinAsset: {
|
||||
value: 'Builtin asset',
|
||||
tooltip: t('A Vega builtin asset'),
|
||||
@ -274,7 +284,7 @@ const ContractAddressLink = ({ address }: { address: string }) => {
|
||||
const etherscanLink = useEtherscanLink();
|
||||
const href = etherscanLink(`/address/${address}`);
|
||||
return (
|
||||
<Link href={href} target="_blank">
|
||||
<Link href={href} target="_blank" title={t('View on etherscan')}>
|
||||
{address}
|
||||
</Link>
|
||||
);
|
||||
|
@ -15,7 +15,7 @@ const vegaCustomClasses = plugin(function ({ addUtilities }) {
|
||||
},
|
||||
'.syntax-highlighter-wrapper .hljs': {
|
||||
fontSize: '1rem',
|
||||
fontFamily: "Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace",
|
||||
fontFamily: "'Roboto Mono', monospace",
|
||||
display: 'block',
|
||||
overflowX: 'auto',
|
||||
padding: '1em',
|
||||
@ -23,6 +23,10 @@ const vegaCustomClasses = plugin(function ({ addUtilities }) {
|
||||
color: colors.neutral[700],
|
||||
border: `1px solid ${colors.neutral[300]}`,
|
||||
},
|
||||
'.syntax-highlighter-wrapper-sm .hljs': {
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: '1.25rem',
|
||||
},
|
||||
'.dark .syntax-highlighter-wrapper .hljs': {
|
||||
background: colors.neutral[800],
|
||||
color: theme.colors.vega.green.DEFAULT,
|
||||
|
@ -1,8 +1,19 @@
|
||||
import classNames from 'classnames';
|
||||
import Highlighter from 'react-syntax-highlighter';
|
||||
|
||||
export const SyntaxHighlighter = ({ data }: { data: unknown }) => {
|
||||
export const SyntaxHighlighter = ({
|
||||
data,
|
||||
size = 'default',
|
||||
}: {
|
||||
data: unknown;
|
||||
size?: 'smaller' | 'default';
|
||||
}) => {
|
||||
return (
|
||||
<div className="syntax-highlighter-wrapper">
|
||||
<div
|
||||
className={classNames('syntax-highlighter-wrapper', {
|
||||
'syntax-highlighter-wrapper-sm': size === 'smaller',
|
||||
})}
|
||||
>
|
||||
<Highlighter language="json" useInlineStyles={false}>
|
||||
{JSON.stringify(data, null, ' ')}
|
||||
</Highlighter>
|
||||
|
Loading…
Reference in New Issue
Block a user