Additional alignment of staking UI layout (#8828)
* Additional alignment of staking UI layout * Allow for validator tagging & sorting * Adjust flag injection (re-create object) * currentEra for staking queries * Pass SessionInfo structure * Display badges * Adjust flags * Adjust status media * Badge background adjustments * Don't do additional table split * Adjust table split
This commit is contained in:
parent
39de5d761a
commit
6ecccdc031
@ -66,6 +66,7 @@
|
||||
/* --color-table-foot: rgba(254, 240, 240, 0.66); */
|
||||
/* --color-table-head: rgba(254, 240, 240, 0.8); */
|
||||
--color-text: rgba(244, 242, 240, 0.8);
|
||||
--opacity-gray: 0.1;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
@ -90,6 +91,7 @@
|
||||
/* --color-table-foot: rgba(78, 78, 78, 0.66); */
|
||||
/* --color-table-head: rgba(78, 78, 78, 0.8); */
|
||||
--color-text: rgba(78, 78, 78, 0.8); /* 1 */
|
||||
--opacity-gray: 0.75;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
|
||||
@ -176,7 +176,7 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
|
||||
|
||||
Object.values(ret).forEach((a): void => {
|
||||
a[0][1] = 'start';
|
||||
a[0][2] = 3;
|
||||
a[0][2] = 4;
|
||||
});
|
||||
|
||||
return ret;
|
||||
|
||||
@ -7,6 +7,8 @@ import type { EventRecord } from '@polkadot/types/interfaces';
|
||||
|
||||
import { createNamedHook, useApi, useEventChanges, useMapKeys } from '@polkadot/react-hooks';
|
||||
|
||||
const EMPTY_PARAMS: unknown[] = [];
|
||||
|
||||
const OPT_KEY = {
|
||||
transform: (keys: StorageKey<[u32]>[]): u32[] =>
|
||||
keys.map(({ args: [id] }) => id)
|
||||
@ -29,7 +31,7 @@ function filter (records: EventRecord[]): Changes<u32> {
|
||||
|
||||
function useAssetIdsImpl (): u32[] | undefined {
|
||||
const { api } = useApi();
|
||||
const startValue = useMapKeys(api.query.assets.asset, [], OPT_KEY);
|
||||
const startValue = useMapKeys(api.query.assets.asset, EMPTY_PARAMS, OPT_KEY);
|
||||
|
||||
return useEventChanges([
|
||||
api.events.assets.Created,
|
||||
|
||||
93
packages/page-staking2/src/Validators/Validator/Status.tsx
Normal file
93
packages/page-staking2/src/Validators/Validator/Status.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
// Copyright 2017-2023 @polkadot/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Validator } from '../../types';
|
||||
import type { Heartbeat } from './types';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { Badge } from '@polkadot/react-components';
|
||||
import { useAccounts } from '@polkadot/react-hooks';
|
||||
|
||||
interface Props {
|
||||
heartbeat: Heartbeat;
|
||||
isChilled?: boolean;
|
||||
isPara?: boolean;
|
||||
isRelay?: boolean;
|
||||
nominators?: string[];
|
||||
validator: Validator;
|
||||
}
|
||||
|
||||
function Status ({ heartbeat: { authoredBlocks, isOnline }, isChilled, isPara, isRelay, nominators, validator: { isElected } }: Props): React.ReactElement<Props> {
|
||||
const { allAccounts } = useAccounts();
|
||||
|
||||
const isNominating = useMemo(
|
||||
() => nominators && nominators.some((a) => allAccounts.includes(a)),
|
||||
[allAccounts, nominators]
|
||||
);
|
||||
|
||||
const emptyBadge = (
|
||||
<Badge
|
||||
className='opaque'
|
||||
color='gray'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isNominating
|
||||
? (
|
||||
<Badge
|
||||
color='green'
|
||||
icon='hand-paper'
|
||||
/>
|
||||
)
|
||||
: emptyBadge
|
||||
}
|
||||
{isRelay && (
|
||||
isPara
|
||||
? (
|
||||
<Badge
|
||||
color='purple'
|
||||
icon='vector-square'
|
||||
/>
|
||||
)
|
||||
: emptyBadge
|
||||
)}
|
||||
{isChilled
|
||||
? (
|
||||
<Badge
|
||||
color='red'
|
||||
icon='cancel'
|
||||
/>
|
||||
)
|
||||
: isElected
|
||||
? (
|
||||
<Badge
|
||||
color='blue'
|
||||
icon='chevron-right'
|
||||
/>
|
||||
)
|
||||
: emptyBadge
|
||||
}
|
||||
{isOnline
|
||||
? authoredBlocks
|
||||
? (
|
||||
<Badge
|
||||
color='green'
|
||||
info={authoredBlocks}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Badge
|
||||
color='green'
|
||||
icon='envelope'
|
||||
/>
|
||||
)
|
||||
: emptyBadge
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Status);
|
||||
@ -1,43 +1,48 @@
|
||||
// Copyright 2017-2023 @polkadot/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { SessionInfo, Validator } from '../../types';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AddressSmall, Table, Tag } from '@polkadot/react-components';
|
||||
import { useToggle } from '@polkadot/react-hooks';
|
||||
import { FormatBalance } from '@polkadot/react-query';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import Status from './Status';
|
||||
import useExposure from './useExposure';
|
||||
import useHeartbeat from './useHeartbeat';
|
||||
|
||||
interface Props {
|
||||
activeEra: BN | null;
|
||||
className?: string;
|
||||
isFavorite: boolean;
|
||||
points?: string;
|
||||
stashId: string;
|
||||
isRelay: boolean;
|
||||
points?: number;
|
||||
sessionInfo: SessionInfo;
|
||||
toggleFavorite: (stashId: string) => void;
|
||||
validator: Validator;
|
||||
}
|
||||
|
||||
interface PropsExpanded {
|
||||
className?: string;
|
||||
stashId: string;
|
||||
validator: Validator;
|
||||
}
|
||||
|
||||
function ValidatorExpanded ({ className = '' }: PropsExpanded): React.ReactElement<PropsExpanded> {
|
||||
return (
|
||||
<tr className={`${className} isExpanded isLast`}>
|
||||
<td />
|
||||
<td></td>
|
||||
<td />
|
||||
<td />
|
||||
<td />
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function Validator ({ activeEra, className = '', isFavorite, points, stashId, toggleFavorite }: Props): React.ReactElement<Props> {
|
||||
function Validator ({ className = '', isRelay, points, sessionInfo, toggleFavorite, validator }: Props): React.ReactElement<Props> {
|
||||
const [isExpanded, toggleExpanded] = useToggle();
|
||||
const exposure = useExposure(stashId, activeEra);
|
||||
const exposure = useExposure(validator, sessionInfo);
|
||||
const heartbeat = useHeartbeat(validator, sessionInfo);
|
||||
|
||||
const pointsAnimClass = useMemo(
|
||||
() => (points && `greyAnim-${Date.now() % 25}`) || '',
|
||||
@ -48,17 +53,27 @@ function Validator ({ activeEra, className = '', isFavorite, points, stashId, to
|
||||
<>
|
||||
<tr className={`${className} isExpanded isFirst packedBottom`}>
|
||||
<Table.Column.Favorite
|
||||
address={stashId}
|
||||
isFavorite={isFavorite}
|
||||
address={validator.stashId}
|
||||
isFavorite={validator.isFavorite}
|
||||
toggle={toggleFavorite}
|
||||
/>
|
||||
<td
|
||||
className='statusInfo'
|
||||
rowSpan={2}
|
||||
>
|
||||
<Status
|
||||
heartbeat={heartbeat}
|
||||
isRelay={isRelay}
|
||||
validator={validator}
|
||||
/>
|
||||
</td>
|
||||
<td className='address relative all'>
|
||||
<AddressSmall value={stashId} />
|
||||
<AddressSmall value={validator.stashId} />
|
||||
{points && (
|
||||
<Tag
|
||||
className={`${pointsAnimClass} absolute`}
|
||||
color='lightgrey'
|
||||
label={points}
|
||||
label={formatNumber(points)}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
@ -70,6 +85,7 @@ function Validator ({ activeEra, className = '', isFavorite, points, stashId, to
|
||||
<tr className={`${className} isExpanded ${isExpanded ? '' : 'isLast'} packedTop`}>
|
||||
<td />
|
||||
<Table.Column.Balance
|
||||
className='relative'
|
||||
label={
|
||||
exposure?.waiting && (
|
||||
<>
|
||||
@ -85,7 +101,7 @@ function Validator ({ activeEra, className = '', isFavorite, points, stashId, to
|
||||
<td />
|
||||
</tr>
|
||||
{isExpanded && (
|
||||
<ValidatorExpanded stashId={stashId} />
|
||||
<ValidatorExpanded validator={validator} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
7
packages/page-staking2/src/Validators/Validator/types.ts
Normal file
7
packages/page-staking2/src/Validators/Validator/types.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// Copyright 2017-2023 @polkadot/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export interface Heartbeat {
|
||||
authoredBlocks?: number;
|
||||
isOnline: boolean;
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletStakingExposure } from '@polkadot/types/lookup';
|
||||
import type { SessionInfo, Validator } from '../../types';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
@ -31,11 +32,11 @@ interface Result {
|
||||
const OPT_EXPOSURE = {
|
||||
transform: ({ others, own, total }: PalletStakingExposure): Exposure => ({
|
||||
others: others
|
||||
.sort((a, b) => a.value.unwrap().cmp(b.value.unwrap()))
|
||||
.map(({ value, who }) => ({
|
||||
value: value.unwrap(),
|
||||
who: who.toString()
|
||||
})),
|
||||
}))
|
||||
.sort((a, b) => b.value.cmp(a.value)),
|
||||
own: own.unwrap(),
|
||||
total: total.unwrap()
|
||||
})
|
||||
@ -52,23 +53,26 @@ function getResult (exposure?: Exposure, clipped?: Exposure): Result {
|
||||
);
|
||||
|
||||
if (others.length) {
|
||||
waiting = { others, total: others.reduce((total, { value }) => total.iadd(value), new BN(0)) };
|
||||
waiting = {
|
||||
others,
|
||||
total: others.reduce((total, { value }) => total.iadd(value), new BN(0))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { clipped, exposure, waiting };
|
||||
}
|
||||
|
||||
function useExposureImpl (stashId: string, activeEra: BN | null): Result | undefined {
|
||||
function useExposureImpl ({ stashId }: Validator, { activeEra }: SessionInfo): Result | undefined {
|
||||
const { api } = useApi();
|
||||
|
||||
const exposureParams = useMemo(
|
||||
const params = useMemo(
|
||||
() => activeEra && [activeEra, stashId],
|
||||
[activeEra, stashId]
|
||||
);
|
||||
|
||||
const fullExposure = useCall(api.query.staking.erasStakers, exposureParams, OPT_EXPOSURE);
|
||||
const clipExposure = useCall(api.query.staking.erasStakersClipped, exposureParams, OPT_EXPOSURE);
|
||||
const fullExposure = useCall(params && api.query.staking.erasStakers, params, OPT_EXPOSURE);
|
||||
const clipExposure = useCall(params && api.query.staking.erasStakersClipped, params, OPT_EXPOSURE);
|
||||
|
||||
return useMemo(
|
||||
() => getResult(fullExposure, clipExposure),
|
||||
@ -0,0 +1,47 @@
|
||||
// Copyright 2017-2023 @polkadot/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option, u32 } from '@polkadot/types';
|
||||
import type { Codec } from '@polkadot/types/types';
|
||||
import type { SessionInfo, Validator } from '../../types';
|
||||
import type { Heartbeat } from './types';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
const OPT_BLOCKS = {
|
||||
transform: (authoredBlocks: u32): number =>
|
||||
authoredBlocks.toNumber()
|
||||
};
|
||||
|
||||
const OPT_BEATS = {
|
||||
// Option<WrapperOpaque<PalletImOnlineBoundedOpaqueNetworkState>>
|
||||
transform: (receivedHeartbeats: Option<Codec>): boolean =>
|
||||
receivedHeartbeats.isSome
|
||||
};
|
||||
|
||||
function useHeartbeatImpl ({ stashId, stashIndex }: Validator, { currentSession }: SessionInfo): Heartbeat {
|
||||
const { api } = useApi();
|
||||
|
||||
const params = useMemo(
|
||||
() => currentSession && ({
|
||||
authoredBlocks: [currentSession, stashId],
|
||||
receivedHeartbeats: [currentSession, stashIndex]
|
||||
}),
|
||||
[currentSession, stashId, stashIndex]
|
||||
);
|
||||
|
||||
const authoredBlocks = useCall(params && api.query.imOnline.authoredBlocks, params?.authoredBlocks, OPT_BLOCKS);
|
||||
const receivedHeartbeats = useCall(params && api.query.imOnline.receivedHeartbeats, params?.receivedHeartbeats, OPT_BEATS);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
authoredBlocks,
|
||||
isOnline: !!(authoredBlocks || receivedHeartbeats)
|
||||
}),
|
||||
[authoredBlocks, receivedHeartbeats]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useHeartbeat', useHeartbeatImpl);
|
||||
@ -1,78 +1,85 @@
|
||||
// Copyright 2017-2023 @polkadot/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SessionInfo } from '../types';
|
||||
import type { SessionInfo, Validator } from '../types';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Legend from '@polkadot/app-staking/Legend';
|
||||
import { Table } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import usePoints from './usePoints';
|
||||
import Validator from './Validator';
|
||||
import ValidatorRow from './Validator';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
isRelay: boolean;
|
||||
sessionInfo: SessionInfo;
|
||||
toggleFavorite: (stashId: string) => void;
|
||||
validatorsActive?: string[];
|
||||
validatorsFavorite?: string[];
|
||||
validatorsSession?: Validator[];
|
||||
}
|
||||
|
||||
function Validators ({ className = '', sessionInfo: { activeEra }, toggleFavorite, validatorsActive, validatorsFavorite }: Props): React.ReactElement<Props> {
|
||||
function Validators ({ className = '', isRelay, sessionInfo, toggleFavorite, validatorsSession }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const points = usePoints(activeEra);
|
||||
const points = usePoints(sessionInfo);
|
||||
|
||||
const headerActive = useRef<[string?, string?, number?][]>([
|
||||
// favorite, details, expand
|
||||
[t<string>('validators'), 'start', 3]
|
||||
]);
|
||||
|
||||
const headerFavorite = useRef<[string?, string?, number?][]>([
|
||||
// favorite, details, expand
|
||||
[t<string>('favorites'), 'start', 3]
|
||||
// favorite, badges, details, expand
|
||||
[t<string>('validators'), 'start', 4]
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{validatorsFavorite && (
|
||||
<Table
|
||||
className={className}
|
||||
header={headerFavorite.current}
|
||||
isSplit
|
||||
>
|
||||
{validatorsFavorite.map((stashId) => (
|
||||
<Validator
|
||||
activeEra={activeEra}
|
||||
isFavorite
|
||||
key={stashId}
|
||||
points={points?.[stashId]}
|
||||
stashId={stashId}
|
||||
toggleFavorite={toggleFavorite}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
)}
|
||||
<Table
|
||||
className={className}
|
||||
empty={validatorsActive && t<string>('No session validators found')}
|
||||
emptySpinner={t<string>('Retrieving session validators')}
|
||||
header={headerActive.current}
|
||||
isSplit
|
||||
>
|
||||
{validatorsActive?.map((stashId) => (
|
||||
<Validator
|
||||
activeEra={activeEra}
|
||||
isFavorite={false}
|
||||
key={stashId}
|
||||
points={points?.[stashId]}
|
||||
stashId={stashId}
|
||||
toggleFavorite={toggleFavorite}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
</>
|
||||
<StyledTable
|
||||
className={className}
|
||||
empty={validatorsSession && t<string>('No session validators found')}
|
||||
emptySpinner={t<string>('Retrieving session validators')}
|
||||
header={headerActive.current}
|
||||
isSplit
|
||||
legend={
|
||||
<Legend
|
||||
isRelay={isRelay}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{validatorsSession?.map((v) => (
|
||||
<ValidatorRow
|
||||
isRelay={isRelay}
|
||||
key={v.key}
|
||||
points={points?.[v.stashId]}
|
||||
sessionInfo={sessionInfo}
|
||||
toggleFavorite={toggleFavorite}
|
||||
validator={v}
|
||||
/>
|
||||
))}
|
||||
</StyledTable>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
td.statusInfo {
|
||||
padding: 0 0 0 0.5rem;
|
||||
vertical-align: middle;
|
||||
|
||||
> div {
|
||||
display: inline-block;
|
||||
max-width: 3.6rem;
|
||||
min-width: 3.6rem;
|
||||
|
||||
.ui--Badge {
|
||||
margin: 0.125rem;
|
||||
|
||||
&.opaque {
|
||||
opacity: var(--opacity-gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ td.address {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Validators);
|
||||
|
||||
@ -2,35 +2,35 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletStakingEraRewardPoints } from '@polkadot/types/lookup';
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { SessionInfo } from '../types';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
|
||||
import { BN_ZERO, formatNumber } from '@polkadot/util';
|
||||
import { BN_ZERO } from '@polkadot/util';
|
||||
|
||||
type Result = Record<string, string>;
|
||||
type Result = Record<string, number>;
|
||||
|
||||
const OPT_POINTS = {
|
||||
transform: ({ individual }: PalletStakingEraRewardPoints): Result =>
|
||||
[...individual.entries()]
|
||||
.filter(([, points]) => points.gt(BN_ZERO))
|
||||
.reduce((result: Result, [stashId, points]): Result => {
|
||||
result[stashId.toString()] = formatNumber(points);
|
||||
result[stashId.toString()] = points.toNumber();
|
||||
|
||||
return result;
|
||||
}, {})
|
||||
};
|
||||
|
||||
function usePointsImpl (activeEra: BN | null): Result | undefined {
|
||||
function usePointsImpl ({ activeEra }: SessionInfo): Result | undefined {
|
||||
const { api } = useApi();
|
||||
|
||||
const pointsParams = useMemo(
|
||||
const queryParams = useMemo(
|
||||
() => activeEra && [activeEra],
|
||||
[activeEra]
|
||||
);
|
||||
|
||||
return useCall(activeEra && api.query.staking.erasRewardPoints, pointsParams, OPT_POINTS);
|
||||
return useCall(queryParams && api.query.staking.erasRewardPoints, queryParams, OPT_POINTS);
|
||||
}
|
||||
|
||||
export default createNamedHook('usePoints', usePointsImpl);
|
||||
|
||||
@ -7,7 +7,7 @@ import React, { useMemo, useRef } from 'react';
|
||||
import { Route, Switch } from 'react-router';
|
||||
|
||||
import { Tabs } from '@polkadot/react-components';
|
||||
import { useFavorites } from '@polkadot/react-hooks';
|
||||
import { useApi, useFavorites } from '@polkadot/react-hooks';
|
||||
|
||||
import { STORE_FAVS_BASE } from './constants';
|
||||
import { useTranslation } from './translate';
|
||||
@ -15,27 +15,16 @@ import useSessionInfo from './useSessionInfo';
|
||||
import useValidatorsActive from './useValidatorsActive';
|
||||
import Validators from './Validators';
|
||||
|
||||
function splitValidators (favorites: string[], validators?: string[]): { validatorsActive?: string[], validatorsFavorite?: string[] } {
|
||||
if (!validators) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const validatorsFavorite = validators.filter((a) => favorites.includes(a));
|
||||
|
||||
return validatorsFavorite.length
|
||||
? { validatorsActive: validators.filter((a) => !validatorsFavorite.includes(a)), validatorsFavorite }
|
||||
: { validatorsActive: validators };
|
||||
}
|
||||
|
||||
function StakingApp ({ basePath }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE);
|
||||
const validators = useValidatorsActive();
|
||||
const sessionInfo = useSessionInfo();
|
||||
const validatorsActive = useValidatorsActive(favorites, sessionInfo);
|
||||
|
||||
const { validatorsActive, validatorsFavorite } = useMemo(
|
||||
() => splitValidators(favorites, validators),
|
||||
[favorites, validators]
|
||||
const isRelay = useMemo(
|
||||
() => !!(api.query.parasShared || api.query.shared)?.activeValidatorIndices,
|
||||
[api]
|
||||
);
|
||||
|
||||
const itemsRef = useRef([
|
||||
@ -55,10 +44,10 @@ function StakingApp ({ basePath }: Props): React.ReactElement<Props> {
|
||||
<Switch>
|
||||
<Route>
|
||||
<Validators
|
||||
isRelay={isRelay}
|
||||
sessionInfo={sessionInfo}
|
||||
toggleFavorite={toggleFavorite}
|
||||
validatorsActive={validatorsActive}
|
||||
validatorsFavorite={validatorsFavorite}
|
||||
validatorsSession={validatorsActive}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
|
||||
@ -4,5 +4,16 @@
|
||||
import type { BN } from '@polkadot/util';
|
||||
|
||||
export interface SessionInfo {
|
||||
activeEra: BN | null;
|
||||
activeEra?: BN | null;
|
||||
currentEra?: BN | null;
|
||||
currentSession?: BN | null;
|
||||
}
|
||||
|
||||
export interface Validator {
|
||||
isElected: boolean;
|
||||
isFavorite: boolean;
|
||||
isOwned: boolean;
|
||||
key: string;
|
||||
stashId: string;
|
||||
stashIndex: number;
|
||||
}
|
||||
|
||||
60
packages/page-staking2/src/useElectedValidators.ts
Normal file
60
packages/page-staking2/src/useElectedValidators.ts
Normal file
@ -0,0 +1,60 @@
|
||||
// Copyright 2017-2023 @polkadot/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { StorageKey, u32 } from '@polkadot/types';
|
||||
import type { AccountId32 } from '@polkadot/types/interfaces';
|
||||
import type { SessionInfo } from './types';
|
||||
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useMapKeys } from '@polkadot/react-hooks';
|
||||
|
||||
interface Cache {
|
||||
currentEra: SessionInfo['currentEra'];
|
||||
elected?: string[];
|
||||
}
|
||||
|
||||
const OPT_ELECTED = {
|
||||
transform: (keys: StorageKey<[u32, AccountId32]>[]): string[] =>
|
||||
keys.map(({ args: [, stashId] }) =>
|
||||
stashId.toString()
|
||||
)
|
||||
};
|
||||
|
||||
// since this would only change once per era for the full runtime,
|
||||
// we just store it globally to not have expensive map-key lookups
|
||||
const cached: Cache = {
|
||||
currentEra: null
|
||||
};
|
||||
|
||||
function useElectedValidatorsImpl ({ currentEra }: SessionInfo): string[] | undefined {
|
||||
const { api } = useApi();
|
||||
|
||||
const electedParams = useMemo(
|
||||
() => currentEra && (!cached.currentEra || !cached.currentEra.eq(currentEra))
|
||||
? [currentEra]
|
||||
: null,
|
||||
[currentEra]
|
||||
);
|
||||
|
||||
const elected = useMapKeys(electedParams && api.query.staking.erasStakers, electedParams, OPT_ELECTED);
|
||||
|
||||
useEffect((): void => {
|
||||
if (currentEra && elected) {
|
||||
cached.currentEra = currentEra;
|
||||
cached.elected = elected;
|
||||
}
|
||||
}, [currentEra, elected]);
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
currentEra &&
|
||||
cached.currentEra &&
|
||||
cached.currentEra.eq(currentEra) &&
|
||||
cached.elected
|
||||
) || elected,
|
||||
[currentEra, elected]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useElectedValidators', useElectedValidatorsImpl);
|
||||
@ -1,7 +1,7 @@
|
||||
// Copyright 2017-2023 @polkadot/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option } from '@polkadot/types';
|
||||
import type { Option, u32 } from '@polkadot/types';
|
||||
import type { PalletStakingActiveEraInfo } from '@polkadot/types/lookup';
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { SessionInfo } from './types';
|
||||
@ -10,8 +10,6 @@ import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
const EMPTY_RESULT: SessionInfo = { activeEra: null };
|
||||
|
||||
const OPT_ACTIVEERA = {
|
||||
transform: (activeEra: Option<PalletStakingActiveEraInfo>): BN | null =>
|
||||
activeEra.isSome
|
||||
@ -19,15 +17,20 @@ const OPT_ACTIVEERA = {
|
||||
: null
|
||||
};
|
||||
|
||||
const OPT_CURRENTERA = {
|
||||
transform: (currentEra: Option<u32>): BN | null =>
|
||||
currentEra.unwrapOr(null)
|
||||
};
|
||||
|
||||
function useSessionInfoImpl (): SessionInfo {
|
||||
const { api } = useApi();
|
||||
const activeEra = useCall(api.query.staking.activeEra, undefined, OPT_ACTIVEERA);
|
||||
const currentEra = useCall(api.query.staking.currentEra, undefined, OPT_CURRENTERA);
|
||||
const currentSession = useCall<u32>(api.query.session.currentIndex);
|
||||
|
||||
return useMemo(
|
||||
() => activeEra
|
||||
? { activeEra }
|
||||
: EMPTY_RESULT,
|
||||
[activeEra]
|
||||
() => ({ activeEra, currentEra, currentSession }),
|
||||
[activeEra, currentEra, currentSession]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
65
packages/page-staking2/src/useTaggedValidators.ts
Normal file
65
packages/page-staking2/src/useTaggedValidators.ts
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright 2017-2023 @polkadot/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SessionInfo, Validator } from './types';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useAccounts } from '@polkadot/react-hooks';
|
||||
import { objectSpread } from '@polkadot/util';
|
||||
|
||||
import useElectedValidators from './useElectedValidators';
|
||||
|
||||
function sort (a: Validator, b: Validator): number {
|
||||
return a.isFavorite === b.isFavorite
|
||||
? a.isOwned === b.isOwned
|
||||
? 0
|
||||
: a.isOwned
|
||||
? -1
|
||||
: 1
|
||||
: a.isFavorite
|
||||
? -1
|
||||
: 1;
|
||||
}
|
||||
|
||||
function withElected (validators: Validator[], elected?: string[]): Validator[] {
|
||||
return elected
|
||||
? validators.map((v): Validator => {
|
||||
const isElected = elected.includes(v.stashId);
|
||||
|
||||
return v.isElected !== isElected
|
||||
? objectSpread({}, v, { isElected })
|
||||
: v;
|
||||
})
|
||||
: validators;
|
||||
}
|
||||
|
||||
function withSort (allAccounts: string[], favorites: string[], validators: Validator[]): Validator[] {
|
||||
return validators
|
||||
.map((v): Validator => {
|
||||
const isFavorite = favorites.includes(v.stashId);
|
||||
const isOwned = allAccounts.includes(v.stashId);
|
||||
|
||||
return v.isFavorite !== isFavorite || v.isOwned !== isOwned
|
||||
? objectSpread({}, v, { isFavorite, isOwned })
|
||||
: v;
|
||||
})
|
||||
.sort(sort);
|
||||
}
|
||||
|
||||
function useTaggedValidatorsImpl (favorites: string[], sessionInfo: SessionInfo, validators?: Validator[]): Validator[] | undefined {
|
||||
const { allAccounts } = useAccounts();
|
||||
const elected = useElectedValidators(sessionInfo);
|
||||
|
||||
const flagged = useMemo(
|
||||
() => validators && withElected(validators, elected),
|
||||
[elected, validators]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => flagged && withSort(allAccounts, favorites, flagged),
|
||||
[allAccounts, favorites, flagged]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useTaggedValidators', useTaggedValidatorsImpl);
|
||||
@ -2,18 +2,33 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId32 } from '@polkadot/types/interfaces';
|
||||
import type { SessionInfo, Validator } from './types';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
import useTaggedValidators from './useTaggedValidators';
|
||||
|
||||
const OPT_VALIDATORS = {
|
||||
transform: (validators: AccountId32[]): string[] =>
|
||||
validators.map((a) => a.toString())
|
||||
transform: (validators: AccountId32[]): Validator[] =>
|
||||
validators.map((a, stashIndex) => {
|
||||
const stashId = a.toString();
|
||||
|
||||
return {
|
||||
isElected: false,
|
||||
isFavorite: false,
|
||||
isOwned: false,
|
||||
key: `${stashId}:${stashIndex}`,
|
||||
stashId,
|
||||
stashIndex
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
function useValidatorsActiveImpl (): string[] | undefined {
|
||||
function useValidatorsActiveImpl (favorites: string[], sessionInfo: SessionInfo): Validator[] | undefined {
|
||||
const { api } = useApi();
|
||||
const validators = useCall(api.query.session.validators, undefined, OPT_VALIDATORS);
|
||||
|
||||
return useCall(api.query.session.validators, undefined, OPT_VALIDATORS);
|
||||
return useTaggedValidators(favorites, sessionInfo, validators);
|
||||
}
|
||||
|
||||
export default createNamedHook('useValidatorsActive', useValidatorsActiveImpl);
|
||||
|
||||
68
packages/page-staking2/src/useValidatorsAll.ts
Normal file
68
packages/page-staking2/src/useValidatorsAll.ts
Normal file
@ -0,0 +1,68 @@
|
||||
// Copyright 2017-2023 @polkadot/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Changes } from '@polkadot/react-hooks/useEventChanges';
|
||||
import type { StorageKey } from '@polkadot/types';
|
||||
import type { AccountId32, EventRecord } from '@polkadot/types/interfaces';
|
||||
import type { SessionInfo, Validator } from './types';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useEventChanges, useMapKeys } from '@polkadot/react-hooks';
|
||||
|
||||
import useTaggedValidators from './useTaggedValidators';
|
||||
|
||||
const EMPTY_PARAMS: unknown[] = [];
|
||||
|
||||
const OPT_VALIDATORS = {
|
||||
transform: (keys: StorageKey<[AccountId32]>[]): AccountId32[] =>
|
||||
keys.map(({ args: [id] }) => id)
|
||||
};
|
||||
|
||||
function eventFilter (records: EventRecord[]): Changes<AccountId32> {
|
||||
const added: AccountId32[] = [];
|
||||
const removed: AccountId32[] = [];
|
||||
|
||||
records.forEach(({ event: { data: [id], method } }): void => {
|
||||
if (method === 'Bonded') {
|
||||
added.push(id as AccountId32);
|
||||
} else {
|
||||
removed.push(id as AccountId32);
|
||||
}
|
||||
});
|
||||
|
||||
return { added, removed };
|
||||
}
|
||||
|
||||
function mapValidators (validators?: AccountId32[]): Validator[] | undefined {
|
||||
return validators && validators.map((a) => {
|
||||
const stashId = a.toString();
|
||||
|
||||
return {
|
||||
isElected: false,
|
||||
isFavorite: false,
|
||||
isOwned: false,
|
||||
key: `${stashId}::-1`,
|
||||
stashId,
|
||||
stashIndex: -1
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function useValidatorsAllImpl (favorites: string[], sessionInfo: SessionInfo): Validator[] | undefined {
|
||||
const { api } = useApi();
|
||||
const startValue = useMapKeys(api.query.staking.validators, EMPTY_PARAMS, OPT_VALIDATORS);
|
||||
|
||||
const validators = useEventChanges([
|
||||
api.events.staking.Bonded
|
||||
], eventFilter, startValue);
|
||||
|
||||
const validatorsIndexed = useMemo(
|
||||
() => mapValidators(validators),
|
||||
[validators]
|
||||
);
|
||||
|
||||
return useTaggedValidators(favorites, sessionInfo, validatorsIndexed);
|
||||
}
|
||||
|
||||
export default createNamedHook('useValidatorsAll', useValidatorsAllImpl);
|
||||
31
packages/page-staking2/src/useValidatorsWaiting.ts
Normal file
31
packages/page-staking2/src/useValidatorsWaiting.ts
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright 2017-2023 @polkadot/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SessionInfo, Validator } from './types';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook } from '@polkadot/react-hooks';
|
||||
|
||||
import useValidatorsActive from './useValidatorsActive';
|
||||
import useValidatorsAll from './useValidatorsAll';
|
||||
|
||||
function excludeValidators (from?: Validator[], exclude?: Validator[]): Validator[] | undefined {
|
||||
return from && exclude && from.filter(({ stashId }) =>
|
||||
!exclude.some((v) => v.stashId === stashId)
|
||||
);
|
||||
}
|
||||
|
||||
function useValidatorsWaitingImpl (favorites: string[], sessionInfo: SessionInfo): Validator[] | undefined {
|
||||
const activeValidators = useValidatorsActive(favorites, sessionInfo);
|
||||
const allValidators = useValidatorsAll(favorites, sessionInfo);
|
||||
|
||||
// both active and all is already sorted and tagged, so we don't
|
||||
// need to re-sort the waiting list
|
||||
return useMemo(
|
||||
() => excludeValidators(allValidators, activeValidators),
|
||||
[activeValidators, allValidators]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useValidatorsWaiting', useValidatorsWaitingImpl);
|
||||
@ -8,6 +8,7 @@ import React from 'react';
|
||||
import { FormatBalance } from '@polkadot/react-query';
|
||||
|
||||
export interface Props {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
colSpan?: number;
|
||||
label?: React.ReactNode;
|
||||
@ -17,7 +18,7 @@ export interface Props {
|
||||
withLoading?: boolean;
|
||||
}
|
||||
|
||||
function Balance ({ className = '', colSpan, label, labelPost, rowSpan, value, withLoading }: Props): React.ReactElement<Props> {
|
||||
function Balance ({ children, className = '', colSpan, label, labelPost, rowSpan, value, withLoading }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<td
|
||||
className={`${className} ui--Table-Column-Balance number`}
|
||||
@ -39,6 +40,7 @@ function Balance ({ className = '', colSpan, label, labelPost, rowSpan, value, w
|
||||
/>
|
||||
)
|
||||
}
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,13 +9,14 @@ import styled from 'styled-components';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
export interface Props {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
colSpan?: number;
|
||||
rowSpan?: number;
|
||||
value: BN | number;
|
||||
}
|
||||
|
||||
function Id ({ className = '', colSpan, rowSpan, value }: Props): React.ReactElement<Props> {
|
||||
function Id ({ children, className = '', colSpan, rowSpan, value }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<StyledTd
|
||||
className={`${className} ui--Table-Column-Id`}
|
||||
@ -23,6 +24,7 @@ function Id ({ className = '', colSpan, rowSpan, value }: Props): React.ReactEle
|
||||
rowSpan={rowSpan}
|
||||
>
|
||||
<h2 className='--digits'>{formatNumber(value)}</h2>
|
||||
{children}
|
||||
</StyledTd>
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,7 +4,8 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Columar from '../Columar';
|
||||
import { useWindowColumns } from '@polkadot/react-hooks';
|
||||
|
||||
import Body from './Body';
|
||||
import Column from './Column';
|
||||
import Foot from './Foot';
|
||||
@ -24,45 +25,62 @@ interface Props {
|
||||
isInline?: boolean;
|
||||
isSplit?: boolean;
|
||||
legend?: React.ReactNode;
|
||||
maxColumns?: 2 | 3;
|
||||
noBodyTag?: boolean;
|
||||
}
|
||||
|
||||
function extractBody (children: React.ReactNode, isSplit?: boolean): { body: React.ReactNode | React.ReactNode[], bodySplit: [React.ReactNode[], React.ReactNode[]] | null, isEmpty: boolean } {
|
||||
interface BodyState {
|
||||
body: React.ReactNode | React.ReactNode[];
|
||||
isEmpty: boolean;
|
||||
isSplittable: boolean;
|
||||
}
|
||||
|
||||
function extractBody (children: React.ReactNode, isSplit: boolean): BodyState {
|
||||
if (!Array.isArray(children)) {
|
||||
return {
|
||||
body: children,
|
||||
bodySplit: null,
|
||||
isEmpty: !children
|
||||
isEmpty: !children,
|
||||
isSplittable: false
|
||||
};
|
||||
}
|
||||
|
||||
const body = children.filter((child) => !!child);
|
||||
const isEmpty = body.length === 0;
|
||||
let bodySplit: [React.ReactNode[], React.ReactNode[]] | null = null;
|
||||
|
||||
if (!isEmpty && isSplit) {
|
||||
const half = Math.ceil(body.length / 2);
|
||||
|
||||
bodySplit = [
|
||||
body.slice(0, half),
|
||||
body.slice(half)
|
||||
];
|
||||
}
|
||||
const isSplittable = !isEmpty && isSplit;
|
||||
|
||||
return {
|
||||
body: isEmpty || bodySplit
|
||||
body: isEmpty
|
||||
? null
|
||||
: body,
|
||||
bodySplit,
|
||||
isEmpty
|
||||
isEmpty,
|
||||
isSplittable
|
||||
};
|
||||
}
|
||||
|
||||
function TableBase ({ children, className = '', empty, emptySpinner, filter, footer, header, headerChildren, isFixed, isInline, isSplit, legend, noBodyTag }: Props): React.ReactElement<Props> {
|
||||
const { body, bodySplit, isEmpty } = useMemo(
|
||||
function splitBody (body: React.ReactNode[], numColumns: number): React.ReactNode[][] {
|
||||
const result = new Array<React.ReactNode[]>(numColumns);
|
||||
|
||||
for (let i = 0; i < numColumns; i++) {
|
||||
result[i] = [];
|
||||
}
|
||||
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
result[i % numColumns].push(body[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function TableBase ({ children, className = '', empty, emptySpinner, filter, footer, header, headerChildren, isFixed, isInline, isSplit = false, legend, maxColumns, noBodyTag }: Props): React.ReactElement<Props> {
|
||||
const { body, isEmpty, isSplittable } = useMemo(
|
||||
() => extractBody(children, isSplit),
|
||||
[children, isSplit]
|
||||
);
|
||||
const numColumns = useWindowColumns(maxColumns, isSplittable);
|
||||
const bodySplit = useMemo(
|
||||
() => isSplittable && (numColumns > 1) && splitBody(body as React.ReactNode[], numColumns),
|
||||
[body, isSplittable, numColumns]
|
||||
);
|
||||
|
||||
const headerNode = (
|
||||
<Head
|
||||
@ -85,18 +103,18 @@ function TableBase ({ children, className = '', empty, emptySpinner, filter, foo
|
||||
<table className={tableClassName}>
|
||||
{headerNode}
|
||||
</table>
|
||||
<Columar isPadded={false}>
|
||||
<Columar.Column>
|
||||
<table className={tableClassName}>
|
||||
<Body>{bodySplit[0]}</Body>
|
||||
</table>
|
||||
</Columar.Column>
|
||||
<Columar.Column>
|
||||
<table className={tableClassName}>
|
||||
<Body>{bodySplit[1]}</Body>
|
||||
</table>
|
||||
</Columar.Column>
|
||||
</Columar>
|
||||
<div className='ui--Table-Split'>
|
||||
{bodySplit.map((body, index) => (
|
||||
<div
|
||||
className={`ui--Table-Split-Column-${numColumns}`}
|
||||
key={index}
|
||||
>
|
||||
<table className={tableClassName}>
|
||||
<Body>{body}</Body>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
@ -127,9 +145,19 @@ const StyledDiv = styled.div`
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
|
||||
&.isSplit {
|
||||
> .ui--Columar {
|
||||
margin-bottom: 1.5rem;
|
||||
.ui--Table-Split {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
> .ui--Table-Split-Column-3 {
|
||||
max-width: 33.3%;
|
||||
min-width: 33.3%;
|
||||
}
|
||||
|
||||
> .ui--Table-Split-Column-2 {
|
||||
max-width: 50%;
|
||||
min-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -78,4 +78,5 @@ export { useTreasury } from './useTreasury';
|
||||
export { useTxBatch } from './useTxBatch';
|
||||
export { useVotingStatus } from './useVotingStatus';
|
||||
export { useWeight } from './useWeight';
|
||||
export { useWindowColumns } from './useWindowColumns';
|
||||
export { useWindowSize } from './useWindowSize';
|
||||
|
||||
@ -12,12 +12,12 @@ interface Options <T> {
|
||||
}
|
||||
|
||||
// FIXME This is generic, we cannot really use createNamedHook
|
||||
export function useMapEntries <T = any> (entry: QueryableStorageEntry<'promise'> | null | false | undefined, params: unknown[], { transform }: Options<T> = {}, at?: string | null | false): T | undefined {
|
||||
export function useMapEntries <T = any> (entry: QueryableStorageEntry<'promise'> | null | false | undefined, params?: unknown[] | null, { transform }: Options<T> = {}, at?: string | null | false): T | undefined {
|
||||
const [state, setState] = useState<T | undefined>();
|
||||
const checkRef = useRef<string | null>(null);
|
||||
|
||||
useEffect((): void => {
|
||||
if (entry) {
|
||||
if (entry && params) {
|
||||
const check = stringify({ at, params });
|
||||
|
||||
if (check !== checkRef.current) {
|
||||
|
||||
@ -12,12 +12,12 @@ interface Options <T> {
|
||||
}
|
||||
|
||||
// FIXME This is generic, we cannot really use createNamedHook
|
||||
export function useMapKeys <T = any> (entry: QueryableStorageEntry<'promise'> | null | false | undefined, params: unknown[], { transform }: Options<T> = {}, at?: string | null | false): T[] | undefined {
|
||||
export function useMapKeys <T = any> (entry: QueryableStorageEntry<'promise'> | null | false | undefined, params?: unknown[] | null, { transform }: Options<T> = {}, at?: string | null | false): T[] | undefined {
|
||||
const [state, setState] = useState<T[] | undefined>();
|
||||
const checkRef = useRef<string | null>(null);
|
||||
|
||||
useEffect((): void => {
|
||||
if (entry) {
|
||||
if (entry && params) {
|
||||
const check = stringify({ at, params });
|
||||
|
||||
if (check !== checkRef.current) {
|
||||
|
||||
24
packages/react-hooks/src/useWindowColumns.ts
Normal file
24
packages/react-hooks/src/useWindowColumns.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright 2017-2023 @polkadot/react-hooks authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook } from './createNamedHook';
|
||||
import { useWindowSize } from './useWindowSize';
|
||||
|
||||
function useWindowColumnsImpl (maxColumns: 0 | 1 | 2 | 3 = 3, isActive = true): number {
|
||||
const windowSize = useWindowSize(isActive);
|
||||
|
||||
return useMemo(
|
||||
() => isActive
|
||||
? windowSize.width >= 1500
|
||||
? Math.min(3, maxColumns)
|
||||
: windowSize.width >= 1050
|
||||
? Math.min(maxColumns, 2)
|
||||
: 1
|
||||
: 1,
|
||||
[isActive, maxColumns, windowSize]
|
||||
);
|
||||
}
|
||||
|
||||
export const useWindowColumns = createNamedHook('useWindowColumns', useWindowColumnsImpl);
|
||||
@ -3,13 +3,18 @@
|
||||
|
||||
import type { WindowSize } from './ctx/types';
|
||||
|
||||
import { useContext } from 'react';
|
||||
import { useContext, useRef } from 'react';
|
||||
|
||||
import { WindowSizeCtx } from './ctx/WindowSize';
|
||||
import { createNamedHook } from './createNamedHook';
|
||||
|
||||
function useWindowSizeImpl (): WindowSize {
|
||||
return useContext(WindowSizeCtx);
|
||||
function useWindowSizeImpl (isActive = true): WindowSize {
|
||||
const windowSize = useContext(WindowSizeCtx);
|
||||
const staticRef = useRef(windowSize);
|
||||
|
||||
return isActive
|
||||
? windowSize
|
||||
: staticRef.current;
|
||||
}
|
||||
|
||||
export const useWindowSize = createNamedHook('useWindowSize', useWindowSizeImpl);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user