diff --git a/.changelog/1242.feature.md b/.changelog/1242.feature.md new file mode 100644 index 0000000000..d8b6cc7a2b --- /dev/null +++ b/.changelog/1242.feature.md @@ -0,0 +1 @@ +Support limiting scope to one network or layer diff --git a/.env b/.env index 4c551766eb..57ada427b6 100644 --- a/.env +++ b/.env @@ -23,3 +23,5 @@ REACT_APP_SOCIAL_REDDIT=https://www.reddit.com/r/oasisnetwork/ REACT_APP_PRODUCTION_URLS=https://explorer.oasis.io, https://explorer.prd.oasis.io 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 diff --git a/.env.production b/.env.production index f40ed05247..99b926670c 100644 --- a/.env.production +++ b/.env.production @@ -18,3 +18,5 @@ REACT_APP_SOCIAL_REDDIT=https://www.reddit.com/r/oasisnetwork/ REACT_APP_PRODUCTION_URLS=https://explorer.oasis.io, https://explorer.prd.oasis.io 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 diff --git a/src/app/components/LayerPicker/LayerMenu.tsx b/src/app/components/LayerPicker/LayerMenu.tsx index a8420a0368..5c38810a79 100644 --- a/src/app/components/LayerPicker/LayerMenu.tsx +++ b/src/app/components/LayerPicker/LayerMenu.tsx @@ -13,6 +13,7 @@ import { RouteUtils } from '../../utils/route-utils' import { Network } from '../../../types/network' import { isLayerHidden, orderByLayer } from '../../../types/layers' import { useScreenSize } from '../../hooks/useScreensize' +import { useScopeParam } from '../../hooks/useScopeParam' type BaseLayerMenuItemProps = { divider: boolean @@ -107,9 +108,11 @@ export const LayerMenu: FC = ({ selectedNetwork, setSelectedLayer, }) => { + const currentScope = useScopeParam() const [hoveredLayer, setHoveredLayer] = useState() const options = Object.values(Layer) - .filter(layer => !isLayerHidden(layer)) + // Don't show hidden layers, unless we are already viewing them. + .filter(layer => !isLayerHidden(layer) || layer === currentScope?.layer) .map(layer => ({ layer, enabled: RouteUtils.getEnabledLayersForNetwork(selectedNetwork || network).includes(layer), diff --git a/src/app/components/LayerPicker/index.tsx b/src/app/components/LayerPicker/index.tsx index 38a96360ab..ae675086ea 100644 --- a/src/app/components/LayerPicker/index.tsx +++ b/src/app/components/LayerPicker/index.tsx @@ -13,7 +13,7 @@ import { useRequiredScopeParam } from '../../hooks/useScopeParam' import { NetworkMenu } from './NetworkMenu' import { LayerMenu } from './LayerMenu' import { LayerDetails } from './LayerDetails' -import { RouteUtils } from '../../utils/route-utils' +import { scopeFreedom, RouteUtils } from '../../utils/route-utils' import { styled } from '@mui/material/styles' import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft' import { useScreenSize } from '../../hooks/useScreensize' @@ -104,28 +104,33 @@ const LayerPickerContent: FC = ({ isOutOfDate, onClose, {isTablet && (
- {tabletStep === LayerPickerTabletStep.Layer && ( - } - onClick={() => { - setTabletStep(LayerPickerTabletStep.Network) - }} - > - {t('layerPicker.viewNetworks')} - - )} - {tabletStep === LayerPickerTabletStep.LayerDetails && ( - } - onClick={() => { - setTabletStep(LayerPickerTabletStep.Layer) - }} - > - {t('layerPicker.viewLayers')} - - )} + { + // Do we need a "back to networks" button ? + ((scopeFreedom === 'network-layer' && tabletStep === LayerPickerTabletStep.Layer) || // Stepping back from layers + (scopeFreedom === 'network' && tabletStep === LayerPickerTabletStep.LayerDetails)) && ( // Stepping back from details, skipping layers + } + onClick={() => { + setTabletStep(LayerPickerTabletStep.Network) + }} + > + {t('layerPicker.viewNetworks')} + + ) + } + {scopeFreedom !== 'network' && + tabletStep === LayerPickerTabletStep.LayerDetails && ( // Stepping back from details, going to layers + } + onClick={() => { + setTabletStep(LayerPickerTabletStep.Layer) + }} + > + {t('layerPicker.viewLayers')} + + )}
@@ -133,32 +138,38 @@ const LayerPickerContent: FC = ({ isOutOfDate, onClose, - {(!isTablet || (isTablet && tabletStep === LayerPickerTabletStep.Network)) && ( - - { - selectNetwork(network) - setTabletStep(LayerPickerTabletStep.Layer) - }} - /> - - )} - {(!isTablet || (isTablet && tabletStep === LayerPickerTabletStep.Layer)) && ( - - { - setSelectedLayer(layer) - setTabletStep(LayerPickerTabletStep.LayerDetails) - }} - /> - - )} + {scopeFreedom !== 'layer' && + (!isTablet || (isTablet && tabletStep === LayerPickerTabletStep.Network)) && ( + + { + selectNetwork(network) + setTabletStep( + scopeFreedom === 'network' // Are we fixed to a specific layer, selecting only network? + ? LayerPickerTabletStep.LayerDetails // If so, skip layer selection, go straight to layer details. + : LayerPickerTabletStep.Layer, // Otherwise, go to layer selection. + ) + }} + /> + + )} + {scopeFreedom !== 'network' && + (!isTablet || (isTablet && tabletStep === LayerPickerTabletStep.Layer)) && ( + + { + setSelectedLayer(layer) + setTabletStep(LayerPickerTabletStep.LayerDetails) + }} + /> + + )} {(!isTablet || (isTablet && tabletStep === LayerPickerTabletStep.LayerDetails)) && ( { const theme = useTheme() @@ -49,7 +50,7 @@ export const Header: FC = () => { showText={!scrollTrigger && !isMobile} /> - {scope && ( + {scope && isScopeSelectorNeeded(scope) && ( <> diff --git a/src/app/components/PageLayout/NetworkButton.tsx b/src/app/components/PageLayout/NetworkButton.tsx index 79dc638293..e354054167 100644 --- a/src/app/components/PageLayout/NetworkButton.tsx +++ b/src/app/components/PageLayout/NetworkButton.tsx @@ -5,10 +5,12 @@ import Button, { buttonClasses } from '@mui/material/Button' import EditIcon from '@mui/icons-material/Edit' import { styled } from '@mui/material/styles' import { COLORS } from '../../../styles/theme/colors' -import { Network } from '../../../types/network' +import { getNetworkNames, Network } from '../../../types/network' import { Layer } from '../../../oasis-nexus/api' import { getLayerLabels, getNetworkIcons } from '../../utils/content' import { LayerStatus } from '../LayerStatus' +import { fixedLayer } from '../../utils/route-utils' +import { TFunction } from 'i18next' export const StyledNetworkButton = styled(Button)(({ theme }) => ({ alignItems: 'center', @@ -75,9 +77,13 @@ type NetworkButtonProps = { onClick: () => void } +const getNetworkButtonLabel = (t: TFunction, network: Network, layer: Layer) => + fixedLayer // If we are fixed to a layer, + ? getNetworkNames(t)[network] // let's show the name of the network, + : getLayerLabels(t)[layer] // otherwise, the name of the layer. + export const NetworkButton: FC = ({ isOutOfDate, layer, network, onClick }) => { const { t } = useTranslation() - const labels = getLayerLabels(t) const icons = getNetworkIcons() return ( @@ -90,7 +96,7 @@ export const NetworkButton: FC = ({ isOutOfDate, layer, netw onClick={onClick} > - {labels[layer]} + {getNetworkButtonLabel(t, network, layer)} @@ -115,11 +121,10 @@ export const StyledMobileNetworkButton = styled(Button)(({ theme }) => ({ export const MobileNetworkButton: FC = ({ isOutOfDate, layer, network, onClick }) => { const { t } = useTranslation() - const labels = getLayerLabels(t) return ( - {labels[layer]} + {getNetworkButtonLabel(t, network, layer)} ) diff --git a/src/app/components/PageLayout/NetworkSelector.tsx b/src/app/components/PageLayout/NetworkSelector.tsx index de8c428abf..14a33a57ac 100644 --- a/src/app/components/PageLayout/NetworkSelector.tsx +++ b/src/app/components/PageLayout/NetworkSelector.tsx @@ -10,7 +10,7 @@ import { COLORS } from '../../../styles/theme/colors' import { Network, getNetworkNames } from '../../../types/network' import { Layer } from '../../../oasis-nexus/api' import { LayerPicker } from './../LayerPicker' -import { RouteUtils } from '../../utils/route-utils' +import { fixedLayer, RouteUtils } from '../../utils/route-utils' import { useConsensusFreshness, useRuntimeFreshness } from '../OfflineBanner/hook' export const StyledBox = styled(Box)(({ theme }) => ({ @@ -84,7 +84,7 @@ const NetworkSelectorView: FC = ({ isOutOfDate, layer, {!isMobile && ( )} - {!isTablet && network !== Network.mainnet && ( + {!fixedLayer && !isTablet && network !== Network.mainnet && ( = ({ network, @@ -14,6 +15,10 @@ export const ThemeByNetwork: FC<{ network: Network; children: React.ReactNode }> ) -export const withDefaultTheme = (node: ReactNode) => ( +export const withMainnetTheme = (node: ReactNode) => ( {node} ) + +export const withDefaultTheme = (node: ReactNode) => ( + {node} +) diff --git a/src/app/hooks/useSearchQueryNetworkParam.ts b/src/app/hooks/useSearchQueryNetworkParam.ts index ffa28e3586..27a94b81f8 100644 --- a/src/app/hooks/useSearchQueryNetworkParam.ts +++ b/src/app/hooks/useSearchQueryNetworkParam.ts @@ -1,6 +1,6 @@ import { useSearchParams } from 'react-router-dom' import { Network } from '../../types/network' -import { RouteUtils } from '../utils/route-utils' +import { fixedNetwork, RouteUtils } from '../utils/route-utils' import { AppErrors } from '../../types/errors' /** @@ -12,6 +12,12 @@ export const useSearchQueryNetworkParam = (): { setNetwork: (network: Network) => void } => { const [searchParams, setSearchParams] = useSearchParams() + if (fixedNetwork) { + return { + network: fixedNetwork, + setNetwork: () => {}, + } + } const networkQueryParam = searchParams.get('network') ?? Network.mainnet if (!RouteUtils.getEnabledNetworks().includes(networkQueryParam as any)) { throw AppErrors.InvalidUrl diff --git a/src/app/pages/HomePage/Graph/NetworkSelector/index.tsx b/src/app/pages/HomePage/Graph/NetworkSelector/index.tsx index 14d31b6bd9..003e79ec5c 100644 --- a/src/app/pages/HomePage/Graph/NetworkSelector/index.tsx +++ b/src/app/pages/HomePage/Graph/NetworkSelector/index.tsx @@ -18,6 +18,7 @@ import ExpandLessIcon from '@mui/icons-material/ExpandLess' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { getNetworkIcons } from '../../../../utils/content' import { optionClasses } from '@mui/base/Option' +import { Layer } from '../../../../../oasis-nexus/api' interface NetworkOption extends SelectOptionBase { label: Network @@ -119,12 +120,17 @@ const NetworkSelectorButton = forwardRef( interface NetworkSelectProps { network: Network setNetwork: (network: Network | null) => void + forLayer?: Layer } -export const NetworkSelector: FC = ({ network, setNetwork }) => { +export const NetworkSelector: FC = ({ network, setNetwork, forLayer }) => { const { t } = useTranslation() - const options = RouteUtils.getEnabledNetworks().map(network => ({ + const networks = RouteUtils.getEnabledNetworksForLayer(forLayer) + + if (networks.length === 1) return null + + const options = networks.map(network => ({ label: network, value: network, })) diff --git a/src/app/pages/HomePage/Graph/ParaTimeSelector/index.tsx b/src/app/pages/HomePage/Graph/ParaTimeSelector/index.tsx index 3e1daaed8e..7edb482780 100644 --- a/src/app/pages/HomePage/Graph/ParaTimeSelector/index.tsx +++ b/src/app/pages/HomePage/Graph/ParaTimeSelector/index.tsx @@ -24,6 +24,7 @@ import { useSearchQueryNetworkParam } from '../../../../hooks/useSearchQueryNetw import { storage } from '../../../../utils/storage' import { StorageKeys } from '../../../../../types/storage' import { GraphTooltipMobile } from '../GraphTooltipMobile' +import { fixedNetwork } from '../../../../utils/route-utils' interface ParaTimeSelectorBaseProps { disabled: boolean @@ -284,7 +285,7 @@ const ParaTimeSelectorCmp: FC = ({ )} - {step === ParaTimeSelectorStep.Explore && ( + {!fixedNetwork && step === ParaTimeSelectorStep.Explore && ( setNetwork(network ?? Network.mainnet)} /> )} diff --git a/src/app/pages/SearchResultsPage/GlobalSearchResultsView.tsx b/src/app/pages/SearchResultsPage/GlobalSearchResultsView.tsx index 223480793b..defe110fe4 100644 --- a/src/app/pages/SearchResultsPage/GlobalSearchResultsView.tsx +++ b/src/app/pages/SearchResultsPage/GlobalSearchResultsView.tsx @@ -1,7 +1,7 @@ import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' -import { RouteUtils } from '../../utils/route-utils' +import { fixedNetwork, RouteUtils } from '../../utils/route-utils' import { SearchResults } from './hooks' import { NoResultsOnMainnet, NoResultsWhatsoever } from './NoResults' import { SearchResultsList } from './SearchResultsList' @@ -30,6 +30,23 @@ export const GlobalSearchResultsView: FC<{ const themes = getThemesForNetworks() const networkNames = getNetworkNames(t) + + if (fixedNetwork) { + return ( + <> + {!searchResults.length && } + + + ) + } + const otherNetworks = RouteUtils.getEnabledNetworks().filter(isNotMainnet) const notificationTheme = themes[Network.testnet] const mainnetResults = searchResults.filter(isOnMainnet).sort(orderByLayer) diff --git a/src/app/pages/SearchResultsPage/hooks.ts b/src/app/pages/SearchResultsPage/hooks.ts index 2e029472df..6da6903d9f 100644 --- a/src/app/pages/SearchResultsPage/hooks.ts +++ b/src/app/pages/SearchResultsPage/hooks.ts @@ -174,12 +174,10 @@ export function useRuntimeTokenConditionally( export function useNetworkProposalsConditionally( nameFragment: string | undefined, ): ConditionalResults { - const queries = RouteUtils.getEnabledNetworks() - .filter(network => RouteUtils.getEnabledLayersForNetwork(network).includes(Layer.consensus)) - .map(network => - // eslint-disable-next-line react-hooks/rules-of-hooks - useGetConsensusProposalsByName(network, nameFragment), - ) + const queries = RouteUtils.getEnabledNetworksForLayer(Layer.consensus).map(network => + // eslint-disable-next-line react-hooks/rules-of-hooks + useGetConsensusProposalsByName(network, nameFragment), + ) return { isLoading: queries.some(query => query.isInitialLoading), results: queries diff --git a/src/app/utils/env.ts b/src/app/utils/env.ts new file mode 100644 index 0000000000..5d5a81c8d1 --- /dev/null +++ b/src/app/utils/env.ts @@ -0,0 +1 @@ +export const isTesting = process.env.NODE_ENV === 'test' diff --git a/src/app/utils/route-utils.ts b/src/app/utils/route-utils.ts index b8f1916c1e..13d0029cb1 100644 --- a/src/app/utils/route-utils.ts +++ b/src/app/utils/route-utils.ts @@ -7,6 +7,33 @@ import { Network } from '../../types/network' import { SearchScope } from '../../types/searchScope' import { isStableDeploy } from '../../config' import { getSearchTermFromRequest } from '../components/Search/search-utils' +import { isLayerHidden } from '../../types/layers' +import { isTesting } from './env' + +export const fixedNetwork = isTesting + ? undefined + : (process.env.REACT_APP_FIXED_NETWORK as Network | undefined) +export const fixedLayer = isTesting ? undefined : (process.env.REACT_APP_FIXED_LAYER as Layer | undefined) + +export type ScopeFreedom = + | 'network' // We can select only the network + | 'layer' // We can select only the layer + | 'network-layer' // We can select both network and layer + | 'none' // We can't select anything, everything is fixed + +export const scopeFreedom: ScopeFreedom = fixedNetwork + ? fixedLayer + ? 'none' + : 'layer' + : fixedLayer + ? 'network' + : 'network-layer' + +export const hasFixedNetworkAndLayer = !!fixedNetwork && !!fixedLayer + +export const isFixedOnConsensus = fixedLayer === Layer.consensus + +export const isFixedOnParatime = !!fixedLayer && fixedLayer !== Layer.consensus export type SpecifiedPerEnabledLayer = { [N in keyof (typeof RouteUtils)['ENABLED_LAYERS_FOR_NETWORK']]: { @@ -17,8 +44,21 @@ export type SpecifiedPerEnabledLayer = { } } +export const networkRoutePath = fixedNetwork ? '' : `/:_network` + +export const layerRoutePath = fixedLayer ? '' : `/:_layer` + +export const consensusRoutePath = isFixedOnConsensus ? '' : '/consensus' + export type SpecifiedPerEnabledRuntime = SpecifiedPerEnabledLayer +const getNetworkPath = (network: Network) => (fixedNetwork ? '' : `/${encodeURIComponent(network)}`) + +const getLayerPath = (layer: Layer) => (fixedLayer ? '' : `/${encodeURIComponent(layer)}`) + +const getNetworkAndLayerPath = (network: Network, layer: Layer) => + `${getNetworkPath(network)}${getLayerPath(layer)}` + export abstract class RouteUtils { private static ENABLED_LAYERS_FOR_NETWORK = { [Network.mainnet]: { @@ -39,45 +79,39 @@ export abstract class RouteUtils { } satisfies Record> static getDashboardRoute = ({ network, layer }: SearchScope) => { - return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}` + return `${getNetworkAndLayerPath(network, layer)}` } static getLatestTransactionsRoute = ({ network, layer }: SearchScope) => { - return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/tx` + return `${getNetworkAndLayerPath(network, layer)}/tx` } static getTopTokensRoute = ({ network, layer }: SearchScope) => { - return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/token` + return `${getNetworkAndLayerPath(network, layer)}/token` } static getLatestBlocksRoute = ({ network, layer }: SearchScope) => { - return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/block` + return `${getNetworkAndLayerPath(network, layer)}/block` } static getBlockRoute = ({ network, layer }: SearchScope, blockHeight: number) => { - return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/block/${encodeURIComponent( - blockHeight, - )}` + return `${getNetworkAndLayerPath(network, layer)}/block/${encodeURIComponent(blockHeight)}` } static getTransactionRoute = (scope: SearchScope, txHash: string) => { - return `/${encodeURIComponent(scope.network)}/${encodeURIComponent(scope.layer)}/tx/${encodeURIComponent( - txHash, - )}` + return `${getNetworkAndLayerPath(scope.network, scope.layer)}/tx/${encodeURIComponent(txHash)}` } static getAccountRoute = ({ network, layer }: SearchScope, account: string) => { - return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/address/${encodeURIComponent( - account, - )}` + return `${getNetworkAndLayerPath(network, layer)}/address/${encodeURIComponent(account)}` } static getAccountsRoute = (network: Network) => { - return `/${encodeURIComponent(network)}/consensus/address` + return `${getNetworkPath(network)}${consensusRoutePath}/address` } static getValidatorsRoute = (network: Network) => { - return `/${encodeURIComponent(network)}/consensus/validators` + return `${getNetworkPath(network)}${consensusRoutePath}/validators` } static getAccountTokensRoute = ( @@ -97,20 +131,16 @@ export abstract class RouteUtils { static getSearchRoute = (scope: SearchScope | undefined, searchTerm: string) => { return scope - ? `/${scope.network}/${scope.layer}/search?q=${encodeURIComponent(searchTerm)}` + ? `${getNetworkAndLayerPath(scope.network, scope.layer)}/search?q=${encodeURIComponent(searchTerm)}` : `/search?q=${encodeURIComponent(searchTerm)}` } static getTokenRoute = ({ network, layer }: SearchScope, tokenAddress: string) => { - return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/token/${encodeURIComponent( - tokenAddress, - )}` + return `${getNetworkAndLayerPath(network, layer)}/token/${encodeURIComponent(tokenAddress)}` } static getTokenHoldersRoute = ({ network, layer }: SearchScope, tokenAddress: string) => { - return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/token/${encodeURIComponent( - tokenAddress, - )}/holders` + return `${getNetworkAndLayerPath(network, layer)}/token/${encodeURIComponent(tokenAddress)}/holders` } static getNFTInstanceRoute = ( @@ -118,38 +148,62 @@ export abstract class RouteUtils { contractAddress: string, instanceId: string, ): string => - `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/token/${encodeURIComponent( + `${getNetworkAndLayerPath(network, layer)}/token/${encodeURIComponent( contractAddress, )}/instance/${encodeURIComponent(instanceId)}` static getEnabledLayersForNetwork(network: Network): Layer[] { - return Object.values(Layer).filter(layer => RouteUtils.ENABLED_LAYERS_FOR_NETWORK[network][layer]) + return (fixedLayer ? [fixedLayer] : Object.values(Layer)).filter( + layer => this.ENABLED_LAYERS_FOR_NETWORK[network][layer], + ) + } + + static getVisibleLayersForNetwork(network: Network, currentScope: SearchScope | undefined): Layer[] { + return this.getEnabledLayersForNetwork(network).filter( + layer => !isLayerHidden(layer) || layer === currentScope?.layer, + ) } static getProposalsRoute = (network: Network) => { - return `/${encodeURIComponent(network)}/consensus/proposal` + return `${getNetworkPath(network)}/consensus/proposal` } static getProposalRoute = (network: Network, proposalId: string | number) => { - return `/${encodeURIComponent(network)}/consensus/proposal/${encodeURIComponent(proposalId)}` + return `${getNetworkPath(network)}/consensus/proposal/${encodeURIComponent(proposalId)}` } static getEnabledScopes(): SearchScope[] { - return RouteUtils.getEnabledNetworks().flatMap(network => - RouteUtils.getEnabledLayersForNetwork(network).map(layer => ({ network, layer })), + return this.getEnabledNetworks().flatMap(network => + this.getEnabledLayersForNetwork(network).map(layer => ({ network, layer })), ) } + /** + * Get the list of enabled networks. + * + * If this Explorer is fixed to a specific network, the only that network will be returned. + * Furthermore, if this Explorer is fixed to a specific layer, only networks that support that layer are returned. + */ static getEnabledNetworks(): Network[] { - return Object.values(Network).filter(network => { - return RouteUtils.getEnabledLayersForNetwork(network).length > 0 - }) + const networks = fixedNetwork + ? [fixedNetwork] + : Object.values(Network).filter(network => { + return this.getEnabledLayersForNetwork(network).length > 0 + }) + return fixedLayer + ? networks.filter(network => { + return this.getEnabledLayersForNetwork(network).includes(fixedLayer) + }) + : networks } - static getEnabledSearchScopes(): SearchScope[] { - return RouteUtils.getEnabledNetworks().flatMap(network => - RouteUtils.getEnabledLayersForNetwork(network).map(layer => ({ network, layer })), - ) + static getEnabledNetworksForLayer(layer: Layer | undefined): Network[] { + const networks = this.getEnabledNetworks() + return layer + ? networks.filter(network => { + return this.getEnabledLayersForNetwork(network).includes(layer) + }) + : networks } } @@ -243,3 +297,19 @@ export const proposalIdParamLoader = async ({ params, request }: LoaderFunctionA searchTerm: getSearchTermFromRequest(request), } } + +/** + * Is it possible to change the scope, given the current configuration? + */ +export const isScopeSelectorNeeded = (sourceScope: SearchScope | undefined) => { + switch (scopeFreedom) { + case 'network': + return RouteUtils.getEnabledNetworks().length > 1 + case 'layer': + return RouteUtils.getVisibleLayersForNetwork(fixedNetwork!, sourceScope).length > 1 + case 'network-layer': + return RouteUtils.getEnabledScopes().length > 1 + case 'none': + return false + } +} diff --git a/src/global.d.ts b/src/global.d.ts index dc359c1039..eb21230755 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -23,6 +23,8 @@ declare global { REACT_APP_SOCIAL_HOME?: string REACT_APP_PRODUCTION_URLS: string REACT_APP_STAGING_URLS?: string + REACT_APP_FIXED_NETWORK?: string + REACT_APP_FIXED_LAYER?: string } } } diff --git a/src/routes.tsx b/src/routes.tsx index 529136d9d9..d1f1646c89 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,4 +1,4 @@ -import { Outlet, RouteObject, ScrollRestoration } from 'react-router-dom' +import { Outlet, RouteObject, ScrollRestoration, useNavigate } from 'react-router-dom' import { HomePage } from './app/pages/HomePage' import { RuntimeBlocksPage } from './app/pages/RuntimeBlocksPage' import { RuntimeTransactionsPage } from './app/pages/RuntimeTransactionsPage' @@ -15,10 +15,19 @@ import { transactionParamLoader, assertEnabledScope, proposalIdParamLoader, + fixedNetwork, + hasFixedNetworkAndLayer, + fixedLayer, + isFixedOnParatime, + isFixedOnConsensus, + networkRoutePath, + consensusRoutePath, + layerRoutePath, + RouteUtils, } from './app/utils/route-utils' import { searchParamLoader } from './app/components/Search/search-utils' import { RoutingErrorPage } from './app/pages/RoutingErrorPage' -import { ThemeByNetwork, withDefaultTheme } from './app/components/ThemeByNetwork' +import { ThemeByNetwork, withDefaultTheme, withMainnetTheme } from './app/components/ThemeByNetwork' import { useRequiredScopeParam } from './app/hooks/useScopeParam' import { TokensPage } from './app/pages/TokensOverviewPage' import { ContractCodeCard } from './app/pages/AccountDetailsPage/ContractCodeCard' @@ -41,6 +50,7 @@ import { ProposalDetailsPage } from './app/pages/ProposalDetailsPage' import { ConsensusBlocksPage } from './app/pages/ConsensusBlocksPage' import { ConsensusAccountsPage } from './app/pages/ConsensusAccountsPage' import { ConsensusTransactionsPage } from './app/pages/ConsensusTransactionsPage' +import { FC, useEffect } from 'react' const NetworkSpecificPart = () => ( @@ -48,6 +58,69 @@ const NetworkSpecificPart = () => ( ) +/** + * Use the passed RouteObject unless we have a fixed layer. + * + * Normally, this will just wrap the RouteObject into an array. + * Unless the condition is true, because in that case, and empty array will be returned. + */ +const unlessFixedLayer = (route: RouteObject): RouteObject[] => (fixedLayer ? [] : [route]) + +/** + * Use the passed RouteObject unless we have a fixed network and layer. + * + * Normally, this will just wrap the RouteObject into an array. + * Unless the condition is true, because in that case, and empty array will be returned. + */ +const unlessFixedNetworkAndLayer = (route: RouteObject): RouteObject[] => + hasFixedNetworkAndLayer ? [] : [route] + +/** + * Use the passed RouteObject unless we are fixed on a ParaTime. + * + * Normally, this will just wrap the RouteObject into an array. + * Unless the condition is true, because in that case, and empty array will be returned. + */ +const unlessFixedOnParatime = (route: RouteObject): RouteObject[] => (isFixedOnParatime ? [] : [route]) + +/** + * Use the passed RouteObject unless we are fixed on consensus. + * + * Normally, this will just wrap the RouteObject into an array. + * Unless the condition is true, because in that case, and empty array will be returned. + */ +const unlessFixedOnConsensus = (route: RouteObject): RouteObject[] => (isFixedOnConsensus ? [] : [route]) + +/** + * Use the passed RouteObject if we have a fixed layer but no network + * + * Normally, this will just wrap the RouteObject into an array. + * Unless the condition is true, because in that case, and empty array will be returned. + */ +const whenFixedLayerButNoNetwork = (route: RouteObject): RouteObject[] => + !!fixedLayer && !fixedNetwork ? [route] : [] + +/** + * In case of being restricted to a specific layer, jump to a network + * + * This should be rendered on the landing page, + * in order to redirect to a network, since we don't want the + * opening graph. + */ +const RedirectToNetwork: FC = () => { + const navigate = useNavigate() + + useEffect(() => + navigate( + RouteUtils.getDashboardRoute({ + network: RouteUtils.getEnabledNetworksForLayer(fixedLayer!)[0]!, + layer: fixedLayer!, + }), + ), + ) + return null +} + export const routes: RouteObject[] = [ { errorElement: , @@ -58,21 +131,28 @@ export const routes: RouteObject[] = [ ), children: [ - { + ...unlessFixedLayer({ + path: '/', + element: withMainnetTheme(), + }), + ...whenFixedLayerButNoNetwork({ path: '/', - element: withDefaultTheme(), - }, - { + element: , + }), + ...unlessFixedNetworkAndLayer({ path: '/search', // Global search element: withDefaultTheme(), loader: searchParamLoader, - }, - { - path: '/:_network/consensus', + }), + ...unlessFixedOnParatime({ + path: `${networkRoutePath}${consensusRoutePath}`, element: , errorElement: , loader: async ({ params }): Promise => { - return assertEnabledScope({ network: params._network, layer: Layer.consensus }) + return assertEnabledScope({ + network: fixedNetwork ?? params._network, + layer: Layer.consensus, + }) }, id: 'consensusScope', children: [ @@ -111,13 +191,16 @@ export const routes: RouteObject[] = [ element: , }, ], - }, - { - path: '/:_network/:_layer', + }), + ...unlessFixedOnConsensus({ + path: `${networkRoutePath}${layerRoutePath}`, element: , errorElement: , loader: async ({ params }): Promise => { - return assertEnabledScope({ network: params._network, layer: params._layer }) + return assertEnabledScope({ + network: fixedNetwork ?? params._network, + layer: fixedLayer ?? params._layer, + }) }, id: 'runtimeScope', children: [ @@ -229,7 +312,7 @@ export const routes: RouteObject[] = [ ], }, ], - }, + }), ], }, ]