Adding Filters for the Coretime - Overview Tab according to feedback (#11217)

* added Expires soon warning on everything that is not Reservation and expires within 7 days

* added subscan links to the table, link for the project and for the block

* filter by parachain id

* filter by parachain name

* filter by type

* sorting filters by lastBlock

* filters working together

* filtering update

* lint

* refactor

* unused var

* flipped arrows for ASC and DESC

* lint

* lint

* lint
This commit is contained in:
Daria 2025-02-10 21:20:50 +13:00 committed by GitHub
parent 043e0551b4
commit c24c90ec8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 581 additions and 46 deletions

View File

@ -10,28 +10,40 @@ export const Subscan: ExternalDef = {
chains: {
Acala: 'acala',
'Acala Mandala TC5': 'acala-testnet',
Ajuna: 'ajuna',
'Ajuna Polkadot': 'ajuna',
'Aleph Zero': 'alephzero',
'Aleph Zero Testnet': 'alephzero-testnet',
Altair: 'altair',
AssetHub: 'assethub-polkadot',
Astar: 'astar',
Autonomys: 'autonomys',
'Bajun Kusama': 'bajun',
Basilisk: 'basilisk',
Bifrost: 'bifrost-kusama',
'Bifrost Polkadot': 'bifrost',
BridgeHub: 'bridgehub-polkadot',
'Calamari Parachain': 'calamari',
Centrifuge: 'centrifuge',
ChainX: 'chainx',
Clover: 'clover',
Collectives: 'collectives-polkadot',
'Composable Finance': 'composable',
Continuum: 'continuum',
'Continuum Network': 'continuum',
Coretime: 'coretime-polkadot',
Crab2: 'crab',
Creditcoin: 'creditcoin',
'Creditcoin3 Testnet': 'creditcoin3-testnet',
Crust: 'crust',
'Crust Network': 'crust-parachain',
'Crust Shadow': 'shadow',
Darwinia: 'darwinia',
Darwinia2: 'darwinia',
Dock: 'dock',
'Dolphin Parachain Testnet': 'dolphin',
'Energy Web X': 'energywebx',
Humanode: 'humanode',
'Humanode Mainnet': 'humanode',
Hydration: 'hydration',
'Integritee Network (Kusama)': 'integritee',
@ -44,21 +56,30 @@ export const Subscan: ExternalDef = {
Kusama: 'kusama',
'Kusama Asset Hub': 'assethub-kusama',
'Mangata Kusama Mainnet': 'mangatax',
Manta: 'manta',
'Moonbase Alpha': 'moonbase',
Moonbeam: 'moonbeam',
Moonriver: 'moonriver',
Mythos: 'mythos',
NeuroWeb: 'neuroweb',
'NeuroWeb Testnet': 'neuroweb-testnet',
Nodle: 'nodle',
'Nodle Parachain': 'nodle',
'OPAL by UNIQUE': 'opal',
'Paseo Testnet': 'paseo',
Peaq: 'peaq',
Pendulum: 'pendulum',
People: 'people-polkadot',
Phala: 'phala',
'Phala Network': 'phala',
Picasso: 'picasso',
'Pioneer Network': 'pioneer',
Polkadex: 'polkadex',
Polimec: 'polimec',
Polkadex: 'polkadex-parachain',
Polkadot: 'polkadot',
'Polkadot Asset Hub': 'assethub-polkadot',
Polymesh: 'polymesh',
'Polymesh Mainnet': 'polymesh',
'Polymesh Testnet': 'polymesh-testnet',
'QUARTZ by UNIQUE': 'quartz',
Robonomics: 'robonomics',
@ -72,6 +93,8 @@ export const Subscan: ExternalDef = {
Stafi: 'stafi',
'Turing Network': 'turing',
UNIQUE: 'unique',
Unique: 'unique',
Vara: 'vara',
'Vara Network': 'vara',
Westend: 'westend',
Zeitgeist: 'zeitgeist',

View File

@ -0,0 +1,126 @@
// Copyright 2017-2025 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ChainInformation } from '@polkadot/react-hooks/types';
import type { ActiveFilters } from '../types.js';
import React, { useCallback, useState } from 'react';
import { Button, Dropdown, Input } from '@polkadot/react-components';
import { useTranslation } from '../translate.js';
import { FilterType, useBlocksSort, useSearchFilter, useTypeFilter } from './filters/index.js';
interface Props {
chainInfo: Record<number, ChainInformation>;
data: number[];
onFilter: (data: number[]) => void;
}
function Filters ({ chainInfo, data: initialData, onFilter }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [activeFilters, setActiveFilters] = useState<ActiveFilters>({
search: [],
type: []
});
const { apply: applyBlocksSort, direction, onApply: onApplySort, reset: resetSort } = useBlocksSort({
chainInfo,
data: initialData,
onFilter: (data) => handleFilter(data, FilterType.BLOCKS)
});
const { apply: applySearchFilter, onApply: onApplySearch, reset: resetSearch, searchValue } = useSearchFilter({
data: initialData,
onFilter: (data) => handleFilter(data, FilterType.SEARCH)
});
const { apply: applyTypeFilter, onApply: onApplyType, reset: resetType, selectedType, typeOptions } = useTypeFilter({
chainInfo,
data: initialData,
onFilter: (data) => handleFilter(data, FilterType.TYPE)
});
/**
* 1. Applies additional filtering already present in the filters
* 2. Performs filtering based on the filter type
*/
const handleFilter = useCallback((
filteredData: number[],
filterType: FilterType
): void => {
let resultData = filteredData;
if (filterType !== FilterType.SEARCH) {
resultData = applySearchFilter(resultData, activeFilters.search);
}
if (filterType !== FilterType.TYPE) {
resultData = applyTypeFilter(resultData, activeFilters.type);
}
if (filterType !== FilterType.BLOCKS && direction) {
resultData = applyBlocksSort(resultData, direction);
}
if (filterType !== FilterType.BLOCKS) {
setActiveFilters((prev) => ({
...prev,
[filterType]: filteredData.length === initialData.length ? [] : filteredData
}));
}
onFilter(resultData);
}, [initialData, onFilter, activeFilters, direction, applyBlocksSort, applyTypeFilter, applySearchFilter]);
const resetAllFilters = useCallback(() => {
resetSearch();
resetType();
resetSort();
setActiveFilters({ search: [], type: [] });
onFilter(initialData);
}, [initialData, onFilter, resetSearch, resetType, resetSort]);
const hasActiveFilters = searchValue || selectedType || direction;
return (
<div style={{ alignItems: 'center', display: 'flex', flexDirection: 'row', gap: '10px' }}>
<div style={{ minWidth: '250px' }}>
<Input
aria-label={t('Search by parachain id or name')}
className='full isSmall'
label={t('Search')}
onChange={onApplySearch}
placeholder={t('parachain id or name')}
value={searchValue}
/>
</div>
<Dropdown
className='isSmall'
label={t('type')}
onChange={onApplyType}
options={typeOptions}
placeholder='select type'
value={selectedType}
/>
<div style={{ height: '20px' }}>
<Button
icon={direction ? (direction === 'DESC' ? 'arrow-up' : 'arrow-down') : 'sort'}
label={t('blocks')}
onClick={onApplySort}
/>
</div>
{hasActiveFilters && (
<div style={{ height: '20px' }}>
<Button
icon='times'
label={t('Reset filters')}
onClick={resetAllFilters}
/>
</div>
)}
</div>
);
}
export default Filters;

View File

@ -0,0 +1,7 @@
// Copyright 2017-2025 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from '../../types.js';
export * from './useBlockSort.js';
export * from './useSearchFilter.js';
export * from './useTypeFilter.js';

View File

@ -0,0 +1,56 @@
// Copyright 2017-2025 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ChainInformation } from '@polkadot/react-hooks/types';
import type { ChainInfoFilterProps, SortDirection } from '../../types.js';
import { useCallback, useState } from 'react';
export function sortByBlocks (data: number[], chainInfo: Record<number, ChainInformation>, direction: SortDirection): number[] {
if (!data || !chainInfo || !direction) {
return data || [];
}
const filteredData = data.filter((block) => !!chainInfo[block]?.workTaskInfo[0]);
return [...filteredData].sort((a, b) => {
const aInfo = chainInfo[a]?.workTaskInfo[0];
const bInfo = chainInfo[b]?.workTaskInfo[0];
return direction === 'DESC'
? bInfo.lastBlock - aInfo.lastBlock
: aInfo.lastBlock - bInfo.lastBlock;
});
}
const getNextSortState = (current: SortDirection): SortDirection =>
({ '': 'DESC', ASC: '', DESC: 'ASC' } as const)[current];
export function useBlocksSort ({ chainInfo, data, onFilter }: ChainInfoFilterProps) {
const [direction, setDirection] = useState<SortDirection>('');
const apply = useCallback((data: number[], sort: SortDirection): number[] => {
return sort
? sortByBlocks(data, chainInfo, sort)
: data;
}, [chainInfo]);
const onApply = useCallback(() => {
const nextDirection = getNextSortState(direction);
setDirection(nextDirection);
onFilter(nextDirection ? sortByBlocks(data, chainInfo, nextDirection) : data);
}, [data, chainInfo, onFilter, direction]);
const reset = useCallback(() => {
setDirection('');
onFilter(data || []);
}, [data, onFilter]);
return {
apply,
direction,
onApply,
reset
};
}

View File

@ -0,0 +1,78 @@
// Copyright 2017-2025 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback, useMemo, useState } from 'react';
import { useRelayEndpoints } from '@polkadot/react-hooks/useParaEndpoints';
interface UseSearchFilterProps {
data: number[];
onFilter: (data: number[]) => void;
}
export function useSearchFilter ({ data, onFilter }: UseSearchFilterProps) {
const [searchValue, setSearchValue] = useState('');
const endpoints = useRelayEndpoints();
const endPointsMap = useMemo(() =>
Object.fromEntries(
endpoints
.filter((e) => e?.text && e.paraId)
.map((e) => [
React.isValidElement(e.text) ? '' : String(e.text),
e.paraId
])
),
[endpoints]
);
const apply = useCallback((data: number[], activeSearch: number[]): number[] => {
return activeSearch.length > 0
? data.filter((id) => activeSearch.includes(id))
: data;
}, []);
const reset = useCallback(() => {
setSearchValue('');
onFilter(data);
}, [data, onFilter]);
const onInputChange = useCallback((v: string) => {
setSearchValue(v);
const searchLower = v.trim().toLowerCase();
if (!searchLower) {
onFilter(data);
return;
}
const matchingIds = new Set<number>();
for (const item of data) {
const itemStr = item.toString().toLowerCase();
if (itemStr.includes(searchLower)) {
matchingIds.add(item);
continue;
}
for (const [key, value] of Object.entries(endPointsMap)) {
if (key.toLowerCase().includes(searchLower) && value === item) {
matchingIds.add(item);
break;
}
}
}
const filteredData = Array.from(matchingIds);
onFilter(apply(data, filteredData));
}, [data, endPointsMap, onFilter, apply]);
return {
apply,
onApply: onInputChange,
reset,
searchValue
};
}

View File

@ -0,0 +1,76 @@
// Copyright 2017-2025 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { FlagColor } from '@polkadot/react-components/types';
import type { ChainInfoFilterProps } from '../../types.js';
import React, { useCallback, useState } from 'react';
import { Tag } from '@polkadot/react-components';
import { CoreTimeTypes } from '@polkadot/react-hooks/constants';
import { coretimeTypeColours } from '../../utils/index.js';
const coretimeTypes = Object.keys(CoreTimeTypes).filter((key) => isNaN(Number(key)));
const typeOptions = [
{
text: 'All',
value: 'All'
},
...coretimeTypes.map((type) => ({
text: (
<Tag
color={coretimeTypeColours[CoreTimeTypes[type as keyof typeof CoreTimeTypes]] as FlagColor}
label={type}
/>
),
value: CoreTimeTypes[type as keyof typeof CoreTimeTypes].toString()
}))
];
export function useTypeFilter ({ chainInfo, data, onFilter }: ChainInfoFilterProps) {
const [selectedType, setSelectedType] = useState<string>('');
const [activeType, setActiveType] = useState<number[]>([]);
const apply = useCallback((data: number[], activeType: number[]): number[] => {
return activeType.length > 0
? data.filter((id) => activeType.includes(id))
: data;
}, []);
const reset = useCallback(() => {
setSelectedType('');
setActiveType([]);
onFilter(data);
}, [data, onFilter]);
const onDropDownChange = useCallback((v: string) => {
setSelectedType(v);
if (!v || v === 'All') {
setActiveType([]);
onFilter(data);
return;
}
const filteredData = data.filter((paraId) => {
const taskInfo = chainInfo[paraId]?.workTaskInfo;
return taskInfo?.length > 0 && taskInfo[0].type.toString() === v;
});
setActiveType(filteredData);
onFilter(apply(data, filteredData));
}, [chainInfo, data, onFilter, apply]);
return {
activeType,
apply,
onApply: onDropDownChange,
reset,
selectedType,
typeOptions
};
}

View File

@ -1,11 +1,12 @@
// Copyright 2017-2025 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useRef } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Table } from '@polkadot/react-components';
import { type CoretimeInformation } from '@polkadot/react-hooks/types';
import Filters from './Overview/Filters.js';
import ParachainTableRow from './ParachainTableRow.js';
import { useTranslation } from './translate.js';
@ -24,31 +25,54 @@ function ParachainsTable ({ coretimeInfo }: Props): React.ReactElement<Props> {
[t('end'), 'start media--1000'],
[t('renewal'), 'start media--1200'],
[t('renewal price'), 'start media--1200'],
[t('links'), 'start media--800'],
[t('other cores'), 'end']
]);
const [taskIds, setTaskIds] = useState<number[]>([]);
const onFilter = useCallback((filteredData: number[]) => {
setTaskIds(filteredData);
}, []);
useEffect(() => {
if (coretimeInfo?.taskIds) {
setTaskIds(coretimeInfo?.taskIds);
}
}, [coretimeInfo?.taskIds]);
return (
<Table
emptySpinner={false}
header={headerRef.current}
isSplit={false}
>
<>
<Filters
chainInfo={coretimeInfo?.chainInfo}
data={coretimeInfo?.taskIds}
onFilter={onFilter}
/>
<Table
emptySpinner={false}
header={headerRef.current}
isSplit={false}
>
{taskIds?.map((taskId: number) => {
const chain = coretimeInfo.chainInfo[taskId];
{coretimeInfo?.taskIds?.map((taskId: number) => {
const chain = coretimeInfo.chainInfo[taskId];
if (!chain) {
return null;
}
return (
<ParachainTableRow
chain={chain}
key={chain.id}
lastCommittedTimeslice={coretimeInfo.status.lastCommittedTimeslice}
regionBegin={coretimeInfo.salesInfo.regionBegin}
regionEnd={coretimeInfo.salesInfo.regionEnd}
/>
);
})}
return (
<ParachainTableRow
chain={chain}
key={chain.id}
lastCommittedTimeslice={coretimeInfo.status.lastCommittedTimeslice}
regionBegin={coretimeInfo.salesInfo.regionBegin}
regionEnd={coretimeInfo.salesInfo.regionEnd}
/>
);
})}
</Table>
</Table>
</>
);
}

View File

@ -6,11 +6,12 @@ import type { ChainWorkTaskInformation, LegacyLease } from '@polkadot/react-hook
import React from 'react';
import { ParaLink, styled, Tag } from '@polkadot/react-components';
import { MarkWarning, ParaLink, styled, Tag } from '@polkadot/react-components';
import { ParaLinkType } from '@polkadot/react-components/ParaLink';
import { ChainRenewalStatus, CoreTimeTypes } from '@polkadot/react-hooks/constants';
import { BN, formatBalance, formatNumber } from '@polkadot/util';
import { estimateTime } from './utils/index.js';
import { coretimeTypeColours, estimateTime } from './utils/index.js';
import { useCoretimeContext } from './CoretimeContext.js';
interface Props {
@ -23,47 +24,87 @@ interface Props {
highlight?: boolean
}
const colours: Record<string, string> = {
[CoreTimeTypes.Reservation]: 'orange',
[CoreTimeTypes.Lease]: 'blue',
[CoreTimeTypes['Bulk Coretime']]: 'pink'
};
interface StyledCellProps {
$p: boolean;
$width?: string;
}
const StyledCell = styled.td<{ $p: boolean }>`
const StyledCell = styled.td<StyledCellProps>`
&& {
background-color: ${({ $p }) => ($p ? '#F9FAFB' : undefined)};
width: ${({ $width }) => $width};
}
height: 55px;
`;
const StyledMarkWarning = styled(MarkWarning)`
width: fit-content;
margin: 0;
display: inline-block;
vertical-align: middle;
&.mark {
margin: 0 0 0 1rem;
display: inline;
}
`;
const EXPIRES_IN_DAYS = 7;
function Row ({ chainRecord, highlight = false, id, lastCommittedTimeslice, lease, regionBegin, regionEnd }: Props): React.ReactElement<Props> {
const chainRegionEnd = (chainRecord.renewalStatus === ChainRenewalStatus.Renewed ? regionEnd : regionBegin);
const targetTimeslice = lease?.until || chainRegionEnd;
const showEstimates = !!targetTimeslice && Object.values(CoreTimeTypes)[chainRecord.type] !== CoreTimeTypes.Reservation;
const { coretimeInfo, get } = useCoretimeContext();
const estimatedTime = showEstimates && get && coretimeInfo &&
estimateTime(targetTimeslice, get.blocks.relay(lastCommittedTimeslice), coretimeInfo?.constants?.relay);
const isWithinWeek = estimatedTime && new Date(estimatedTime).getTime() - Date.now() < EXPIRES_IN_DAYS * 24 * 60 * 60 * 1000;
const isReservation = chainRecord.type === CoreTimeTypes.Reservation;
return (
<React.Fragment key={`${id}`}>
<StyledCell $p={highlight}>{id}</StyledCell>
<StyledCell
$p={highlight}
$width='150px'
>{id}</StyledCell>
<StyledCell
$p={highlight}
$width='150px'
className='media--800'
>{<ParaLink id={new BN(id)} />}</StyledCell>
<StyledCell $p={highlight}>{chainRecord?.workload?.core}</StyledCell>
<StyledCell $p={highlight}>
<Tag
color={colours[chainRecord.type] as FlagColor}
color={coretimeTypeColours[chainRecord.type] as FlagColor}
label={Object.values(CoreTimeTypes)[chainRecord.type]}
/>
</StyledCell>
<StyledCell
$p={highlight}
className='media--800'
>{showEstimates && get && formatNumber(get.blocks.relay(targetTimeslice)).toString()}</StyledCell>
>{showEstimates && chainRecord?.lastBlock &&
<a
href={`https://polkadot.subscan.io/block/${chainRecord?.lastBlock}`}
rel='noreferrer'
target='_blank'
>{formatNumber(chainRecord?.lastBlock)}
</a>}
</StyledCell>
<StyledCell
$p={highlight}
className='media--1000'
>{showEstimates && get && coretimeInfo && estimateTime(targetTimeslice, get.blocks.relay(lastCommittedTimeslice), coretimeInfo?.constants?.relay)}</StyledCell>
style={{ whiteSpace: 'nowrap' }}
>
<span>
{estimatedTime}
{!!isWithinWeek && !isReservation && (
<StyledMarkWarning
content='Expires Soon'
/>
)}
</span>
</StyledCell>
<StyledCell
$p={highlight}
className='media--1200'
@ -72,6 +113,24 @@ function Row ({ chainRecord, highlight = false, id, lastCommittedTimeslice, leas
$p={highlight}
className='media--1200'
>{chainRecord?.renewal ? formatBalance(chainRecord.renewal?.price.toString()) : ''}</StyledCell>
<StyledCell
$p={highlight}
className='media--800'
>{<div style={{ alignItems: 'center', columnGap: '12px', display: 'flex', flexDirection: 'row', justifyContent: 'left' }}>
<ParaLink
id={new BN(id)}
showLogo={false}
type={ParaLinkType.SUBSCAN}
/>
<div style={{ marginBottom: '2px' }}>
<ParaLink
id={new BN(id)}
showLogo={false}
type={ParaLinkType.HOME}
/>
</div>
</div>}
</StyledCell>
{highlight && <StyledCell $p={highlight} />}
</React.Fragment>

View File

@ -1,6 +1,7 @@
// Copyright 2017-2025 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ChainInformation } from '@polkadot/react-hooks/types';
import type { PhaseName } from './constants.js';
export interface PhaseInfo {
@ -94,3 +95,25 @@ export interface GetResponse {
relay: (blocks: number) => number;
};
}
export interface BaseFilterProps {
data: number[];
onFilter: (data: number[]) => void;
}
export interface ChainInfoFilterProps extends BaseFilterProps {
chainInfo: Record<number, ChainInformation>;
}
export type SortDirection = 'DESC' | 'ASC' | '';
export enum FilterType {
BLOCKS = 'blocks',
SEARCH = 'search',
TYPE = 'type'
}
export interface ActiveFilters {
search: number[];
type: number[];
}

View File

@ -4,6 +4,7 @@
import type { ChainBlockConstants, ChainConstants, CoretimeInformation } from '@polkadot/react-hooks/types';
import type { ChainName, GetResponse, RegionInfo } from '../types.js';
import { CoreTimeTypes } from '@polkadot/react-hooks/constants';
import { BN } from '@polkadot/util';
type FirstCycleStartType = Record<
@ -34,6 +35,12 @@ export const FirstCycleStart: FirstCycleStartType = {
}
};
export const coretimeTypeColours: Record<string, string> = {
[CoreTimeTypes.Reservation]: 'orange',
[CoreTimeTypes.Lease]: 'blue',
[CoreTimeTypes['Bulk Coretime']]: 'pink'
};
export function formatDate (date: Date) {
const day = date.getDate();
const month = date.toLocaleString('default', { month: 'short' });

View File

@ -5,17 +5,27 @@ import type { BN } from '@polkadot/util';
import React, { useMemo } from 'react';
import { Subscan } from '@polkadot/apps-config/links/subscan';
import { useParaEndpoints } from '@polkadot/react-hooks';
import ChainImg from './ChainImg.js';
import Icon from './Icon.js';
import { styled } from './styled.js';
export enum ParaLinkType {
PJS = 'pjs',
HOME = 'home',
SUBSCAN = 'subscan'
}
interface Props {
className?: string;
id: BN;
showLogo?: boolean;
type?: ParaLinkType;
}
function ParaLink ({ className, id }: Props): React.ReactElement<Props> | null {
function ParaLink ({ className, id, showLogo = true, type = ParaLinkType.PJS }: Props): React.ReactElement<Props> | null {
const endpoints = useParaEndpoints(id);
const links = useMemo(
() => endpoints.filter(({ isDisabled, isUnreachable }) => !isDisabled && !isUnreachable),
@ -26,25 +36,63 @@ function ParaLink ({ className, id }: Props): React.ReactElement<Props> | null {
return null;
}
const { text, ui, value } = links.length
const { homepage, text, ui, value } = links.length
? links[links.length - 1]
: endpoints[0];
const subscanUrl = text &&
typeof text === 'string' &&
Subscan.chains[text] &&
Subscan.create(Subscan.chains[text], '', '').toString();
return (
<StyledDiv className={className}>
<ChainImg
isInline
logo={ui.logo || 'empty'}
withoutHl
/>
{showLogo && (
<ChainImg
isInline
logo={ui.logo || 'empty'}
withoutHl
/>
)}
{links.length
? (
<a
className='chainAlign'
href={`${window.location.origin}${window.location.pathname}?rpc=${encodeURIComponent(value)}`}
>{text}</a>
<>
{type === ParaLinkType.SUBSCAN && !!subscanUrl && (
<a
href={subscanUrl}
rel='noopener noreferrer'
target='_blank'
>
<img
alt='Subscan'
height='20'
src={Subscan.ui.logo}
/>
</a>
)}
{type === ParaLinkType.HOME && homepage && (
<a
href={homepage}
rel='noopener noreferrer'
target='_blank'
>
<Icon
className='parent-icon'
icon='house'
/>
</a>
)}
{type === ParaLinkType.PJS && (
<a
className='chainAlign'
href={`${window.location.origin}${window.location.pathname}?rpc=${encodeURIComponent(value)}`}
>
{typeof text === 'string' ? text : text?.toString()}
</a>
)}
</>
)
: text
: type === ParaLinkType.PJS ? (typeof text === 'string' ? text : text?.toString()) : null
}
</StyledDiv>
);

View File

@ -332,6 +332,7 @@ export interface PalletBrokerConfigRecord {
}
export interface ChainWorkTaskInformation {
lastBlock: number
renewal: PotentialRenewal | undefined
renewalStatus: string
type: CoreTimeTypes

View File

@ -119,7 +119,7 @@ function useCoretimeInformationImpl (api: ApiPromise, ready: boolean): CoretimeI
}, [workloads, coreInfos]);
useEffect((): void => {
if (!workloadData?.length || !reservations?.length) {
if (!workloadData?.length || !reservations?.length || !coretimeConstants) {
return;
}
@ -135,9 +135,15 @@ function useCoretimeInformationImpl (api: ApiPromise, ready: boolean): CoretimeI
// parachain can be renewed on a different core
const workplan = workplans?.filter((workplan) => workplan.info.task.toString() === taskId);
const type = getOccupancyType(lease, reservation, workload?.info.isPool ?? false);
const potentialRenewal = potentialRenewalsCurrentRegion?.find((renewal) => renewal.task.toString() === taskId);
let renewalStatus = potentialRenewal ? ChainRenewalStatus.Eligible : ChainRenewalStatus.None;
const chainRegionEnd = (renewalStatus === ChainRenewalStatus.Renewed ? salesInfo?.regionEnd : salesInfo?.regionBegin);
const targetTimeslice = lease?.until || chainRegionEnd;
const lastBlock = targetTimeslice ? targetTimeslice * coretimeConstants?.relay.blocksPerTimeslice : 0;
const chainRenewedCore = type === CoreTimeTypes['Bulk Coretime'] && !!workplan?.length;
if (chainRenewedCore) {
@ -146,6 +152,7 @@ function useCoretimeInformationImpl (api: ApiPromise, ready: boolean): CoretimeI
return {
chainRenewedCore,
lastBlock,
renewal: potentialRenewal,
renewalStatus,
type,