Simplified Chain Endpoint Selection (#11742)
* feat: added star icon for chain * feat: add favorite chains header and localization support * fix: star ux flow enhanced * feat: mark chains as favorite and store it * feat: added transitions and shown already favorite chains * fix: remove favorite chains key from localStorage on error * feat: integrate favorite chains functionality in Group * chore: fix spacing between group headers * feat: enhance favorite chains functionality with structured storage and utility functions * fix: prevent rendering empty component for groups with no favorite chains * chore: translations * chore: match favorite network with relay and paraId * chore: remove logging * chore: providers list removed * feat: created dropdown for choosing provider * feat: add alert for no available WebSocket providers in NetworkDisplay * fix: update setApiUrl to preserve groupIndex * feat: improved favorite chain header style * feat: add getContrastingColor utility and update GroupDisplay styling
This commit is contained in:
parent
21ded61c60
commit
27ce6e2703
@ -8,7 +8,7 @@ import React, { useCallback, useMemo } from 'react';
|
||||
import { Icon, styled } from '@polkadot/react-components';
|
||||
|
||||
import Network from './Network.js';
|
||||
import { isFavoriteChain } from './utils.js';
|
||||
import { getContrastingColor, isFavoriteChain } from './utils.js';
|
||||
|
||||
interface Props {
|
||||
affinities: Record<string, string>;
|
||||
@ -22,27 +22,33 @@ interface Props {
|
||||
setApiUrl: (network: string, apiUrl: string) => void;
|
||||
setGroup: (groupIndex: number) => void;
|
||||
value: Group;
|
||||
highlightColor: string;
|
||||
}
|
||||
|
||||
function GroupDisplay ({ affinities, apiUrl, children, className = '', favoriteChains, index, isSelected, setApiUrl, setGroup, toggleFavoriteChain, value: { header, isSpaced, networks } }: Props): React.ReactElement<Props> {
|
||||
function GroupDisplay ({ affinities, apiUrl, children, className = '', favoriteChains, highlightColor, index, isSelected, setApiUrl, setGroup, toggleFavoriteChain, value: { header, isSpaced, networks } }: Props): React.ReactElement<Props> {
|
||||
const _setGroup = useCallback(
|
||||
() => setGroup(isSelected ? -1 : index),
|
||||
[index, isSelected, setGroup]
|
||||
);
|
||||
|
||||
const isFavoriteHeader = useMemo(() => header?.toString().includes('Favorite'), [header]);
|
||||
|
||||
const filtered = useMemo(
|
||||
() => networks.filter(({ isUnreachable }) => !isUnreachable),
|
||||
[networks]
|
||||
);
|
||||
|
||||
if (header?.toString().includes('Favorite') && Object.keys(favoriteChains).length === 0) {
|
||||
if (isFavoriteHeader && Object.keys(favoriteChains).length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledDiv className={`${className}${isSelected ? ' isSelected' : ''}`}>
|
||||
<StyledDiv
|
||||
className={`${className}${isSelected ? ' isSelected' : ''}`}
|
||||
highlightColor={highlightColor}
|
||||
>
|
||||
<div
|
||||
className={`groupHeader${isSpaced ? ' isSpaced' : ''}`}
|
||||
className={`groupHeader${isSpaced ? ' isSpaced' : ''}${isFavoriteHeader ? ' isFavoriteHeader' : ''}`}
|
||||
onClick={_setGroup}
|
||||
>
|
||||
<Icon icon={isSelected ? 'caret-up' : 'caret-down'} />
|
||||
@ -74,7 +80,7 @@ function GroupDisplay ({ affinities, apiUrl, children, className = '', favoriteC
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
const StyledDiv = styled.div<{ highlightColor: string; }>`
|
||||
.groupHeader {
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
@ -94,6 +100,23 @@ const StyledDiv = styled.div`
|
||||
.ui--Icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
&.isFavoriteHeader {
|
||||
&:hover {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
${(props) => props.highlightColor}f2 0%,
|
||||
${(props) => props.highlightColor}99 100%
|
||||
);
|
||||
color: ${(props) => getContrastingColor(props.highlightColor)};
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '⭐';
|
||||
margin-left: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.groupNetworks {
|
||||
|
||||
@ -5,10 +5,9 @@ import type { IFavoriteChainProps, Network } from './types.js';
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { ChainImg, Icon, styled } from '@polkadot/react-components';
|
||||
import { ChainImg, Dropdown, Icon, styled } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Url from './Url.js';
|
||||
|
||||
interface Props {
|
||||
affinity?: string; // unused - previous selection
|
||||
@ -27,10 +26,23 @@ function NetworkDisplay ({ apiUrl, className = '', isFavorite, setApiUrl, toggle
|
||||
[apiUrl, providers]
|
||||
);
|
||||
|
||||
const providersOptions = useMemo(() => {
|
||||
return providers.map(({ name, url }) => ({
|
||||
text: name,
|
||||
value: url
|
||||
}));
|
||||
}, [providers]);
|
||||
|
||||
const _selectUrl = useCallback(
|
||||
() => {
|
||||
const filteredProviders = providers.filter(({ url }) => !url.startsWith('light://'));
|
||||
|
||||
if (filteredProviders.length === 0) {
|
||||
alert('No WebSocket (wss://) provider available');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return setApiUrl(name, filteredProviders[Math.floor(Math.random() * filteredProviders.length)].url);
|
||||
},
|
||||
[name, providers, setApiUrl]
|
||||
@ -82,15 +94,15 @@ function NetworkDisplay ({ apiUrl, className = '', isFavorite, setApiUrl, toggle
|
||||
onClick={_toggleFavoriteChain}
|
||||
/>
|
||||
</div>
|
||||
{isSelected && providers.map(({ name, url }): React.ReactNode => (
|
||||
<Url
|
||||
apiUrl={apiUrl}
|
||||
key={url}
|
||||
label={name}
|
||||
setApiUrl={_setApiUrl}
|
||||
url={url}
|
||||
{isSelected &&
|
||||
<Dropdown
|
||||
className='isSmall'
|
||||
onChange={_setApiUrl}
|
||||
options={providersOptions}
|
||||
value={apiUrl}
|
||||
withLabel={false}
|
||||
/>
|
||||
))}
|
||||
}
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
// Copyright 2017-2025 @polkadot/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { styled, Toggle } from '@polkadot/react-components';
|
||||
|
||||
interface Props {
|
||||
apiUrl: string;
|
||||
className?: string;
|
||||
label: React.ReactNode;
|
||||
setApiUrl: (apiUrl: string) => void;
|
||||
url: string;
|
||||
}
|
||||
|
||||
function Url ({ apiUrl, className, label, setApiUrl, url }: Props): React.ReactElement<Props> {
|
||||
const _setApiUrl = useCallback(
|
||||
() => setApiUrl(url),
|
||||
[setApiUrl, url]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledToggle
|
||||
className={className}
|
||||
isRadio
|
||||
label={label}
|
||||
onChange={_setApiUrl}
|
||||
value={apiUrl === url}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledToggle = styled(Toggle)`
|
||||
padding: 0.25rem;
|
||||
text-align: right;
|
||||
|
||||
> label {
|
||||
max-width: 12.5rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Url);
|
||||
@ -11,6 +11,7 @@ import store from 'store';
|
||||
|
||||
import { createWsEndpoints, CUSTOM_ENDPOINT_KEY } from '@polkadot/apps-config';
|
||||
import { Button, Input, Sidebar, styled } from '@polkadot/react-components';
|
||||
import { defaultHighlight } from '@polkadot/react-components/styles';
|
||||
import { useApi } from '@polkadot/react-hooks';
|
||||
import { settings } from '@polkadot/ui-settings';
|
||||
import { isAscii } from '@polkadot/util';
|
||||
@ -189,7 +190,7 @@ function isLocalForkDisabled (hasUrlChanged: boolean, apiUrl: string, isUrlValid
|
||||
function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const linkOptions = createWsEndpoints(t);
|
||||
const { isLocalFork } = useApi();
|
||||
const { apiEndpoint, isLocalFork } = useApi();
|
||||
const [favoriteChains, setFavoriteChains] = useState(() => getFavoriteChains());
|
||||
const [groups, setGroups] = useState(() => combineEndpoints(linkOptions));
|
||||
const [{ apiUrl, groupIndex, hasUrlChanged, isUrlValid }, setApiUrl] = useState<UrlState>(() => extractUrlState(settings.get().apiUrl, groups));
|
||||
@ -269,7 +270,7 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem
|
||||
|
||||
return newValue;
|
||||
});
|
||||
setApiUrl(extractUrlState(apiUrl, groups));
|
||||
setApiUrl((state) => ({ ...extractUrlState(apiUrl, groups), groupIndex: state.groupIndex }));
|
||||
},
|
||||
[groups]
|
||||
);
|
||||
@ -280,7 +281,7 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem
|
||||
apiUrl = punycode.toASCII(apiUrl);
|
||||
}
|
||||
|
||||
setApiUrl(extractUrlState(apiUrl, groups));
|
||||
setApiUrl((state) => ({ ...extractUrlState(apiUrl, groups), groupIndex: state.groupIndex }));
|
||||
},
|
||||
[groups]
|
||||
);
|
||||
@ -368,6 +369,7 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem
|
||||
affinities={affinities}
|
||||
apiUrl={apiUrl}
|
||||
favoriteChains={favoriteChains}
|
||||
highlightColor={apiEndpoint?.ui.color || defaultHighlight}
|
||||
index={index}
|
||||
isSelected={groupIndex === index}
|
||||
key={index}
|
||||
|
||||
@ -112,3 +112,31 @@ export const isFavoriteChain = (
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export function getContrastingColor (hexColor: string): string {
|
||||
if (typeof hexColor !== 'string') {
|
||||
return '#000000';
|
||||
}
|
||||
|
||||
let hex = hexColor.replace('#', '').trim();
|
||||
|
||||
if (hex.length === 3) {
|
||||
hex = hex.split('').map((c) => c + c).join('');
|
||||
}
|
||||
|
||||
if (hex.length !== 6 || /[^0-9a-f]/i.test(hex)) {
|
||||
return '#000000';
|
||||
}
|
||||
|
||||
try {
|
||||
const r = parseInt(hex.substring(0, 2), 16);
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
|
||||
return luminance > 0.5 ? '#000000' : '#FFFFFF';
|
||||
} catch {
|
||||
return '#000000';
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user