diff --git a/.changelog/1441.internal.md b/.changelog/1441.internal.md new file mode 100644 index 000000000..9de0c9493 --- /dev/null +++ b/.changelog/1441.internal.md @@ -0,0 +1 @@ +Add more configuration options for paratimes diff --git a/.env b/.env index 41f5ce993..ffcfc490d 100644 --- a/.env +++ b/.env @@ -27,4 +27,5 @@ REACT_APP_STAGING_URLS=https://explorer.stg.oasis.io REACT_APP_SHOW_BUILD_BANNERS=true # REACT_APP_FIXED_NETWORK=testnet # REACT_APP_FIXED_LAYER=sapphire +# REACT_APP_SKIP_GRAPH=true REACT_APP_SHOW_FIAT_VALUES=true diff --git a/.env.production b/.env.production index 165b287f8..6551bf3ff 100644 --- a/.env.production +++ b/.env.production @@ -22,4 +22,5 @@ REACT_APP_STAGING_URLS=https://explorer.stg.oasis.io REACT_APP_SHOW_BUILD_BANNERS=true # REACT_APP_FIXED_NETWORK=testnet # REACT_APP_FIXED_LAYER=sapphire +# REACT_APP_SKIP_GRAPH=true REACT_APP_SHOW_FIAT_VALUES=true diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index a17c4bcaf..7677f19cb 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -2,7 +2,7 @@ import React from 'react' import { Preview } from '@storybook/react' import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport' import '../src/locales/i18n' -import { withDefaultTheme } from '../src/app/components/ThemeByNetwork' +import { withDefaultTheme } from '../src/app/components/ThemeByScope' import { initialize, mswLoader } from 'msw-storybook-addon' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { handlers } from '../internals/mocks/msw-handlers' diff --git a/src/app/components/AnalyticsConsent/index.tsx b/src/app/components/AnalyticsConsent/index.tsx index 721cdebe0..4c8ff5d7c 100644 --- a/src/app/components/AnalyticsConsent/index.tsx +++ b/src/app/components/AnalyticsConsent/index.tsx @@ -6,7 +6,7 @@ import Button from '@mui/material/Button' import Link from '@mui/material/Link' import { Trans, useTranslation } from 'react-i18next' import * as matomo from './initializeMatomo' -import { ThemeByNetwork } from '../ThemeByNetwork' +import { ThemeByScope } from '../ThemeByScope' import { Network } from '../../../types/network' import { AnalyticsIsBlocked } from './AnalyticsIsBlocked' import { AnalyticsDialogLayout } from './AnalyticsDialogLayout' @@ -63,7 +63,7 @@ export const AnalyticsConsentProvider = (props: { children: React.ReactNode }) = > {props.children} {/* Theme is needed because AnalyticsConsentProvider is outside network-themed routes */} - + { @@ -80,7 +80,7 @@ export const AnalyticsConsentProvider = (props: { children: React.ReactNode }) = onReload={() => window.location.reload()} onClose={() => setHasAccepted('timed_out_matomo_not_loaded')} /> - + ) } diff --git a/src/app/components/ThemeByNetwork/index.tsx b/src/app/components/ThemeByScope/index.tsx similarity index 58% rename from src/app/components/ThemeByNetwork/index.tsx rename to src/app/components/ThemeByScope/index.tsx index da52c9481..23dba0716 100644 --- a/src/app/components/ThemeByNetwork/index.tsx +++ b/src/app/components/ThemeByScope/index.tsx @@ -1,26 +1,28 @@ import { FC, ReactNode } from 'react' import { Network } from '../../../types/network' import { ThemeProvider } from '@mui/material/styles' -import { getThemesForNetworks } from '../../../styles/theme' +import { getThemeForScope } from '../../../styles/theme' import CssBaseline from '@mui/material/CssBaseline' import { fixedNetwork } from '../../utils/route-utils' +import { Layer } from '../../../oasis-nexus/api' -export const ThemeByNetwork: FC<{ network: Network; isRootTheme: boolean; children: React.ReactNode }> = ({ - network, - isRootTheme, - children, -}) => ( - +export const ThemeByScope: FC<{ + network: Network + layer?: Layer + isRootTheme: boolean + children: React.ReactNode +}> = ({ network, layer, isRootTheme, children }) => ( + {isRootTheme && } {children} ) export const withDefaultTheme = (node: ReactNode, alwaysMainnet = false) => ( - {node} - + ) diff --git a/src/app/pages/HomePage/index.tsx b/src/app/pages/HomePage/index.tsx index eab589ecf..0d53f27f6 100644 --- a/src/app/pages/HomePage/index.tsx +++ b/src/app/pages/HomePage/index.tsx @@ -14,7 +14,7 @@ import { useTranslation } from 'react-i18next' import { ParaTimeSelectorStep } from './Graph/types' import { BuildBanner } from '../../components/BuildBanner' import { useSearchQueryNetworkParam } from '../../hooks/useSearchQueryNetworkParam' -import { ThemeByNetwork } from '../../components/ThemeByNetwork' +import { ThemeByScope } from '../../components/ThemeByScope' import { NetworkOfflineBanner } from '../../components/OfflineBanner' import { useIsApiReachable } from '../../components/OfflineBanner/hook' @@ -168,7 +168,7 @@ export const HomePage: FC = () => { )} - + { onGraphZoomedIn={setIsGraphZoomedIn} /> - + diff --git a/src/app/pages/RoutingErrorPage/index.tsx b/src/app/pages/RoutingErrorPage/index.tsx index d581f00b3..c19db1ac9 100644 --- a/src/app/pages/RoutingErrorPage/index.tsx +++ b/src/app/pages/RoutingErrorPage/index.tsx @@ -3,18 +3,18 @@ import Divider from '@mui/material/Divider' import { PageLayout } from '../../components/PageLayout' import { ErrorDisplay } from '../../components/ErrorDisplay' import { useRouteError } from 'react-router-dom' -import { ThemeByNetwork } from '../../components/ThemeByNetwork' +import { ThemeByScope } from '../../components/ThemeByScope' import { useScopeParam } from '../../hooks/useScopeParam' import { Network } from '../../../types/network' export const RoutingErrorPage: FC = () => { const scope = useScopeParam() return ( - + - + ) } diff --git a/src/app/pages/SearchResultsPage/GlobalSearchResultsView.tsx b/src/app/pages/SearchResultsPage/GlobalSearchResultsView.tsx index a847b50de..5d9e78c4c 100644 --- a/src/app/pages/SearchResultsPage/GlobalSearchResultsView.tsx +++ b/src/app/pages/SearchResultsPage/GlobalSearchResultsView.tsx @@ -15,7 +15,7 @@ import { Network, } from '../../../types/network' import { HideMoreResults, ShowMoreResults } from './notifications' -import { getThemesForNetworks } from '../../../styles/theme' +import { getThemeForScope } from '../../../styles/theme' import { orderByLayer } from '../../../types/layers' import { useRedirectIfSingleResult } from './useRedirectIfSingleResult' import { SearchParams } from '../../components/Search/search-utils' @@ -30,7 +30,6 @@ export const GlobalSearchResultsView: FC<{ const [othersOpen, setOthersOpen] = useState(false) useRedirectIfSingleResult(undefined, searchParams, searchResults) - const themes = getThemesForNetworks() const networkNames = getNetworkNames(t) const { searchTerm } = searchParams @@ -51,7 +50,7 @@ export const GlobalSearchResultsView: FC<{ } const otherNetworks = RouteUtils.getEnabledNetworks().filter(isNotMainnet) - const notificationTheme = themes[Network.testnet] + const notificationTheme = getThemeForScope(Network.testnet) const mainnetResults = searchResults.filter(isOnMainnet).sort(orderByLayer) const otherResults = searchResults.filter(isNotOnMainnet).sort(orderByLayer) diff --git a/src/app/pages/SearchResultsPage/ScopedSearchResultsView.tsx b/src/app/pages/SearchResultsPage/ScopedSearchResultsView.tsx index 27cb31e5f..b5e4676b5 100644 --- a/src/app/pages/SearchResultsPage/ScopedSearchResultsView.tsx +++ b/src/app/pages/SearchResultsPage/ScopedSearchResultsView.tsx @@ -7,7 +7,7 @@ import { getInverseFilterForScope, SearchScope, } from '../../../types/searchScope' -import { getThemesForNetworks } from '../../../styles/theme' +import { getThemeForScope } from '../../../styles/theme' import { RouteUtils } from '../../utils/route-utils' import { SearchResults } from './hooks' import { SearchResultsList } from './SearchResultsList' @@ -27,12 +27,13 @@ export const ScopedSearchResultsView: FC<{ const { t } = useTranslation() const [othersOpen, setOthersOpen] = useState(false) const networkNames = getNetworkNames(t) - const themes = getThemesForNetworks() const isInWantedScope = getFilterForScope(wantedScope) const isNotInWantedScope = getInverseFilterForScope(wantedScope) const wantedResults = searchResults.filter(isInWantedScope) const otherResults = searchResults.filter(isNotInWantedScope) - const notificationTheme = themes[otherResults.some(isOnMainnet) ? Network.mainnet : Network.testnet] + const notificationTheme = getThemeForScope( + otherResults.some(isOnMainnet) ? Network.mainnet : Network.testnet, + ) useRedirectIfSingleResult(wantedScope, searchParams, searchResults) diff --git a/src/app/pages/SearchResultsPage/SearchResultsList.tsx b/src/app/pages/SearchResultsPage/SearchResultsList.tsx index 9d40a7a91..d98f07b38 100644 --- a/src/app/pages/SearchResultsPage/SearchResultsList.tsx +++ b/src/app/pages/SearchResultsPage/SearchResultsList.tsx @@ -15,7 +15,7 @@ import { TokenResult, TransactionResult, } from './hooks' -import { getThemesForNetworks } from '../../../styles/theme' +import { getThemeForScope } from '../../../styles/theme' import { Network } from '../../../types/network' import { SubPageCard } from '../../components/SubPageCard' import { AllTokenPrices } from '../../../coin-gecko/api' @@ -44,7 +44,7 @@ export const SearchResultsList: FC<{ if (!numberOfResults) { return null } - const theme = getThemesForNetworks()[networkForTheme] + const theme = getThemeForScope(networkForTheme) return ( diff --git a/src/app/utils/renderWithProviders.tsx b/src/app/utils/renderWithProviders.tsx index 8120e51c7..2556b7361 100644 --- a/src/app/utils/renderWithProviders.tsx +++ b/src/app/utils/renderWithProviders.tsx @@ -1,6 +1,6 @@ import { MemoryRouter } from 'react-router-dom' import { render } from '@testing-library/react' -import { withDefaultTheme } from '../components/ThemeByNetwork' +import { withDefaultTheme } from '../components/ThemeByScope' import React from 'react' import { useIsApiReachable, useRuntimeFreshness } from '../components/OfflineBanner/hook' diff --git a/src/app/utils/route-utils.ts b/src/app/utils/route-utils.ts index b518783aa..0cbbfec79 100644 --- a/src/app/utils/route-utils.ts +++ b/src/app/utils/route-utils.ts @@ -5,12 +5,13 @@ import { AppError, AppErrors } from '../../types/errors' import { EvmTokenType, Layer } from '../../oasis-nexus/api' import { Network } from '../../types/network' import { SearchScope } from '../../types/searchScope' -import { isStableDeploy } from '../../config' +import { isStableDeploy, specialScopePaths } from '../../config' import { getSearchTermFromRequest } from '../components/Search/search-utils' import type { HasLayer } from '../../types/layers' export const fixedNetwork = process.env.REACT_APP_FIXED_NETWORK as Network | undefined export const fixedLayer = process.env.REACT_APP_FIXED_LAYER as Layer | undefined +export const skipGraph = !!fixedLayer || !!(process.env.REACT_APP_SKIP_GRAPH as boolean | undefined) export type ScopeFreedom = | 'network' // We can select only the network @@ -37,6 +38,33 @@ export type SpecifiedPerEnabledLayer = { export type SpecifiedPerEnabledRuntime = SpecifiedPerEnabledLayer +export const specialScopeRecognition: Partial>>> = {} + +function invertSpecialScopePaths() { + const networks = Object.keys(specialScopePaths) as Network[] + + networks.forEach(network => { + const networkPaths = specialScopePaths[network]! + const layers = Object.keys(networkPaths) as Layer[] + layers.forEach(layer => { + const [word1, word2] = networkPaths[layer]! + if (!specialScopeRecognition[word1]) { + specialScopeRecognition[word1] = {} + } + if (specialScopeRecognition[word1]![word2]) { + const other = specialScopeRecognition[word1]![word2]! + console.warn( + `Wrong config: conflicting special scope paths ${word1}/${word2} definitions used both for ${other.network}/${other.layer} and ${network}/${layer} `, + ) + } else { + specialScopeRecognition[word1]![word2] = { network, layer } + } + }) + }) +} + +invertSpecialScopePaths() + export const hiddenLayers: Layer[] = [Layer.pontusxdev] export const isLayerHidden = (layer: Layer): boolean => hiddenLayers.includes(layer) @@ -65,38 +93,38 @@ export abstract class RouteUtils { }, } satisfies Record> - static getDashboardRoute = ({ network, layer }: SearchScope) => { - return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}` + static getScopeRoute = ({ network, layer }: SearchScope) => { + const specialPath = specialScopePaths[network]?.[layer] + const result = specialPath + ? `/${specialPath[0]}/${specialPath[1]}` + : `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}` + return result } - static getLatestTransactionsRoute = ({ network, layer }: SearchScope) => { - return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/tx` + static getDashboardRoute = (scope: SearchScope) => this.getScopeRoute(scope) + + static getLatestTransactionsRoute = (scope: SearchScope) => { + return `${this.getScopeRoute(scope)}/tx` } - static getTopTokensRoute = ({ network, layer }: SearchScope) => { - return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/token` + static getTopTokensRoute = (scope: SearchScope) => { + return `${this.getScopeRoute(scope)}/token` } - static getLatestBlocksRoute = ({ network, layer }: SearchScope) => { - return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/block` + static getLatestBlocksRoute = (scope: SearchScope) => { + return `${this.getScopeRoute(scope)}/block` } - static getBlockRoute = ({ network, layer }: SearchScope, blockHeight: number) => { - return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/block/${encodeURIComponent( - blockHeight, - )}` + static getBlockRoute = (scope: SearchScope, blockHeight: number) => { + return `${this.getScopeRoute(scope)}/block/${encodeURIComponent(blockHeight)}` } static getTransactionRoute = (scope: SearchScope, txHash: string) => { - return `/${encodeURIComponent(scope.network)}/${encodeURIComponent(scope.layer)}/tx/${encodeURIComponent( - txHash, - )}` + return `${this.getScopeRoute(scope)}/tx/${encodeURIComponent(txHash)}` } - static getAccountRoute = ({ network, layer }: SearchScope, account: string) => { - return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/address/${encodeURIComponent( - account, - )}` + static getAccountRoute = (scope: SearchScope, accountAddress: string) => { + return `${this.getScopeRoute(scope)}/address/${encodeURIComponent(accountAddress)}` } static getAccountsRoute = (network: Network) => { @@ -128,28 +156,20 @@ export abstract class RouteUtils { static getSearchRoute = (scope: SearchScope | undefined, searchTerm: string) => { return scope - ? `/${encodeURIComponent(scope.network)}/${encodeURIComponent(scope.layer)}/search?q=${encodeURIComponent(searchTerm)}` + ? `${this.getScopeRoute(scope)}/search?q=${encodeURIComponent(searchTerm)}` : `/search?q=${encodeURIComponent(searchTerm)}` } - static getTokenRoute = ({ network, layer }: SearchScope, tokenAddress: string) => { - return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/token/${encodeURIComponent( - tokenAddress, - )}` + static getTokenRoute = (scope: SearchScope, tokenAddress: string) => { + return `${this.getScopeRoute(scope)}/token/${encodeURIComponent(tokenAddress)}` } - static getTokenHoldersRoute = ({ network, layer }: SearchScope, tokenAddress: string) => { - return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/token/${encodeURIComponent( - tokenAddress, - )}/holders` + static getTokenHoldersRoute = (scope: SearchScope, tokenAddress: string) => { + return `${this.getScopeRoute(scope)}/token/${encodeURIComponent(tokenAddress)}/holders` } - static getNFTInstanceRoute = ( - { network, layer }: SearchScope, - contractAddress: string, - instanceId: string, - ): string => - `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/token/${encodeURIComponent( + static getNFTInstanceRoute = (scope: SearchScope, contractAddress: string, instanceId: string): string => + `${this.getScopeRoute(scope)}/token/${encodeURIComponent( contractAddress, )}/instance/${encodeURIComponent(instanceId)}` @@ -315,19 +335,25 @@ export const runtimeTransactionParamLoader = async ({ params }: LoaderFunctionAr return validateRuntimeTxHashParam(params.hash!) } -export const assertEnabledScope = ({ - network, - layer, -}: { +export const assertEnabledScope = (params: { network: string | undefined layer: string | undefined }): SearchScope => { - if (!network || !RouteUtils.getEnabledNetworks().includes(network as Network)) { + const { network: networkLike, layer: layerLike } = params + if (!networkLike || !layerLike) { + throw new AppError(AppErrors.InvalidUrl) + } + + const { network, layer } = specialScopeRecognition[networkLike]?.[layerLike] ?? { + network: networkLike as Network, + layer: layerLike as Layer, + } + + if (!RouteUtils.getEnabledNetworks().includes(network as Network)) { throw new AppError(AppErrors.InvalidUrl) } if ( - !layer || // missing param !RouteUtils.getAllLayersForNetwork(network as Network).enabled.includes(layer as Layer) // unsupported on network ) { throw new AppError(AppErrors.UnsupportedLayer) diff --git a/src/config.ts b/src/config.ts index c14207c66..92ad4e2bb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,6 +3,8 @@ import { Layer } from './oasis-nexus/generated/api' import { NativeToken, NativeTokenInfo } from './types/ticker' import { SearchScope } from './types/searchScope' +import { Network } from './types/network' +import type { Theme } from '@mui/material/styles/createTheme' export const consensusDecimals = 9 /** @@ -202,3 +204,18 @@ export const getFiatCurrencyForScope = (scope: SearchScope | undefined) => (scope ? paraTimesConfig[scope.layer]?.[scope.network]?.fiatCurrency : undefined) ?? 'usd' export const showFiatValues = process.env.REACT_APP_SHOW_FIAT_VALUES === 'true' + +export const specialScopeNames: Partial>>> = { + [Network.mainnet]: {}, + [Network.testnet]: {}, +} + +export const specialScopePaths: Partial>>> = { + [Network.mainnet]: {}, + [Network.testnet]: {}, +} + +export const specialScopeThemes: Partial>>> = { + [Network.mainnet]: {}, + [Network.testnet]: {}, +} diff --git a/src/routes.tsx b/src/routes.tsx index f183bd10a..6125f6628 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -25,9 +25,10 @@ import { fixedNetwork, fixedLayer, RouteUtils, + skipGraph, } from './app/utils/route-utils' import { RoutingErrorPage } from './app/pages/RoutingErrorPage' -import { ThemeByNetwork, withDefaultTheme } from './app/components/ThemeByNetwork' +import { ThemeByScope, withDefaultTheme } from './app/components/ThemeByScope' import { useRequiredScopeParam } from './app/hooks/useScopeParam' import { TokensPage } from './app/pages/TokensOverviewPage' import { AccountEventsCard } from 'app/pages/RuntimeAccountDetailsPage/AccountEventsCard' @@ -65,14 +66,17 @@ import { ConsensusAccountTransactionsCard } from './app/pages/ConsensusAccountDe import { FC, useEffect } from 'react' import { AnalyticsConsentProvider } from './app/components/AnalyticsConsent' -const NetworkSpecificPart = () => ( - - - -) +const ScopeSpecificPart = () => { + const { network, layer } = useRequiredScopeParam() + return ( + + + + ) +} /** - * In case of being restricted to a specific layer, jump to a dashboard + * In case of being restricted to a specific layer or layers, jump to a dashboard * * This should be rendered on the landing page, since we don't want the opening graph. */ @@ -82,8 +86,11 @@ const RedirectToDashboard: FC = () => { useEffect(() => navigate( RouteUtils.getDashboardRoute({ - network: fixedNetwork ?? RouteUtils.getEnabledNetworksForLayer(fixedLayer!)[0]!, - layer: fixedLayer!, + network: + fixedNetwork ?? fixedLayer + ? RouteUtils.getEnabledNetworksForLayer(fixedLayer)[0]! + : RouteUtils.getEnabledScopes()[0].network, + layer: fixedLayer ?? RouteUtils.getEnabledScopes()[0].layer, }), ), ) @@ -106,7 +113,7 @@ export const routes: RouteObject[] = [ children: [ { path: '/', - element: fixedLayer ? : withDefaultTheme(, true), + element: skipGraph ? : withDefaultTheme(, true), }, ...(!!fixedNetwork && !!fixedLayer ? [] @@ -118,7 +125,7 @@ export const routes: RouteObject[] = [ ]), { path: '/:_network/consensus', - element: , + element: , errorElement: , loader: async ({ params }): Promise => { return assertEnabledScope({ network: params._network, layer: Layer.consensus }) @@ -216,7 +223,7 @@ export const routes: RouteObject[] = [ }, { path: '/:_network/:_layer', - element: , + element: , errorElement: , loader: async ({ params }): Promise => { return assertEnabledScope({ network: params._network, layer: params._layer }) diff --git a/src/styles/theme/index.ts b/src/styles/theme/index.ts index ff2cec728..aabd697fd 100644 --- a/src/styles/theme/index.ts +++ b/src/styles/theme/index.ts @@ -2,6 +2,8 @@ import { Network } from '../../types/network' import { defaultTheme } from './defaultTheme' import { testnetTheme } from './testnet/theme' import type { Theme } from '@mui/material/styles/createTheme' +import { Layer } from '../../oasis-nexus/api' +import { specialScopeThemes } from '../../config' export { defaultTheme } from './defaultTheme' export { testnetTheme } from './testnet/theme' @@ -9,7 +11,14 @@ export { testnetTheme } from './testnet/theme' export const tooltipDelay = 500 export const typingDelay = 1000 -export const getThemesForNetworks: () => Record = () => ({ - [Network.mainnet]: defaultTheme, - [Network.testnet]: testnetTheme, -}) +export const getThemeForScope = (network: Network, layer?: Layer): Theme => { + const specialTheme = layer ? specialScopeThemes[network]?.[layer] : undefined + if (specialTheme) return specialTheme + + switch (network) { + case Network.mainnet: + return defaultTheme + case Network.testnet: + return testnetTheme + } +} diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 42bdd6740..ce3fbc22b 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -28,6 +28,7 @@ declare global { REACT_APP_STAGING_URLS?: string REACT_APP_FIXED_NETWORK?: string REACT_APP_FIXED_LAYER?: string + REACT_APP_SKIP_GRAPH?: string REACT_APP_SHOW_FIAT_VALUES: 'true' | 'false' } } diff --git a/src/types/searchScope.ts b/src/types/searchScope.ts index 74a8a5888..035d2bfcd 100644 --- a/src/types/searchScope.ts +++ b/src/types/searchScope.ts @@ -2,6 +2,7 @@ import { getNetworkNames, Network } from './network' import { getLayerLabels } from '../app/utils/content' import { HasScope, Layer } from '../oasis-nexus/api' import { TFunction } from 'i18next' +import { specialScopeNames } from '../config' export interface SearchScope { network: Network @@ -14,6 +15,7 @@ export const MainnetEmerald: SearchScope = { } export const getNameForScope = (t: TFunction, scope: SearchScope) => + specialScopeNames[scope.network]?.[scope.layer] ?? `${getLayerLabels(t)[scope.layer]} ${getNetworkNames(t)[scope.network]}` export const getKeyForScope: (scope: SearchScope) => string = ({ network, layer }) => `${network}.${layer}`