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:
parent
043e0551b4
commit
c24c90ec8c
@ -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',
|
||||
|
||||
126
packages/page-coretime/src/Overview/Filters.tsx
Normal file
126
packages/page-coretime/src/Overview/Filters.tsx
Normal 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;
|
||||
7
packages/page-coretime/src/Overview/filters/index.ts
Normal file
7
packages/page-coretime/src/Overview/filters/index.ts
Normal 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';
|
||||
56
packages/page-coretime/src/Overview/filters/useBlockSort.tsx
Normal file
56
packages/page-coretime/src/Overview/filters/useBlockSort.tsx
Normal 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
|
||||
};
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
@ -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' });
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -332,6 +332,7 @@ export interface PalletBrokerConfigRecord {
|
||||
}
|
||||
|
||||
export interface ChainWorkTaskInformation {
|
||||
lastBlock: number
|
||||
renewal: PotentialRenewal | undefined
|
||||
renewalStatus: string
|
||||
type: CoreTimeTypes
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user