Commit 02556490 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #2129 from blockscout/release/v1_33_0-fixes

fixes for Release v1.33.0
parents c4ae053b b53a425d
import type { SmartContractVerificationMethodExtra } from 'types/client/contract';
import { SMART_CONTRACT_EXTRA_VERIFICATION_METHODS } from 'types/client/contract';
import type { AddressViewId, IdenticonType } from 'types/views/address';
import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from 'types/views/address';
......@@ -24,10 +26,26 @@ const hiddenViews = (() => {
return result;
})();
const extraVerificationMethods: Array<SmartContractVerificationMethodExtra> = (() => {
const envValue = getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS');
if (envValue === 'none') {
return [];
}
if (!envValue) {
return SMART_CONTRACT_EXTRA_VERIFICATION_METHODS;
}
const parsedMethods = parseEnvJson<Array<SmartContractVerificationMethodExtra>>(getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS')) || [];
return SMART_CONTRACT_EXTRA_VERIFICATION_METHODS.filter((method) => parsedMethods.includes(method));
})();
const config = Object.freeze({
identiconType,
hiddenViews,
solidityscanEnabled: getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED') === 'true',
extraVerificationMethods,
});
export default config;
/* eslint-disable max-len */
declare module 'yup' {
interface StringSchema {
// Yup's URL validator is not perfect so we made our own
......@@ -11,7 +12,7 @@ import * as yup from 'yup';
import type { AdButlerConfig } from '../../../types/client/adButlerConfig';
import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS, SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS } from '../../../types/client/adProviders';
import type { AdTextProviders, AdBannerProviders, AdBannerAdditionalProviders } from '../../../types/client/adProviders';
import type { ContractCodeIde } from '../../../types/client/contract';
import { SMART_CONTRACT_EXTRA_VERIFICATION_METHODS, type ContractCodeIde, type SmartContractVerificationMethodExtra } from '../../../types/client/contract';
import type { DeFiDropdownItem } from '../../../types/client/deFiDropdown';
import type { GasRefuelProviderConfig } from '../../../types/client/gasRefuelProviderConfig';
import { GAS_UNITS } from '../../../types/client/gasTracker';
......@@ -576,6 +577,21 @@ const schema = yup
.json()
.of(yup.string<AddressViewId>().oneOf(ADDRESS_VIEWS_IDS)),
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED: yup.boolean(),
NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS: yup
.mixed()
.test(
'shape',
'Invalid schema were provided for NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS, it should be either array of method ids or "none" string literal',
(data) => {
const isNoneSchema = yup.string().oneOf([ 'none' ]);
const isArrayOfMethodsSchema = yup
.array()
.transform(replaceQuotes)
.json()
.of(yup.string<SmartContractVerificationMethodExtra>().oneOf(SMART_CONTRACT_EXTRA_VERIFICATION_METHODS));
return isNoneSchema.isValidSync(data) || isArrayOfMethodsSchema.isValidSync(data);
}),
NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS: yup
.array()
.transform(replaceQuotes)
......
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=none
NEXT_PUBLIC_API_SPEC_URL=none
\ No newline at end of file
NEXT_PUBLIC_API_SPEC_URL=none
NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none
\ No newline at end of file
......@@ -68,6 +68,7 @@ NEXT_PUBLIC_STATS_API_BASE_PATH=/
NEXT_PUBLIC_USE_NEXT_JS_PROXY=false
NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=gradient_avatar
NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS=['top_accounts']
NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=['solidity-hardhat','solidity-foundry']
NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward']
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'NFT Marketplace','collection_url':'https://example.com/{hash}','instance_url':'https://example.com/{hash}/{id}','logo_url':'https://example.com/logo.png'}]
NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS=['fee_per_gas']
......
......@@ -221,6 +221,7 @@ Settings for meta tags, OG tags and SEO
| NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE | `"github" \| "jazzicon" \| "gradient_avatar" \| "blockie"` | Default style of address identicon appearance. Choose between [GitHub](https://github.blog/2013-08-14-identicons/), [Metamask Jazzicon](https://metamask.github.io/jazzicon/), [Gradient Avatar](https://github.com/varld/gradient-avatar) and [Ethereum Blocky](https://mycryptohq.github.io/ethereum-blockies-base64/) | - | `jazzicon` | `gradient_avatar` | v1.12.0+ |
| NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS | `Array<AddressViewId>` | Address views that should not be displayed. See below the list of the possible id values. | - | - | `'["top_accounts"]'` | v1.15.0+ |
| NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED | `boolean` | Set to `true` if SolidityScan reports are supported | - | - | `true` | v1.19.0+ |
| NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS | `Array<'solidity-hardhat' \| 'solidity-foundry'>` | Pass an array of additional methods from which users can choose while verifying a smart contract. Both methods are available by default, pass `'none'` string to disable them all. | - | - | `['solidity-hardhat']` | v1.33.0+ |
##### Address views list
| Id | Description |
......
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 29 28">
<g fill="currentColor" clip-path="url(#clock-light_svg__a)">
<path d="M14.75 25.375a11.375 11.375 0 1 1 0-22.75 11.375 11.375 0 0 1 0 22.75Zm0-21a9.625 9.625 0 1 0 0 19.25 9.625 9.625 0 0 0 0-19.25Z"/>
<path d="M19.563 19.688a.875.875 0 0 1-.622-.254L14.13 14.62a.874.874 0 0 1-.254-.621V7a.875.875 0 0 1 1.75 0v6.641l4.559 4.55a.874.874 0 0 1-.622 1.497Z"/>
</g>
<defs>
<clipPath id="clock-light_svg__a">
<path fill="#fff" d="M.75 0h28v28h-28z"/>
</clipPath>
</defs>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.444 18.315a10 10 0 1 0 11.112-16.63 10 10 0 0 0-11.112 16.63ZM5.3 2.965a8.462 8.462 0 1 1 9.402 14.07 8.462 8.462 0 0 1-9.402-14.07Zm8.637 11.978a.768.768 0 0 0 .295.057.769.769 0 0 0 .546-1.315l-4.008-4V3.846a.769.769 0 1 0-1.538 0V10a.77.77 0 0 0 .223.546l4.23 4.23a.77.77 0 0 0 .252.167Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.666 15.833h.834V17.5H1.666v-1.667H2.5v-12.5a.833.833 0 0 1 .833-.833h7.5a.833.833 0 0 1 .833.833V10h1.667A1.667 1.667 0 0 1 15 11.667V15a.833.833 0 0 0 1.666 0V9.167H15a.833.833 0 0 1-.834-.834V5.345l-1.38-1.38 1.178-1.18 4.125 4.126a.831.831 0 0 1 .244.589V15a2.5 2.5 0 0 1-5 0v-3.333h-1.667v4.166Zm-7.5 0H10v-5H4.167v5Zm0-11.666v5H10v-5H4.167Z" fill="currentColor"/>
<g clip-path="url(#a)">
<path d="M12 17h1v2H0v-2h1V2a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v8h2a2 2 0 0 1 2 2v4a1 1 0 0 0 2 0V9h-2a1 1 0 0 1-1-1V4.414l-1.657-1.657 1.414-1.414 4.95 4.95A.996.996 0 0 1 20 7v9a3 3 0 0 1-6 0v-4h-2v5Zm-9 0h7v-6H3v6ZM3 3v6h7V3H3Z" fill="currentColor"/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h20v20H0z"/>
</clipPath>
</defs>
</svg>
......@@ -54,7 +54,7 @@ import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransac
import type { BackendVersionConfig } from 'types/api/configs';
import type {
SmartContract,
SmartContractVerificationConfig,
SmartContractVerificationConfigRaw,
SolidityscanReport,
SmartContractSecurityAudits,
} from 'types/api/contract';
......@@ -1028,7 +1028,7 @@ Q extends 'contract_solidityscan_report' ? SolidityscanReport :
Q extends 'verified_contracts' ? VerifiedContractsResponse :
Q extends 'verified_contracts_counters' ? VerifiedContractsCounters :
Q extends 'visualize_sol2uml' ? visualizer.VisualizeResponse :
Q extends 'contract_verification_config' ? SmartContractVerificationConfig :
Q extends 'contract_verification_config' ? SmartContractVerificationConfigRaw :
Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters :
Q extends 'optimistic_l2_output_roots' ? OptimisticL2OutputRootsResponse :
......
......@@ -78,6 +78,8 @@ export default function useTimeAgoIncrement(ts: string | null, isEnabled?: boole
isEnabled && startIncrement();
!isEnabled && setValue(dayjs(ts).fromNow());
return () => {
timeouts.forEach(window.clearTimeout);
intervals.forEach(window.clearInterval);
......
......@@ -64,6 +64,11 @@ export const withoutBothPrices: HomeStats = {
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null, fiat_price: null }) : null),
};
export const withoutGasInfo: HomeStats = {
...base,
gas_prices: null,
};
export const withSecondaryCoin: HomeStats = {
...base,
secondary_coin_price: '3.398',
......
......@@ -144,6 +144,16 @@ export const apiDocs: GetServerSideProps<Props> = async(context) => {
return base(context);
};
export const graphIQl: GetServerSideProps<Props> = async(context) => {
if (!config.features.graphqlApiDocs.isEnabled) {
return {
notFound: true,
};
}
return base(context);
};
export const csvExport: GetServerSideProps<Props> = async(context) => {
if (!config.features.csvExport.isEnabled) {
return {
......
......@@ -27,4 +27,4 @@ const Page: NextPage = () => {
export default Page;
export { base as getServerSideProps } from 'nextjs/getServerSideProps';
export { graphIQl as getServerSideProps } from 'nextjs/getServerSideProps';
......@@ -75,7 +75,7 @@ export interface SmartContractExternalLibrary {
// VERIFICATION
export type SmartContractVerificationMethod = 'flattened-code' | 'standard-input' | 'sourcify' | 'multi-part'
export type SmartContractVerificationMethodApi = 'flattened-code' | 'standard-input' | 'sourcify' | 'multi-part'
| 'vyper-code' | 'vyper-multi-part' | 'vyper-standard-input';
export interface SmartContractVerificationConfigRaw {
......@@ -88,10 +88,6 @@ export interface SmartContractVerificationConfigRaw {
license_types: Record<SmartContractLicenseType, number>;
}
export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw {
verification_options: Array<SmartContractVerificationMethod>;
}
export type SmartContractVerificationResponse = {
status: 'error';
errors: SmartContractVerificationError;
......
import type { SmartContractLicenseType } from 'types/api/contract';
import type { SmartContractLicenseType, SmartContractVerificationConfigRaw, SmartContractVerificationMethodApi } from 'types/api/contract';
export interface ContractCodeIde {
title: string;
......@@ -12,3 +12,16 @@ export interface ContractLicense {
label: string;
title: string;
}
export const SMART_CONTRACT_EXTRA_VERIFICATION_METHODS = [
'solidity-hardhat' as const,
'solidity-foundry' as const,
];
export type SmartContractVerificationMethodExtra = (typeof SMART_CONTRACT_EXTRA_VERIFICATION_METHODS)[number];
export type SmartContractVerificationMethod = SmartContractVerificationMethodApi | SmartContractVerificationMethodExtra;
export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw {
verification_options: Array<SmartContractVerificationMethod>;
}
......@@ -5,6 +5,7 @@ import type { AbiParameter } from 'viem';
import { route } from 'nextjs-routes';
import { WEI } from 'lib/consts';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import LinkInternal from 'ui/shared/links/LinkInternal';
......@@ -51,7 +52,7 @@ const ItemPrimitive = ({ abiParameter, data, level, hideLabel }: Props) => {
const intMatch = matchInt(abiParameter.type);
if (intMatch && typeof data === 'bigint' && intMatch.max > INT_TOOLTIP_THRESHOLD && data > INT_TOOLTIP_THRESHOLD) {
const dividedValue = BigNumber(data.toString()).div(BigNumber(INT_TOOLTIP_THRESHOLD));
const dividedValue = BigNumber(data.toString()).div(WEI);
return (
<Tooltip label={ dividedValue.toLocaleString() + ' ETH' }>
<span>{ castValueToString(data) }</span>
......
......@@ -6,6 +6,7 @@ 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 DataFetchAlert from 'ui/shared/DataFetchAlert';
import TruncatedValue from 'ui/shared/TruncatedValue';
import AddressMudBreadcrumbs from './AddressMudBreadcrumbs';
......@@ -36,7 +37,7 @@ const AddressMudRecord = ({ tableId, recordId, isQueryEnabled = true, scrollRef
}
if (isError) {
return <Box>error message</Box>;
return <DataFetchAlert/>;
}
return (
......
......@@ -47,7 +47,7 @@ const AddressMudRecordsTable = ({
}: Props) => {
const totalColsCut = data.schema.key_names.length + data.schema.value_names.length;
const isMobile = useIsMobile(false);
const [ colsCutCount, setColsCutCount ] = React.useState<number>(isMobile ? 2 : 0);
const [ colsCutCount, setColsCutCount ] = React.useState<number>(isMobile ? MIN_CUT_COUNT : 0);
const [ isOpened, setIsOpened ] = useBoolean(false);
const [ hasCut, setHasCut ] = useBoolean(isMobile ? totalColsCut > MIN_CUT_COUNT : true);
......@@ -70,11 +70,14 @@ const AddressMudRecordsTable = ({
e.preventDefault();
router.push(
{ pathname: '/address/[hash]', query: { hash, tab: 'mud', table_id: data.table.table_id, record_id: e.currentTarget.getAttribute('data-id') as string } },
undefined,
{ shallow: true },
);
const recordId = e.currentTarget.getAttribute('data-id');
if (recordId) {
router.push(
{ pathname: '/address/[hash]', query: { hash, tab: 'mud', table_id: data.table.table_id, record_id: recordId } },
undefined,
{ shallow: true },
);
}
scrollRef?.current?.scrollIntoView();
}, [ router, scrollRef, hash, data.table.table_id ]);
......@@ -96,7 +99,7 @@ const AddressMudRecordsTable = ({
React.useEffect(() => {
if (hasCut && !colsCutCount && containerRef.current) {
const count = Math.floor((containerRef.current.getBoundingClientRect().width - CUT_COL_WIDTH) / COL_MIN_WIDTH);
if (totalColsCut > 2 && count - 1 < totalColsCut) {
if (totalColsCut > MIN_CUT_COUNT && count - 1 < totalColsCut) {
setColsCutCount(count - 1);
} else {
setHasCut.off();
......
......@@ -19,6 +19,9 @@ import AddressMudBreadcrumbs from './AddressMudBreadcrumbs';
import AddressMudRecordsTable from './AddressMudRecordsTable';
import { getNameTypeText, SORT_SEQUENCE } from './utils';
const BREADCRUMBS_HEIGHT = 60;
const FILTERS_HEIGHT = 44;
type Props ={
scrollRef?: React.RefObject<HTMLDivElement>;
isQueryEnabled?: boolean;
......@@ -65,10 +68,10 @@ const AddressMudTable = ({ scrollRef, tableId, isQueryEnabled = true }: Props) =
const hasActiveFilters = Object.values(filters).some(Boolean);
const actionBatHeight = React.useMemo(() => {
const heightWithoutFilters = pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 60;
const actionBarHeight = React.useMemo(() => {
const heightWithoutFilters = pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : BREADCRUMBS_HEIGHT;
return hasActiveFilters ? heightWithoutFilters + 44 : heightWithoutFilters;
return hasActiveFilters ? heightWithoutFilters + FILTERS_HEIGHT : heightWithoutFilters;
}, [ pagination.isVisible, hasActiveFilters ]);
if (isLoading) {
......@@ -76,7 +79,7 @@ const AddressMudTable = ({ scrollRef, tableId, isQueryEnabled = true }: Props) =
}
const filtersTags = hasActiveFilters ? (
<HStack gap={ 3 } mb={ 1 }>
<HStack gap={ 3 } mb={ 1 } flexWrap="wrap">
{ Object.entries(filters).map(([ key, value ]) => {
const index = key as FilterKeys === 'filter_key0' ? 0 : 1;
return (
......@@ -118,7 +121,7 @@ const AddressMudTable = ({ scrollRef, tableId, isQueryEnabled = true }: Props) =
const content = data?.items ? (
<AddressMudRecordsTable
data={ data }
top={ actionBatHeight }
top={ actionBarHeight }
sorting={ sorting }
toggleSorting={ toggleSorting }
setFilters={ setFilters }
......
......@@ -32,11 +32,15 @@ const AddressMudTablesListItem = ({ item, isLoading, scrollRef, hash }: Props) =
e.preventDefault();
router.push(
{ pathname: '/address/[hash]', query: { hash, tab: 'mud', table_id: e.currentTarget.getAttribute('data-id') as string } },
undefined,
{ shallow: true },
);
const tableId = e.currentTarget.getAttribute('data-id');
if (tableId) {
router.push(
{ pathname: '/address/[hash]', query: { hash, tab: 'mud', table_id: tableId } },
undefined,
{ shallow: true },
);
}
scrollRef?.current?.scrollIntoView();
}, [ router, scrollRef, hash ]);
......
......@@ -30,11 +30,14 @@ const AddressMudTablesTableItem = ({ item, isLoading, scrollRef, hash }: Props)
e.preventDefault();
router.push(
{ pathname: '/address/[hash]', query: { hash, tab: 'mud', table_id: e.currentTarget.getAttribute('data-id') as string } },
undefined,
{ shallow: true },
);
const tableId = e.currentTarget.getAttribute('data-id');
if (tableId) {
router.push(
{ pathname: '/address/[hash]', query: { hash, tab: 'mud', table_id: tableId } },
undefined,
{ shallow: true },
);
}
scrollRef?.current?.scrollIntoView();
}, [ router, scrollRef, hash ]);
......
......@@ -49,8 +49,7 @@ const AddressesTableItem = ({
</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" as="span">{ addressBalanceChunks[0] + (addressBalanceChunks[1] ? '.' : '') }</Text>
<Text lineHeight="24px" variant="secondary" as="span">{ addressBalanceChunks[1] }</Text>
</Skeleton>
</Td>
......
import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import * as socketServer from 'playwright/fixtures/socketServer';
import { test, expect } from 'playwright/lib';
......@@ -37,6 +37,8 @@ const formConfig: SmartContractVerificationConfig = {
'vyper-code',
'vyper-multi-part',
'vyper-standard-input',
'solidity-hardhat',
'solidity-foundry',
],
vyper_compiler_versions: [
'v0.3.7+commit.6020b8bb',
......@@ -82,7 +84,7 @@ test('flatten source code method +@dark-mode +@mobile', async({ render, page })
// select method
await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).fill('solidity');
await page.getByRole('button', { name: /flattened source code/i }).click();
await page.getByRole('button', { name: /single file/i }).click();
await page.getByText(/add contract libraries/i).click();
await page.locator('button[aria-label="add"]').click();
......@@ -191,3 +193,25 @@ test('vyper vyper-standard-input method', async({ render, page }) => {
await expect(component).toHaveScreenshot();
});
test('solidity-hardhat method', async({ render, page }) => {
const component = await render(<ContractVerificationForm config={ formConfig } hash={ hash }/>, { hooksConfig });
// select method
await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).fill('hardhat');
await page.getByRole('button', { name: /hardhat/i }).click();
await expect(component).toHaveScreenshot();
});
test('solidity-foundry method', async({ render, page }) => {
const component = await render(<ContractVerificationForm config={ formConfig } hash={ hash }/>, { hooksConfig });
// select method
await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).fill('foundry');
await page.getByRole('button', { name: /foundry/i }).click();
await expect(component).toHaveScreenshot();
});
......@@ -5,7 +5,8 @@ import { useForm, FormProvider } from 'react-hook-form';
import type { FormFields } from './types';
import type { SocketMessage } from 'lib/socket/types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig, SmartContract } from 'types/api/contract';
import type { SmartContract, SmartContractVerificationMethodApi } from 'types/api/contract';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import { route } from 'nextjs-routes';
......@@ -22,6 +23,8 @@ import ContractVerificationFieldLicenseType from './fields/ContractVerificationF
import ContractVerificationFieldMethod from './fields/ContractVerificationFieldMethod';
import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode';
import ContractVerificationMultiPartFile from './methods/ContractVerificationMultiPartFile';
import ContractVerificationSolidityFoundry from './methods/ContractVerificationSolidityFoundry';
import ContractVerificationSolidityHardhat from './methods/ContractVerificationSolidityHardhat';
import ContractVerificationSourcify from './methods/ContractVerificationSourcify';
import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput';
import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract';
......@@ -30,7 +33,7 @@ import ContractVerificationVyperStandardInput from './methods/ContractVerificati
import { prepareRequestBody, formatSocketErrors, getDefaultValues, METHOD_LABELS } from './utils';
interface Props {
method?: SmartContractVerificationMethod;
method?: SmartContractVerificationMethodApi;
config: SmartContractVerificationConfig;
hash?: string;
}
......@@ -159,6 +162,8 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
'vyper-code': <ContractVerificationVyperContract config={ config }/>,
'vyper-multi-part': <ContractVerificationVyperMultiPartFile/>,
'vyper-standard-input': <ContractVerificationVyperStandardInput/>,
'solidity-hardhat': <ContractVerificationSolidityHardhat config={ config }/>,
'solidity-foundry': <ContractVerificationSolidityFoundry/>,
};
}, [ config ]);
const method = watch('method');
......@@ -193,7 +198,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
/>
</Grid>
{ content }
{ Boolean(method) && (
{ Boolean(method) && method.value !== 'solidity-hardhat' && method.value !== 'solidity-foundry' && (
<Button
variant="solid"
size="lg"
......
import { Code } from '@chakra-ui/react';
import React from 'react';
interface Props {
code: string;
}
const ContractVerificationFormCodeSnippet = ({ code }: Props) => {
return (
<Code whiteSpace="pre-wrap" wordBreak="break-all" p={ 2 } borderRadius="base">
{ code }
</Code>
);
};
export default React.memo(ContractVerificationFormCodeSnippet);
import { FormControl, Link, Textarea } from '@chakra-ui/react';
import { FormControl, Textarea } from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
......@@ -43,12 +43,7 @@ const ContractVerificationFieldCode = ({ isVyper }: Props) => {
rules={{ required: true }}
/>
{ isVyper ? null : (
<>
<span>We recommend using flattened code. This is necessary if your code utilizes a library or inherits dependencies. Use the </span>
<Link href="https://hardhat.org/hardhat-runner/docs/advanced/flattening" target="_blank">Hardhat flattener</Link>
<span> or the </span>
<Link href="https://www.npmjs.com/package/truffle-flattener" target="_blank">Truffle flattener</Link>
</>
<span>If your code utilizes a library or inherits dependencies, we recommend using other verification methods instead.</span>
) }
</ContractVerificationFormRow>
);
......
......@@ -5,7 +5,7 @@ import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
......
......@@ -5,7 +5,7 @@ import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
......
......@@ -17,7 +17,7 @@ import type { ControllerRenderProps, Control } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { FormFields } from '../types';
import type { SmartContractVerificationConfig, SmartContractVerificationMethod } from 'types/api/contract';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/client/contract';
import useIsMobile from 'lib/hooks/useIsMobile';
import Popover from 'ui/shared/chakra/Popover';
......@@ -58,7 +58,7 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
const renderPopoverListItem = React.useCallback((method: SmartContractVerificationMethod) => {
switch (method) {
case 'flattened-code':
return <ListItem key={ method }>Verification through flattened source code.</ListItem>;
return <ListItem key={ method }>Verification through a single file.</ListItem>;
case 'multi-part':
return <ListItem key={ method }>Verification of multi-part Solidity files.</ListItem>;
case 'sourcify':
......@@ -93,6 +93,10 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
<span> file.</span>
</ListItem>
);
case 'solidity-hardhat':
return <ListItem key={ method }>Verification through Hardhat plugin.</ListItem>;
case 'solidity-foundry':
return <ListItem key={ method }>Verification through Foundry.</ListItem>;
}
}, []);
......
import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs';
......
import { Box, Flex, Link } from '@chakra-ui/react';
import React from 'react';
import { useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import config from 'configs/app';
import ContractVerificationFormCodeSnippet from '../ContractVerificationFormCodeSnippet';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
import ContractVerificationMethod from '../ContractVerificationMethod';
const ContractVerificationSolidityFoundry = () => {
const { watch } = useFormContext<FormFields>();
const address = watch('address');
const codeSnippet = `forge verify-contract \\
--rpc-url ${ config.chain.rpcUrl || `${ config.api.endpoint }/api/eth-rpc` } \\
--verifier blockscout \\
--verifier-url '${ config.api.endpoint }/api/' \\
${ address || '<address>' } \\
[contractFile]:[contractName]`;
return (
<ContractVerificationMethod title="Contract verification via Foundry">
<ContractVerificationFormRow>
<Flex flexDir="column">
<ContractVerificationFormCodeSnippet code={ codeSnippet }/>
</Flex>
<Box whiteSpace="pre-wrap">
<span>Full tutorial about contract verification via Foundry on Blockscout is available </span>
<Link href="https://docs.blockscout.com/for-users/verifying-a-smart-contract/foundry-verification" target="_blank">
here
</Link>
</Box>
</ContractVerificationFormRow>
</ContractVerificationMethod>
);
};
export default React.memo(ContractVerificationSolidityFoundry);
import { Box, Flex, Link } from '@chakra-ui/react';
import React from 'react';
import { useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import config from 'configs/app';
import ContractVerificationFormCodeSnippet from '../ContractVerificationFormCodeSnippet';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
import ContractVerificationMethod from '../ContractVerificationMethod';
const ContractVerificationSolidityHardhat = ({ config: formConfig }: { config: SmartContractVerificationConfig }) => {
const chainNameSlug = config.chain.name?.toLowerCase().split(' ').join('-');
const { watch } = useFormContext<FormFields>();
const address = watch('address');
const latestSolidityVersion = formConfig.solidity_compiler_versions.find((version) => !version.includes('nightly'))?.split('+')[0];
const firstCodeSnippet = `const config: HardhatUserConfig = {
solidity: "${ latestSolidityVersion || '0.8.24' }", // replace if necessary
networks: {
'${ chainNameSlug }': {
url: '${ config.chain.rpcUrl || `${ config.api.endpoint }/api/eth-rpc` }'
},
},
etherscan: {
apiKey: {
'${ chainNameSlug }': 'empty'
},
customChains: [
{
network: "${ chainNameSlug }",
chainId: ${ config.chain.id },
urls: {
apiURL: "${ config.api.endpoint }/api",
browserURL: "${ config.app.baseUrl }"
}
}
]
}
};`;
const secondCodeSnippet = `npx hardhat verify \\
--network ${ chainNameSlug } \\
${ address || '<address>' } \\
[...constructorArgs]`;
return (
<ContractVerificationMethod title="Contract verification via Solidity Hardhat plugin">
<ContractVerificationFormRow>
<Flex flexDir="column" rowGap={ 3 }>
<ContractVerificationFormCodeSnippet code={ firstCodeSnippet }/>
<ContractVerificationFormCodeSnippet code={ secondCodeSnippet }/>
</Flex>
<Box whiteSpace="pre-wrap">
<span>Full tutorial about contract verification via Hardhat on Blockscout is available </span>
<Link href="https://docs.blockscout.com/for-users/verifying-a-smart-contract/hardhat-verification-plugin" target="_blank">
here
</Link>
</Box>
</ContractVerificationFormRow>
</ContractVerificationMethod>
);
};
export default React.memo(ContractVerificationSolidityHardhat);
import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs';
......
import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode';
......
import type { SmartContractLicenseType, SmartContractVerificationMethod } from 'types/api/contract';
import type { SmartContractLicenseType } from 'types/api/contract';
import type { SmartContractVerificationMethod } from 'types/client/contract';
import type { Option } from 'ui/shared/FancySelect/types';
export interface ContractLibrary {
......
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { isValidVerificationMethod, sortVerificationMethods } from './utils';
......@@ -8,7 +9,10 @@ export default function useFormConfigQuery(enabled: boolean) {
select: (data) => {
return {
...data,
verification_options: data.verification_options.filter(isValidVerificationMethod).sort(sortVerificationMethods),
verification_options: [
...data.verification_options,
...config.UI.views.address.extraVerificationMethods,
].filter(isValidVerificationMethod).sort(sortVerificationMethods),
};
},
enabled,
......
......@@ -12,11 +12,10 @@ import type {
FormFieldsVyperStandardInput,
} from './types';
import type {
SmartContractVerificationMethod,
SmartContractVerificationError,
SmartContractVerificationConfig,
SmartContractLicenseType,
} from 'types/api/contract';
import type { SmartContractVerificationConfig, SmartContractVerificationMethod } from 'types/client/contract';
import type { Params as FetchParams } from 'lib/hooks/useFetch';
......@@ -25,19 +24,23 @@ export const SUPPORTED_VERIFICATION_METHODS: Array<SmartContractVerificationMeth
'standard-input',
'sourcify',
'multi-part',
'solidity-hardhat',
'solidity-foundry',
'vyper-code',
'vyper-multi-part',
'vyper-standard-input',
];
export const METHOD_LABELS: Record<SmartContractVerificationMethod, string> = {
'flattened-code': 'Solidity (Flattened source code)',
'flattened-code': 'Solidity (Single file)',
'standard-input': 'Solidity (Standard JSON input)',
sourcify: 'Solidity (Sourcify)',
'multi-part': 'Solidity (Multi-part files)',
'vyper-code': 'Vyper (Contract)',
'vyper-multi-part': 'Vyper (Multi-part files)',
'vyper-standard-input': 'Vyper (Standard JSON input)',
'solidity-hardhat': 'Solidity (Hardhat)',
'solidity-foundry': 'Solidity (Foundry)',
};
export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> = {
......@@ -129,6 +132,26 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
sources: [],
license_type: null,
},
'solidity-hardhat': {
address: '',
method: {
value: 'solidity-hardhat' as const,
label: METHOD_LABELS['solidity-hardhat'],
},
compiler: null,
sources: [],
license_type: null,
},
'solidity-foundry': {
address: '',
method: {
value: 'solidity-foundry' as const,
label: METHOD_LABELS['solidity-foundry'],
},
compiler: null,
sources: [],
license_type: null,
},
};
export function getDefaultValues(
......
......@@ -19,6 +19,13 @@ test.describe('all items', () => {
});
});
test('no gas info', async({ render, mockApiResponse }) => {
await mockApiResponse('stats', statsMock.withoutGasInfo);
const component = await render(<Stats/>);
await expect(component).toHaveScreenshot();
});
test('4 items default view +@mobile -@default', async({ render, mockApiResponse, mockEnvs }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME', 'false' ],
......
......@@ -9,6 +9,7 @@ import { HOMEPAGE_STATS } from 'stubs/stats';
import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip';
import GasPrice from 'ui/shared/gas/GasPrice';
import IconSvg from 'ui/shared/IconSvg';
import type { Props as StatsWidgetProps } from 'ui/shared/stats/StatsWidget';
import StatsWidget from 'ui/shared/stats/StatsWidget';
const hasAvgBlockTime = config.UI.homepage.showAvgBlockTime;
......@@ -53,19 +54,10 @@ const Stats = () => {
(rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && zkEvmLatestBatchQuery.isPlaceholderData) ||
(rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && zkSyncLatestBatchQuery.isPlaceholderData);
let content;
const lastItemStyle = { gridColumn: 'span 2' };
let itemsCount = 5;
!hasGasTracker && itemsCount--;
!hasAvgBlockTime && itemsCount--;
if (data) {
!data.gas_prices && itemsCount--;
data.rootstock_locked_btc && itemsCount++;
rollupFeature.isEnabled && data.last_output_root_size && itemsCount++;
const isOdd = Boolean(itemsCount % 2);
const content = (() => {
if (!data) {
return null;
}
const gasInfoTooltip = hasGasTracker && data.gas_prices && data.gas_prices.average ? (
<GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt }>
<IconSvg
......@@ -80,88 +72,82 @@ const Stats = () => {
</GasInfoTooltip>
) : null;
content = (
const items: Array<StatsWidgetProps> = [
rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && {
icon: 'txn_batches_slim' as const,
label: 'Latest batch',
value: (zkEvmLatestBatchQuery.data || 0).toLocaleString(),
href: { pathname: '/batches' as const },
isLoading,
},
rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && {
icon: 'txn_batches_slim' as const,
label: 'Latest batch',
value: (zkSyncLatestBatchQuery.data || 0).toLocaleString(),
href: { pathname: '/batches' as const },
isLoading,
},
!(rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync')) && {
icon: 'block_slim' as const,
label: 'Total blocks',
value: Number(data.total_blocks).toLocaleString(),
href: { pathname: '/blocks' as const },
isLoading,
},
hasAvgBlockTime && {
icon: 'clock-light' as const,
label: 'Average block time',
value: `${ (data.average_block_time / 1000).toFixed(1) }s`,
isLoading,
},
{
icon: 'transactions_slim' as const,
label: 'Total transactions',
value: Number(data.total_transactions).toLocaleString(),
href: { pathname: '/txs' as const },
isLoading,
},
rollupFeature.isEnabled && data.last_output_root_size && {
icon: 'txn_batches_slim' as const,
label: 'Latest L1 state batch',
value: data.last_output_root_size,
href: { pathname: '/batches' as const },
isLoading,
},
{
icon: 'wallet' as const,
label: 'Wallet addresses',
value: Number(data.total_addresses).toLocaleString(),
isLoading,
},
hasGasTracker && data.gas_prices && {
icon: 'gas' as const,
label: 'Gas tracker',
value: data.gas_prices.average ? <GasPrice data={ data.gas_prices.average }/> : 'N/A',
hint: gasInfoTooltip,
isLoading,
},
data.rootstock_locked_btc && {
icon: 'coins/bitcoin' as const,
label: 'BTC Locked in 2WP',
value: `${ BigNumber(data.rootstock_locked_btc).div(WEI).dp(0).toFormat() } RBTC`,
isLoading,
},
].filter(Boolean);
return (
<>
{ rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && (
<StatsWidget
icon="txn_batches_slim"
label="Latest batch"
value={ (zkEvmLatestBatchQuery.data || 0).toLocaleString() }
href={{ pathname: '/batches' }}
isLoading={ isLoading }
/>
) }
{ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && (
{ items.map((item, index) => (
<StatsWidget
icon="txn_batches_slim"
label="Latest batch"
value={ (zkSyncLatestBatchQuery.data || 0).toLocaleString() }
href={{ pathname: '/batches' }}
key={ item.icon }
{ ...item }
isLoading={ isLoading }
/>
) }
{ !(rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync')) && (
<StatsWidget
icon="block_slim"
label="Total blocks"
value={ Number(data.total_blocks).toLocaleString() }
href={{ pathname: '/blocks' }}
isLoading={ isLoading }
/>
) }
{ hasAvgBlockTime && (
<StatsWidget
icon="clock"
label="Average block time"
value={ `${ (data.average_block_time / 1000).toFixed(1) }s` }
isLoading={ isLoading }
/>
) }
<StatsWidget
icon="transactions_slim"
label="Total transactions"
value={ Number(data.total_transactions).toLocaleString() }
href={{ pathname: '/txs' }}
isLoading={ isLoading }
/>
{ rollupFeature.isEnabled && data.last_output_root_size && (
<StatsWidget
icon="txn_batches_slim"
label="Latest L1 state batch"
value={ data.last_output_root_size }
href={{ pathname: '/batches' }}
isLoading={ isLoading }
/>
) }
<StatsWidget
icon="wallet"
label="Wallet addresses"
value={ Number(data.total_addresses).toLocaleString() }
isLoading={ isLoading }
_last={ isOdd ? lastItemStyle : undefined }
/>
{ hasGasTracker && data.gas_prices && (
<StatsWidget
icon="gas"
label="Gas tracker"
value={ data.gas_prices.average ? <GasPrice data={ data.gas_prices.average }/> : 'N/A' }
hint={ gasInfoTooltip }
isLoading={ isLoading }
_last={ isOdd ? lastItemStyle : undefined }
/>
) }
{ data.rootstock_locked_btc && (
<StatsWidget
icon="coins/bitcoin"
label="BTC Locked in 2WP"
value={ `${ BigNumber(data.rootstock_locked_btc).div(WEI).dp(0).toFormat() } RBTC` }
isLoading={ isLoading }
_last={ isOdd ? lastItemStyle : undefined }
/>
_last={ items.length % 2 === 1 && index === items.length - 1 ? { gridColumn: 'span 2' } : undefined }/>
),
) }
</>
);
}
})();
return (
<Grid
......
......@@ -20,8 +20,7 @@ const MudWorldsTableItem = ({ item, isLoading }: Props) => {
</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" as="span">{ addressBalanceChunks[0] + (addressBalanceChunks[1] ? '.' : '') }</Text>
<Text lineHeight="24px" variant="secondary" as="span">{ addressBalanceChunks[1] }</Text>
</Skeleton>
</Td>
......
......@@ -106,10 +106,11 @@ const AddressPageContent = () => {
addressEnsDomainsQuery.data?.items.find((domain) => domain.name === addressQuery.data?.ens_domain_name) :
undefined;
const isLoading = addressQuery.isPlaceholderData || (config.features.userOps.isEnabled && userOpsAccountQuery.isPlaceholderData);
const isLoading = addressQuery.isPlaceholderData;
const isTabsLoading =
isLoading ||
addressTabsCountersQuery.isPlaceholderData ||
(config.features.userOps.isEnabled && userOpsAccountQuery.isPlaceholderData) ||
(config.features.mudFramework.isEnabled && mudTablesCountQuery.isPlaceholderData);
const handleFetchedBytecodeMessage = React.useCallback(() => {
......
import { useRouter } from 'next/router';
import React from 'react';
import type { SmartContractVerificationMethod } from 'types/api/contract';
import type { SmartContractVerificationMethodApi } from 'types/api/contract';
import type { SmartContractVerificationMethod } from 'types/client/contract';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
......@@ -59,7 +60,7 @@ const ContractVerificationForAddress = () => {
return (
<ContractVerificationForm
method={ method && configQuery.data.verification_options.includes(method) ? method : undefined }
method={ method && configQuery.data.verification_options.includes(method) ? method as SmartContractVerificationMethodApi : undefined }
config={ configQuery.data }
hash={ hash }
/>
......
......@@ -13,7 +13,7 @@ type FilterProps = {
type Props = {
isError: boolean;
items?: Array<unknown>;
emptyText: string | React.ReactNode;
emptyText: React.ReactNode;
actionBar?: React.ReactNode;
showActionBarIfEmpty?: boolean;
content: React.ReactNode;
......
......@@ -9,19 +9,6 @@ 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;
......@@ -32,6 +19,13 @@ type ContentProps = {
children: React.ReactNode;
}
type Props = ContentProps & {
columnName: string;
isActive?: boolean;
isLoading?: boolean;
className?: string;
}
const TableColumnFilterContent = ({ title, isFilled, hasReset, onFilter, onReset, onClose, children }: ContentProps) => {
const onFilterClick = React.useCallback(() => {
onClose && onClose();
......
......@@ -5,6 +5,7 @@ import {
useDisclosure,
IconButton,
chakra,
Portal,
} from '@chakra-ui/react';
import React from 'react';
......@@ -32,7 +33,7 @@ const TableColumnFilterWrapper = ({ columnName, isActive, className, children, i
);
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy lazyBehavior="unmount" strategy="fixed">
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy lazyBehavior="unmount">
<PopoverTrigger>
<IconButton
onClick={ onToggle }
......@@ -47,11 +48,13 @@ const TableColumnFilterWrapper = ({ columnName, isActive, className, children, i
color="text_secondary"
/>
</PopoverTrigger>
<PopoverContent className={ className }>
<PopoverBody px={ 4 } py={ 6 } display="flex" flexDir="column" rowGap={ 3 }>
{ modifiedChildren }
</PopoverBody>
</PopoverContent>
<Portal>
<PopoverContent className={ className }>
<PopoverBody px={ 4 } py={ 6 } display="flex" flexDir="column" rowGap={ 3 }>
{ modifiedChildren }
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
};
......
......@@ -3,10 +3,7 @@ export default function getNextSortValue<SortField extends string, Sort extends
) {
return (prevValue: Sort | undefined) => {
const sequence = sortSequence[field];
getNextValueFromSequence(sequence, prevValue);
const curIndex = sequence.findIndex((sort) => sort === prevValue);
const nextIndex = curIndex + 1 > sequence.length - 1 ? 0 : curIndex + 1;
return sequence[nextIndex];
return getNextValueFromSequence(sequence, prevValue);
};
}
......
......@@ -8,7 +8,7 @@ import Hint from 'ui/shared/Hint';
import IconSvg, { type IconName } from 'ui/shared/IconSvg';
import TruncatedValue from 'ui/shared/TruncatedValue';
type Props = {
export type Props = {
className?: string;
label: string;
value: string | React.ReactNode;
......
......@@ -26,7 +26,7 @@ const NavLinkGroup = ({ item }: Props) => {
return (
<Popover
trigger="hover"
placement="bottom-start"
placement="bottom"
isLazy
gutter={ 8 }
>
......
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