Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
25 changes: 24 additions & 1 deletion packages/apps-config/src/links/subscan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand Down
126 changes: 126 additions & 0 deletions packages/page-coretime/src/Overview/Filters.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions packages/page-coretime/src/Overview/filters/index.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 56 additions & 0 deletions packages/page-coretime/src/Overview/filters/useBlockSort.tsx
Original file line number Diff line number Diff line change
@@ -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
};
}
78 changes: 78 additions & 0 deletions packages/page-coretime/src/Overview/filters/useSearchFilter.tsx
Original file line number Diff line number Diff line change
@@ -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
};
}
Loading