diff --git a/.changelog/1688.trivial.md b/.changelog/1688.trivial.md new file mode 100644 index 000000000..62152304f --- /dev/null +++ b/.changelog/1688.trivial.md @@ -0,0 +1 @@ +Add support for merging all networks in the layer selector diff --git a/src/app/components/LayerPicker/LayerDetails.tsx b/src/app/components/LayerPicker/LayerDetails.tsx index 814ee5174..06cf7bb61 100644 --- a/src/app/components/LayerPicker/LayerDetails.tsx +++ b/src/app/components/LayerPicker/LayerDetails.tsx @@ -16,10 +16,11 @@ import { Link as RouterLink } from 'react-router-dom' import { docs } from '../../utils/externalLinks' import { TextList, TextListItem } from '../TextList' import { getLayerLabels, getNetworkIcons } from '../../utils/content' -import { getNameForScope } from '../../../types/searchScope' +import { getNameForScope, SearchScope } from '../../../types/searchScope' import { useConsensusFreshness, useRuntimeFreshness } from '../OfflineBanner/hook' import { LayerStatus } from '../LayerStatus' import { useScreenSize } from '../../hooks/useScreensize' +import { mergeNetworksInLayerSelector } from '../../utils/route-utils' type LayerDetailsContent = { description: string @@ -115,9 +116,7 @@ export const StyledButton = styled(Button)(({ theme }) => ({ type LayerDetailsProps = { handleConfirm: () => void - network: Network - selectedLayer: Layer - selectedNetwork: Network + selectedScope: SearchScope isOutOfDate: boolean | undefined } @@ -125,22 +124,24 @@ type LayerDetailsProps = { const contentMinHeight = '270px' export const LayerDetails: FC<LayerDetailsProps> = (props: LayerDetailsProps) => - props.selectedLayer === Layer.consensus ? <ConsensusDetails {...props} /> : <RuntimeDetails {...props} /> + props.selectedScope.layer === Layer.consensus ? ( + <ConsensusDetails {...props} /> + ) : ( + <RuntimeDetails {...props} /> + ) -const ConsensusDetails: FC<LayerDetailsProps & { selectedNetwork: Network }> = props => { +const ConsensusDetails: FC<LayerDetailsProps> = props => { const { t } = useTranslation() - const { handleConfirm, network, selectedLayer, selectedNetwork } = props - const isOutOfDate = useConsensusFreshness(network).outOfDate - const isLocal = selectedNetwork === 'localnet' + const { handleConfirm, selectedScope } = props + const isOutOfDate = useConsensusFreshness(selectedScope.network).outOfDate + const isLocal = selectedScope.network === 'localnet' return ( <LayerDetailsSection docsUrl={isLocal ? undefined : docs.consensus} handleConfirm={handleConfirm} isOutOfDate={isOutOfDate} - selectedLayer={selectedLayer} - selectedNetwork={selectedNetwork} - network={network} + selectedScope={selectedScope} > <Typography sx={{ fontSize: '14px', color: COLORS.brandExtraDark, pb: 4 }}> {t('layerPicker.consensus')} @@ -151,19 +152,25 @@ const ConsensusDetails: FC<LayerDetailsProps & { selectedNetwork: Network }> = p const RuntimeDetails: FC<LayerDetailsProps> = props => { const { t } = useTranslation() - const { handleConfirm, network, selectedLayer, selectedNetwork } = props - const isOutOfDate = useRuntimeFreshness({ network, layer: selectedLayer }).outOfDate - const details = getDetails(t)[network]?.[selectedLayer] + const { handleConfirm, selectedScope } = props + const isOutOfDate = useRuntimeFreshness(selectedScope).outOfDate + const details = getDetails(t)[selectedScope.network]?.[selectedScope.layer] + const networkNames = getNetworkNames(t) return ( <LayerDetailsSection docsUrl={details?.docs} handleConfirm={handleConfirm} isOutOfDate={isOutOfDate} - selectedLayer={selectedLayer} - selectedNetwork={selectedNetwork} - network={network} + selectedScope={selectedScope} > + {mergeNetworksInLayerSelector && ( + <Typography sx={{ fontSize: '14px', color: COLORS.brandExtraDark, pb: 4 }}> + {t('layerPicker.hostedOn', { + network: networkNames[selectedScope.network], + })} + </Typography> + )} {details?.description && ( <Typography sx={{ fontSize: '14px', color: COLORS.brandExtraDark, pb: 4 }}> {details.description} @@ -216,13 +223,12 @@ export const LayerDetailsSection: FC<LayerDetailsSectionProps> = ({ docsUrl, handleConfirm, isOutOfDate, - network, - selectedLayer, + selectedScope, }) => { const { t } = useTranslation() const theme = useTheme() const { isTablet } = useScreenSize() - const labels = getNetworkNames(t) + const networkNames = getNetworkNames(t) const layerLabels = getLayerLabels(t) const icons = getNetworkIcons() @@ -238,7 +244,7 @@ export const LayerDetailsSection: FC<LayerDetailsSectionProps> = ({ borderStyle: 'solid', }} > - {icons[network]} + {icons[selectedScope.network]} </Circle> </Box> <Box> @@ -250,7 +256,7 @@ export const LayerDetailsSection: FC<LayerDetailsSectionProps> = ({ }} > <StyledButton variant="text" onClick={handleConfirm}> - {getNameForScope(t, { network, layer: selectedLayer })} + {getNameForScope(t, selectedScope)} </StyledButton> <LayerStatus isOutOfDate={isOutOfDate} withTooltip /> </Box> @@ -270,8 +276,8 @@ export const LayerDetailsSection: FC<LayerDetailsSectionProps> = ({ }} > {t('layerPicker.readMore', { - layer: layerLabels[selectedLayer], - network: labels[network], + layer: layerLabels[selectedScope.layer], + network: networkNames[selectedScope.network], })} <OpenInNewIcon sx={{ fontSize: '16px' }} /> </Link> diff --git a/src/app/components/LayerPicker/LayerMenu.tsx b/src/app/components/LayerPicker/LayerMenu.tsx index 26bcb177f..1826bb89d 100644 --- a/src/app/components/LayerPicker/LayerMenu.tsx +++ b/src/app/components/LayerPicker/LayerMenu.tsx @@ -9,15 +9,16 @@ import Tooltip from '@mui/material/Tooltip' import { COLORS } from '../../../styles/theme/colors' import { Layer } from '../../../oasis-nexus/api' import { getLayerLabels } from '../../utils/content' -import { isScopeHidden, RouteUtils } from '../../utils/route-utils' +import { isScopeHidden, mergeNetworksInLayerSelector, RouteUtils } from '../../utils/route-utils' import { Network } from '../../../types/network' import { orderByLayer } from '../../../types/layers' import { useScreenSize } from '../../hooks/useScreensize' import { useScopeParam } from '../../hooks/useScopeParam' +import { SearchScope } from '../../../types/searchScope' type BaseLayerMenuItemProps = { divider: boolean - layer: Layer + targetScope: SearchScope } const LayerMenuItemCaption: FC<PropsWithChildren> = ({ children }) => ( @@ -29,7 +30,7 @@ const LayerMenuItemCaption: FC<PropsWithChildren> = ({ children }) => ( </Typography> ) -export const DisabledLayerMenuItem: FC<BaseLayerMenuItemProps> = ({ divider, layer }) => { +export const DisabledLayerMenuItem: FC<BaseLayerMenuItemProps> = ({ divider, targetScope }) => { const { isTablet } = useScreenSize() const { t } = useTranslation() const labels = getLayerLabels(t) @@ -40,7 +41,7 @@ export const DisabledLayerMenuItem: FC<BaseLayerMenuItemProps> = ({ divider, lay <div> <MenuItem disabled divider={divider}> <ListItemText> - {labels[layer]} + {labels[targetScope.layer]} {isTablet && <LayerMenuItemCaption>{t('layerPicker.comingSoonLabel')}</LayerMenuItemCaption>} </ListItemText> </MenuItem> @@ -51,78 +52,81 @@ export const DisabledLayerMenuItem: FC<BaseLayerMenuItemProps> = ({ divider, lay type LayerMenuItemProps = LayerMenuProps & BaseLayerMenuItemProps & { - hoveredLayer?: Layer - setHoveredLayer: (layer?: Layer) => void + hoveredScope?: SearchScope + setHoveredScope: (scope?: SearchScope) => void } export const LayerMenuItem: FC<LayerMenuItemProps> = ({ - activeLayer, divider, - layer, - network, - selectedLayer, - selectedNetwork, - setHoveredLayer, - setSelectedLayer, + targetScope, + selectedScope, + setHoveredScope, + setSelectedScope, }) => { const { t } = useTranslation() const labels = getLayerLabels(t) - const activeLayerSelection = layer === selectedLayer + const activeScope = useScopeParam()! + const isSelected = + targetScope.network === selectedScope?.network && targetScope.layer === selectedScope?.layer + const isActive = targetScope.network === activeScope.network && targetScope.layer === activeScope.layer return ( <MenuItem divider={divider} onMouseEnter={() => { - setHoveredLayer(layer) + setHoveredScope(targetScope) }} onMouseLeave={() => { - setHoveredLayer() + setHoveredScope() }} onClick={() => { - setSelectedLayer(layer) + setSelectedScope(targetScope) }} - selected={activeLayerSelection} - tabIndex={activeLayerSelection ? 0 : -1} + selected={isSelected} + tabIndex={isSelected ? 0 : -1} > <ListItemText> - {labels[layer]} - {selectedNetwork === network && activeLayer === layer && ( - <LayerMenuItemCaption>{t('layerPicker.selected')}</LayerMenuItemCaption> - )} + {labels[targetScope.layer]} + {isActive && <LayerMenuItemCaption>{t('layerPicker.active')}</LayerMenuItemCaption>} </ListItemText> - {layer === selectedLayer && <KeyboardArrowRightIcon />} + {isSelected && <KeyboardArrowRightIcon />} </MenuItem> ) } type LayerMenuProps = { - activeLayer: Layer - network: Network - selectedLayer?: Layer - selectedNetwork: Network - setSelectedLayer: (layer: Layer) => void + selectedNetwork?: Network + selectedScope?: SearchScope + setSelectedScope: (scope: SearchScope) => void } -export const LayerMenu: FC<LayerMenuProps> = ({ - activeLayer, - network, - selectedLayer, - selectedNetwork, - setSelectedLayer, -}) => { - const currentScope = useScopeParam() - const [hoveredLayer, setHoveredLayer] = useState<undefined | Layer>() - const options = Object.values(Layer) +type LayerMenuOption = { + scope: SearchScope + enabled: boolean +} + +const getOptionsForNetwork = (network: Network, activeScope?: SearchScope | undefined): LayerMenuOption[] => + Object.values(Layer) + .sort(orderByLayer) // Don't show hidden layers, unless we are already viewing them. .filter( layer => - !isScopeHidden({ network: selectedNetwork, layer }) || - (layer === currentScope?.layer && selectedNetwork === currentScope?.network), + !isScopeHidden({ network, layer }) || + (layer === activeScope?.layer && network === activeScope?.network), ) .map(layer => ({ - layer, - enabled: RouteUtils.getAllLayersForNetwork(selectedNetwork || network).enabled.includes(layer), + scope: { + network, + layer, + }, + enabled: RouteUtils.getAllLayersForNetwork(network).enabled.includes(layer), })) - .sort(orderByLayer) + +export const LayerMenu: FC<LayerMenuProps> = ({ selectedNetwork, selectedScope, setSelectedScope }) => { + const activeScope = useScopeParam()! + const [hoveredScope, setHoveredScope] = useState<undefined | SearchScope>() + const options = mergeNetworksInLayerSelector + ? RouteUtils.getVisibleScopes(activeScope).map(scope => ({ scope, enabled: true })) + : getOptionsForNetwork(selectedNetwork ?? activeScope.network, activeScope) return ( <MenuList> @@ -132,23 +136,21 @@ export const LayerMenu: FC<LayerMenuProps> = ({ return ( <DisabledLayerMenuItem divider={index !== options.length - 1} - key={option.layer} - layer={option.layer} + key={option.scope.network + option.scope.layer} + targetScope={option.scope} /> ) } else { return ( <LayerMenuItem - activeLayer={activeLayer} divider={index !== options.length - 1} - hoveredLayer={hoveredLayer} - key={option.layer} - layer={option.layer} - network={network} - selectedLayer={selectedLayer} + hoveredScope={hoveredScope} + key={option.scope.network + option.scope.layer} + targetScope={option.scope} + selectedScope={selectedScope} selectedNetwork={selectedNetwork} - setHoveredLayer={setHoveredLayer} - setSelectedLayer={setSelectedLayer} + setHoveredScope={setHoveredScope} + setSelectedScope={setSelectedScope} /> ) } diff --git a/src/app/components/LayerPicker/NetworkMenu.tsx b/src/app/components/LayerPicker/NetworkMenu.tsx index f0d02d120..b7b5d18e3 100644 --- a/src/app/components/LayerPicker/NetworkMenu.tsx +++ b/src/app/components/LayerPicker/NetworkMenu.tsx @@ -29,7 +29,8 @@ export const NetworkMenuItem: FC<NetworkMenuItemProps> = ({ const { t } = useTranslation() const labels = getNetworkNames(t) const icons = getNetworkIcons() - const activeNetworkSelection = network === selectedNetwork + const isSelected = network === selectedNetwork + const isActive = network === activeNetwork return ( <MenuItem @@ -40,8 +41,8 @@ export const NetworkMenuItem: FC<NetworkMenuItemProps> = ({ onMouseLeave={() => { setHoveredNetwork(undefined) }} - selected={activeNetworkSelection} - tabIndex={activeNetworkSelection ? 0 : -1} + selected={isSelected} + tabIndex={isSelected ? 0 : -1} onClick={() => { setSelectedNetwork(network) }} @@ -49,12 +50,12 @@ export const NetworkMenuItem: FC<NetworkMenuItemProps> = ({ <ListItemIcon>{icons[network]}</ListItemIcon> <ListItemText> {labels[network]} - {activeNetwork === network && ( + {isActive && ( <Typography component="span" sx={{ fontSize: '10px', fontStyle: 'italic', color: COLORS.grayMedium, ml: 2 }} > - {t('layerPicker.selected')} + {t('layerPicker.active')} </Typography> )} </ListItemText> diff --git a/src/app/components/LayerPicker/index.tsx b/src/app/components/LayerPicker/index.tsx index 22c30be93..2c48b9ddf 100644 --- a/src/app/components/LayerPicker/index.tsx +++ b/src/app/components/LayerPicker/index.tsx @@ -8,12 +8,12 @@ import Grid from '@mui/material/Unstable_Grid2' import { HomePageLink } from '../PageLayout/Logotype' import { COLORS } from '../../../styles/theme/colors' import { Network } from '../../../types/network' -import { Layer } from '../../../oasis-nexus/api' +import { SearchScope } from '../../../types/searchScope' import { useRequiredScopeParam } from '../../hooks/useScopeParam' import { NetworkMenu } from './NetworkMenu' import { LayerMenu } from './LayerMenu' import { LayerDetails } from './LayerDetails' -import { scopeFreedom, RouteUtils } from '../../utils/route-utils' +import { scopeFreedom, RouteUtils, mergeNetworksInLayerSelector } from '../../utils/route-utils' import { styled } from '@mui/material/styles' import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft' import { useScreenSize } from '../../hooks/useScreensize' @@ -21,7 +21,7 @@ import { MobileNetworkButton } from '../PageLayout/NetworkButton' type LayerPickerProps = { onClose: () => void - onConfirm: (network: Network, layer: Layer) => void + onConfirm: (scope: SearchScope) => void open: boolean isOutOfDate: boolean | undefined } @@ -84,17 +84,20 @@ enum LayerPickerTabletStep { const LayerPickerContent: FC<LayerPickerContentProps> = ({ isOutOfDate, onClose, onConfirm }) => { const { isMobile, isTablet } = useScreenSize() const { t } = useTranslation() - const { network, layer } = useRequiredScopeParam() - const [selectedLayer, setSelectedLayer] = useState<Layer>(layer) - const [selectedNetwork, setSelectedNetwork] = useState<Network>(network) + const activeScope = useRequiredScopeParam() + const [selectedScope, setSelectedScope] = useState<SearchScope>(activeScope) + const [selectedNetwork, setSelectedNetwork] = useState<Network>(activeScope.network) const [tabletStep, setTabletStep] = useState<LayerPickerTabletStep>(LayerPickerTabletStep.LayerDetails) const selectNetwork = (newNetwork: Network) => { const enabledLayers = RouteUtils.getAllLayersForNetwork(newNetwork).enabled - const targetLayer = enabledLayers.includes(selectedLayer) ? selectedLayer : enabledLayers[0] + const targetScope = { + network: newNetwork, + layer: enabledLayers.includes(selectedScope.layer) ? selectedScope.layer : enabledLayers[0], + } setSelectedNetwork(newNetwork) - setSelectedLayer(targetLayer) + setSelectedScope(targetScope) } - const handleConfirm = () => onConfirm(selectedNetwork, selectedLayer) + const handleConfirm = () => onConfirm(selectedScope) return ( <StyledLayerPickerContent> @@ -106,7 +109,9 @@ const LayerPickerContent: FC<LayerPickerContentProps> = ({ isOutOfDate, onClose, <div> { // Do we need a "back to networks" button ? - ((scopeFreedom === 'network-layer' && tabletStep === LayerPickerTabletStep.Layer) || // Stepping back from layers + ((scopeFreedom === 'network-layer' && + !mergeNetworksInLayerSelector && + tabletStep === LayerPickerTabletStep.Layer) || // Stepping back from layers (scopeFreedom === 'network' && tabletStep === LayerPickerTabletStep.LayerDetails)) && ( // Stepping back from details, skipping layers <TabletBackButton variant="text" @@ -132,17 +137,22 @@ const LayerPickerContent: FC<LayerPickerContentProps> = ({ isOutOfDate, onClose, </TabletBackButton> )} </div> - <MobileNetworkButton isOutOfDate={isOutOfDate} network={network} layer={layer} onClick={onClose} /> + <MobileNetworkButton + isOutOfDate={isOutOfDate} + network={activeScope.network} + layer={activeScope.layer} + onClick={onClose} + /> </TabletActionBar> )} <Divider /> <StyledContent> <Grid container> - {scopeFreedom !== 'layer' && + {!(scopeFreedom === 'layer' || mergeNetworksInLayerSelector) && (!isTablet || (isTablet && tabletStep === LayerPickerTabletStep.Network)) && ( <Grid xs={12} md={3}> <NetworkMenu - activeNetwork={network} + activeNetwork={activeScope.network} selectedNetwork={selectedNetwork} setSelectedNetwork={network => { selectNetwork(network) @@ -159,12 +169,10 @@ const LayerPickerContent: FC<LayerPickerContentProps> = ({ isOutOfDate, onClose, (!isTablet || (isTablet && tabletStep === LayerPickerTabletStep.Layer)) && ( <Grid xs={12} md={3}> <LayerMenu - activeLayer={layer} - network={network} - selectedLayer={selectedLayer} selectedNetwork={selectedNetwork} - setSelectedLayer={layer => { - setSelectedLayer(layer) + selectedScope={selectedScope} + setSelectedScope={scope => { + setSelectedScope(scope) setTabletStep(LayerPickerTabletStep.LayerDetails) }} /> @@ -174,9 +182,7 @@ const LayerPickerContent: FC<LayerPickerContentProps> = ({ isOutOfDate, onClose, <Grid xs={12} md={6}> <LayerDetails handleConfirm={handleConfirm} - selectedLayer={selectedLayer} - selectedNetwork={selectedNetwork} - network={selectedNetwork} + selectedScope={selectedScope} isOutOfDate={isOutOfDate} /> </Grid> @@ -189,7 +195,7 @@ const LayerPickerContent: FC<LayerPickerContentProps> = ({ isOutOfDate, onClose, </Button> <Button onClick={handleConfirm} color="primary" variant="contained" size="large"> - {selectedNetwork === network && selectedLayer === layer + {selectedScope.network === activeScope.network && selectedScope.layer === activeScope.layer ? t('layerPicker.goToDashboard') : t('common.select')} </Button> diff --git a/src/app/components/PageLayout/NetworkSelector.tsx b/src/app/components/PageLayout/NetworkSelector.tsx index 14a33a57a..9ee467276 100644 --- a/src/app/components/PageLayout/NetworkSelector.tsx +++ b/src/app/components/PageLayout/NetworkSelector.tsx @@ -10,8 +10,9 @@ import { COLORS } from '../../../styles/theme/colors' import { Network, getNetworkNames } from '../../../types/network' import { Layer } from '../../../oasis-nexus/api' import { LayerPicker } from './../LayerPicker' -import { fixedLayer, RouteUtils } from '../../utils/route-utils' +import { fixedLayer, fixedNetwork, RouteUtils } from '../../utils/route-utils' import { useConsensusFreshness, useRuntimeFreshness } from '../OfflineBanner/hook' +import { SearchScope } from '../../../types/searchScope' export const StyledBox = styled(Box)(({ theme }) => ({ marginLeft: `-${theme.spacing(1)}`, @@ -75,16 +76,16 @@ const NetworkSelectorView: FC<NetworkSelectorViewProps> = ({ isOutOfDate, layer, <LayerPicker open={openDrawer} onClose={handleDrawerClose} - onConfirm={(network: Network, layer: Layer) => { + onConfirm={(scope: SearchScope) => { handleDrawerClose() - navigate(RouteUtils.getDashboardRoute({ network, layer })) + navigate(RouteUtils.getDashboardRoute(scope)) }} isOutOfDate={isOutOfDate} /> {!isMobile && ( <NetworkButton isOutOfDate={isOutOfDate} layer={layer} network={network} onClick={handleDrawerOpen} /> )} - {!fixedLayer && !isTablet && network !== Network.mainnet && ( + {!fixedNetwork && !fixedLayer && !isTablet && network !== Network.mainnet && ( <StyledBox> <Typography component="span" diff --git a/src/app/utils/route-utils.ts b/src/app/utils/route-utils.ts index 1d9768db9..7cc2e0796 100644 --- a/src/app/utils/route-utils.ts +++ b/src/app/utils/route-utils.ts @@ -72,6 +72,8 @@ export const hiddenScopes: SearchScope[] = [ // { network: Network.mainnet, layer: Layer.sapphire }, // This is only for testing ] +export const mergeNetworksInLayerSelector = false + export const isScopeHidden = (scope: SearchScope): boolean => !!hiddenScopes.find(s => s.network === scope.network && s.layer === scope.layer) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 5440788fb..9711cd81f 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -544,10 +544,11 @@ "consensus": "The consensus layer is a scalable, high-throughput, secure, proof-of-stake consensus run by a decentralized set of validator nodes.", "goToDashboard": "Go to dashboard", "hex": "Hex: {{ id }}", + "hostedOn": "Hosted on {{ network }}", "readMore": "Read more about {{ layer }} on {{ network }} in Oasis Docs", "rpcHttp": "RPC HTTP endpoint: {{ endpoint }}", "rpcWebSockets": "RPC WebSockets endpoint: {{ endpoint }}", - "selected": "(active)", + "active": "(active)", "viewNetworks": "View Networks", "viewLayers": "View layers", "mainnet": {