diff --git a/src/api/blocks.tsx b/src/api/blocks.tsx index 383c663..8801666 100644 --- a/src/api/blocks.tsx +++ b/src/api/blocks.tsx @@ -165,6 +165,57 @@ export const blocks = { } } + totalCount + } + highSecuritySets: highSecuritySetsConnection( + orderBy: timestamp_DESC + first: $limit + where: { + block: { height_eq: $height } + OR: { block: { hash_eq: $hash } } + } + ) { + edges { + node { + extrinsicHash + timestamp + delay + block { + height + } + who { + id + } + interceptor { + id + } + } + } + + totalCount + } + errorEvents: errorEventsConnection( + orderBy: timestamp_DESC + first: $limit + where: { + block: { height_eq: $height } + OR: { block: { hash_eq: $hash } } + } + ) { + edges { + node { + errorDocs + errorModule + errorName + errorType + extrinsicHash + timestamp + block { + height + } + } + } + totalCount } } diff --git a/src/api/search.tsx b/src/api/search.tsx index 3218ac9..47552a2 100644 --- a/src/api/search.tsx +++ b/src/api/search.tsx @@ -54,6 +54,15 @@ export const search = (fetcher: DataFetcher) => ({ } timestamp } + errorEvents( + limit: $limit + where: { + errorType_containsInsensitive: $keyword + OR: { errorName_containsInsensitive: $keyword } + } + ) { + extrinsicHash + } } `; diff --git a/src/components/common/table-columns/BLOCK_ERROR_EVENT_COLUMNS.tsx b/src/components/common/table-columns/BLOCK_ERROR_EVENT_COLUMNS.tsx new file mode 100644 index 0000000..bbeaf7a --- /dev/null +++ b/src/components/common/table-columns/BLOCK_ERROR_EVENT_COLUMNS.tsx @@ -0,0 +1,56 @@ +import { createColumnHelper } from '@tanstack/react-table'; + +import { LinkWithCopy } from '@/components/ui/composites/link-with-copy/LinkWithCopy'; +import { TimestampDisplay } from '@/components/ui/timestamp-display'; +import { RESOURCES } from '@/constants/resources'; +import type { BlockErrorEvent } from '@/schemas'; +import { formatTxAddress } from '@/utils/formatter'; + +const columnHelper = createColumnHelper(); + +export const BLOCK_ERROR_EVENT_COLUMNS = [ + columnHelper.accessor('node.extrinsicHash', { + id: 'extrinsicHash', + header: 'Extrinsic Hash', + cell: (props) => + props.getValue() ? ( + + ) : ( + 'Is not available' + ), + enableSorting: false + }), + columnHelper.accessor('node.block.height', { + id: 'block_height', + header: 'Block', + cell: (props) => ( + + ), + enableSorting: true + }), + columnHelper.accessor('node.timestamp', { + id: 'timestamp', + header: 'Timestamp', + cell: (props) => , + enableSorting: true + }), + columnHelper.accessor('node.errorType', { + id: 'errorType', + header: 'Type', + cell: (props) => props.getValue(), + enableSorting: true + }), + columnHelper.accessor('node.errorName', { + id: 'errorName', + header: 'Name', + cell: (props) => props.getValue() ?? '-', + enableSorting: true + }) +]; diff --git a/src/components/common/table-columns/BLOCK_HIGH_SECURITY_SET_COLUMNS.tsx b/src/components/common/table-columns/BLOCK_HIGH_SECURITY_SET_COLUMNS.tsx new file mode 100644 index 0000000..12da72f --- /dev/null +++ b/src/components/common/table-columns/BLOCK_HIGH_SECURITY_SET_COLUMNS.tsx @@ -0,0 +1,74 @@ +import { createColumnHelper } from '@tanstack/react-table'; + +import { LinkWithCopy } from '@/components/ui/composites/link-with-copy/LinkWithCopy'; +import { TimestampDisplay } from '@/components/ui/timestamp-display'; +import { RESOURCES } from '@/constants/resources'; +import type { BlockHighSecuritySet } from '@/schemas'; +import { formatDuration, formatTxAddress } from '@/utils/formatter'; + +const columnHelper = createColumnHelper(); + +export const BLOCK_HIGH_SECURITY_SET_COLUMNS = [ + columnHelper.accessor('node.extrinsicHash', { + id: 'tx-hash', + header: 'Hash', + cell: (props) => + props.getValue() ? ( + + ) : ( + 'Is not available' + ), + enableSorting: false + }), + columnHelper.accessor('node.block.height', { + id: 'block_height', + header: 'Block', + cell: (props) => ( + + ), + enableSorting: true + }), + columnHelper.accessor('node.timestamp', { + id: 'timestamp', + header: 'Timestamp', + cell: (props) => , + enableSorting: true + }), + columnHelper.accessor('node.who.id', { + id: 'who', + header: 'Beneficiary', + cell: (props) => ( + + ), + enableSorting: false + }), + columnHelper.accessor('node.interceptor.id', { + id: 'interceptor', + header: 'Guardian', + cell: (props) => ( + + ), + enableSorting: false + }), + columnHelper.accessor('node.delay', { + id: 'delay', + header: 'Reversible Time', + cell: (props) => formatDuration(props.getValue()), + enableSorting: true + }) +]; diff --git a/src/components/features/block-details/block-data-tabs/BlockDataTabs.tsx b/src/components/features/block-details/block-data-tabs/BlockDataTabs.tsx index 055b181..c65b2e7 100644 --- a/src/components/features/block-details/block-data-tabs/BlockDataTabs.tsx +++ b/src/components/features/block-details/block-data-tabs/BlockDataTabs.tsx @@ -14,6 +14,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import type { BlockResponse } from '@/schemas'; import { formatOption } from '@/utils/formatter'; +import { BlockErrorEvents } from '../block-error-events/BlockErrorEvents'; +import { BlockHighSecuritySets } from '../block-high-security-sets/BlockHighSecuritySets'; import { BlockReversibleTransactions } from '../block-reversible-transactions/BlockReversibleTransactions'; import { BlockTransactions } from '../block-transactions/BlockTransactions'; @@ -23,7 +25,9 @@ export interface BlockDataTabsProps { const TAB_OPTIONS = { immediate: 'immediate-transactions', - reversible: 'reversible-transactions' + reversible: 'reversible-transactions', + highSecuritySets: 'high-security-sets', + errorEvents: 'error-events' } as const; const TAB_LIST = Object.values(TAB_OPTIONS); @@ -66,6 +70,12 @@ export const BlockDataTabs: React.FC = ({ query }) => { + + + + + + diff --git a/src/components/features/block-details/block-error-events/BlockErrorEvents.tsx b/src/components/features/block-details/block-error-events/BlockErrorEvents.tsx new file mode 100644 index 0000000..dc47822 --- /dev/null +++ b/src/components/features/block-details/block-error-events/BlockErrorEvents.tsx @@ -0,0 +1,40 @@ +import type { QueryResult } from '@apollo/client'; +import { Link } from '@tanstack/react-router'; +import React from 'react'; + +import { Button } from '@/components/ui/button'; +import { DataTable } from '@/components/ui/composites/data-table/DataTable'; +import { ContentContainer } from '@/components/ui/content-container'; +import type { BlockResponse } from '@/schemas'; + +import { useBlockErrorEvents } from './hook'; + +interface Props { + query: QueryResult; +} + +export const BlockErrorEvents: React.FC = ({ query }) => { + const { getStatus, table, error } = useBlockErrorEvents(query); + + return ( + +

Recent Error Events

+ + Error: {error && error.message}

+ }} + /> + + {!query.loading && query.data?.errorEvents.totalCount !== 0 && ( + + )} +
+ ); +}; diff --git a/src/components/features/block-details/block-error-events/hook.tsx b/src/components/features/block-details/block-error-events/hook.tsx new file mode 100644 index 0000000..af7f374 --- /dev/null +++ b/src/components/features/block-details/block-error-events/hook.tsx @@ -0,0 +1,40 @@ +import type { QueryResult } from '@apollo/client'; +import { getCoreRowModel, useReactTable } from '@tanstack/react-table'; +import { useMemo } from 'react'; + +import { BLOCK_ERROR_EVENT_COLUMNS } from '@/components/common/table-columns/BLOCK_ERROR_EVENT_COLUMNS'; +import type { BlockResponse, BlockErrorEvent } from '@/schemas'; + +export const useBlockErrorEvents = (query: QueryResult) => { + const { data, error: fetchError, loading } = query; + const blockColumns = useMemo(() => BLOCK_ERROR_EVENT_COLUMNS, []); + + const table = useReactTable({ + data: data?.errorEvents?.edges ?? [], + columns: blockColumns, + getCoreRowModel: getCoreRowModel(), + enableSorting: false + }); + + const success = !loading && !fetchError; + const error = !loading && fetchError; + + const getStatus = () => { + switch (true) { + case success: + return 'success'; + case !!error: + return 'error'; + case !!loading: + return 'loading'; + default: + return 'idle'; + } + }; + + return { + table, + getStatus, + error + }; +}; diff --git a/src/components/features/block-details/block-high-security-sets/BlockHighSecuritySets.tsx b/src/components/features/block-details/block-high-security-sets/BlockHighSecuritySets.tsx new file mode 100644 index 0000000..ecf530b --- /dev/null +++ b/src/components/features/block-details/block-high-security-sets/BlockHighSecuritySets.tsx @@ -0,0 +1,43 @@ +import type { QueryResult } from '@apollo/client'; +import { Link } from '@tanstack/react-router'; +import React from 'react'; + +import { Button } from '@/components/ui/button'; +import { DataTable } from '@/components/ui/composites/data-table/DataTable'; +import { ContentContainer } from '@/components/ui/content-container'; +import type { BlockResponse } from '@/schemas'; + +import { useBlockHighSecuritySets } from './hook'; + +interface Props { + query: QueryResult; +} + +export const BlockHighSecuritySets: React.FC = ({ query }) => { + const { getStatus, table, error } = useBlockHighSecuritySets(query); + + return ( + +

Recent High Security Sets

+ + Error: {error && error.message}

+ }} + /> + + {!query.loading && query.data?.highSecuritySets.totalCount !== 0 && ( + + )} +
+ ); +}; diff --git a/src/components/features/block-details/block-high-security-sets/hook.tsx b/src/components/features/block-details/block-high-security-sets/hook.tsx new file mode 100644 index 0000000..b0a6187 --- /dev/null +++ b/src/components/features/block-details/block-high-security-sets/hook.tsx @@ -0,0 +1,40 @@ +import type { QueryResult } from '@apollo/client'; +import { getCoreRowModel, useReactTable } from '@tanstack/react-table'; +import { useMemo } from 'react'; + +import { BLOCK_HIGH_SECURITY_SET_COLUMNS } from '@/components/common/table-columns/BLOCK_HIGH_SECURITY_SET_COLUMNS'; +import type { BlockResponse, BlockHighSecuritySet } from '@/schemas'; + +export const useBlockHighSecuritySets = (query: QueryResult) => { + const { data, error: fetchError, loading } = query; + const blockColumns = useMemo(() => BLOCK_HIGH_SECURITY_SET_COLUMNS, []); + + const table = useReactTable({ + data: data?.highSecuritySets?.edges ?? [], + columns: blockColumns, + getCoreRowModel: getCoreRowModel(), + enableSorting: false + }); + + const success = !loading && !fetchError; + const error = !loading && fetchError; + + const getStatus = () => { + switch (true) { + case success: + return 'success'; + case !!error: + return 'error'; + case !!loading: + return 'loading'; + default: + return 'idle'; + } + }; + + return { + table, + getStatus, + error + }; +}; diff --git a/src/components/features/landing/hero/Hero.tsx b/src/components/features/landing/hero/Hero.tsx index fc8874a..14f2077 100644 --- a/src/components/features/landing/hero/Hero.tsx +++ b/src/components/features/landing/hero/Hero.tsx @@ -18,7 +18,8 @@ export const Hero = (props: HeroProps) => { inputRef, searchError, searchLoading, - searchResult + searchResult, + handleClosePreview } = useHero(); return ( @@ -40,13 +41,14 @@ export const Hero = (props: HeroProps) => { ref={inputRef} onFocus={handleInputFocus} onKeyDown={handleKeyDown} - placeholder="Search by hash, id, or height" + placeholder="Search by hash, id, block height, or error name/type" onKeywordChange={handleKeywordChange} /> {isResultVisible && ( { const api = useApiClient(); + const [searchResult, setSearchResult] = useState(); const [searchLoading, setSearchLoading] = useState(false); const [searchError, setSearchError] = useState(); @@ -18,6 +19,10 @@ export const useHero = () => { setIsResultVisible(false) ); + const handleClosePreview = () => { + setIsResultVisible(false); + }; + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { e.currentTarget.blur(); @@ -55,6 +60,7 @@ export const useHero = () => { isResultVisible, resultRef, inputRef, + handleClosePreview, handleKeywordChange, handleKeyDown, handleInputFocus, diff --git a/src/components/layout/header/Topbar.tsx b/src/components/layout/header/Topbar.tsx index 0c969f7..3c49321 100644 --- a/src/components/layout/header/Topbar.tsx +++ b/src/components/layout/header/Topbar.tsx @@ -16,6 +16,7 @@ export interface TopbarProps { ) => void; handleInputFocus: () => void; handleKeyDown: (e: React.KeyboardEvent) => void; + handleClosePreview: () => void; searchError?: string; searchLoading: boolean; searchResult?: SearchAllResponse; @@ -28,6 +29,7 @@ export const Topbar: React.FC = ({ handleKeywordChange, handleInputFocus, handleKeyDown, + handleClosePreview, searchError, searchLoading, @@ -56,7 +58,7 @@ export const Topbar: React.FC = ({ ref={inputRef} onFocus={handleInputFocus} onKeyDown={handleKeyDown} - placeholder="Search by hash, id, or height" + placeholder="Search by hash, id, block height, or error name/type" onKeywordChange={handleKeywordChange} inputClassName="h-8" buttonClassName="size-7" @@ -69,6 +71,7 @@ export const Topbar: React.FC = ({ searchResult={searchResult} isLoading={searchLoading} error={searchError} + handleClosePreview={handleClosePreview} /> )} diff --git a/src/components/layout/header/hook.tsx b/src/components/layout/header/hook.tsx index d5d7a4c..a956070 100644 --- a/src/components/layout/header/hook.tsx +++ b/src/components/layout/header/hook.tsx @@ -22,6 +22,10 @@ export const useHeader = () => { setIsResultVisible(false) ); + const handleClosePreview = () => { + setIsResultVisible(false); + }; + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { e.currentTarget.blur(); @@ -59,6 +63,7 @@ export const useHeader = () => { isResultVisible, resultRef, inputRef, + handleClosePreview, handleKeywordChange, handleKeyDown, handleInputFocus, diff --git a/src/components/ui/composites/search-preview/SearchPreview.tsx b/src/components/ui/composites/search-preview/SearchPreview.tsx index 83467e6..2755007 100644 --- a/src/components/ui/composites/search-preview/SearchPreview.tsx +++ b/src/components/ui/composites/search-preview/SearchPreview.tsx @@ -95,17 +95,19 @@ interface SearchPreviewProps searchResult?: SearchAllResponse; isLoading: boolean; error?: string; + handleClosePreview: () => void; } export const SearchPreview = forwardRef( - ({ isLoading, error, searchResult, onKeyDown }, ref) => { + ({ isLoading, error, searchResult, onKeyDown, handleClosePreview }, ref) => { const { accounts, transactions, blocks, reversibleTransactions, minerRewards, - highSecuritySets + highSecuritySets, + errorEvents } = searchResult || {}; if ( @@ -115,11 +117,119 @@ export const SearchPreview = forwardRef( !minerRewards && !accounts && !reversibleTransactions && - !highSecuritySets + !highSecuritySets && + !errorEvents ) { return null; } + // Define all sections with their configuration + const sections = [ + { + title: 'Transactions', + emptyMsg: 'No transactions found.', + items: transactions, + renderItem: (tx: any) => ( + + ) + }, + { + title: 'Reversible Transactions', + emptyMsg: 'No reversible transactions found.', + items: reversibleTransactions, + renderItem: (tx: any) => ( + + ) + }, + { + title: 'Accounts', + emptyMsg: 'No accounts found.', + items: accounts, + renderItem: (acc: any) => ( + + ) + }, + { + title: 'Blocks', + emptyMsg: 'No blocks found.', + items: blocks, + renderItem: (block: any) => ( + + ) + }, + { + title: 'Miner Rewards', + emptyMsg: 'No miner rewards found.', + items: minerRewards, + renderItem: (minerReward: any) => ( + + ) + }, + { + title: 'High Security Sets', + emptyMsg: 'No high security sets found.', + items: highSecuritySets, + renderItem: (highSecuritySet: any) => ( + + ) + }, + { + title: 'Error Events', + emptyMsg: 'No error events found.', + items: errorEvents, + renderItem: (errorEvent: any) => ( + + ) + } + ]; + + // Sort sections: resources with results first, then resources with no results + const sortedSections = [...sections].sort((a, b) => { + const aHasResults = a.items && a.items.length > 0; + const bHasResults = b.items && b.items.length > 0; + + // If both have results or both don't have results, maintain original order + if (aHasResults === bHasResults) { + return 0; + } + + // If a has results and b doesn't, a comes first + if (aHasResults && !bHasResults) { + return -1; + } + + // If b has results and a doesn't, b comes first + return 1; + }); + // Accessibility: aria attributes return (
( > - {/* Transactions */} -
( - - )} - /> - - {/* Reversible Transactions */} -
( - - )} - /> - - {/* Accounts */} -
( - - )} - /> - - {/* Blocks */} -
( - - )} - /> - - {/* Miner Rewards */} -
( - - )} - /> - - {/* High Security Sets */} -
( - - )} - /> + {sortedSections.map((section) => ( +
+ ))}
diff --git a/src/schemas/blocks.ts b/src/schemas/blocks.ts index 73b3bc6..f36a8e7 100644 --- a/src/schemas/blocks.ts +++ b/src/schemas/blocks.ts @@ -1,4 +1,6 @@ import type * as gql from '../__generated__/graphql'; +import type { ErrorEvent } from './errors'; +import type { HighSecuritySet } from './high-security-set'; import type { MinerReward } from './miner-reward'; import type { ReversibleTransaction } from './reversible-transaction'; import type { Transaction } from './transcation'; @@ -19,6 +21,16 @@ export interface BlockResponse { /** @description the grand total of the transactions regardless of the return node limit using `first` parameter */ totalCount: number; }; + highSecuritySets: { + edges: BlockHighSecuritySet[]; + /** @description the grand total of the transactions regardless of the return node limit using `first` parameter */ + totalCount: number; + }; + errorEvents: { + edges: BlockErrorEvent[]; + /** @description the grand total of the transactions regardless of the return node limit using `first` parameter */ + totalCount: number; + }; } export interface BlockListResponse { @@ -39,3 +51,11 @@ export interface BlockTransaction { export interface BlockReversibleTransaction { node: ReversibleTransaction; } + +export interface BlockHighSecuritySet { + node: HighSecuritySet; +} + +export interface BlockErrorEvent { + node: ErrorEvent; +} diff --git a/src/schemas/searchs.ts b/src/schemas/searchs.ts index e4ac172..68e75b5 100644 --- a/src/schemas/searchs.ts +++ b/src/schemas/searchs.ts @@ -1,5 +1,6 @@ import type { Account } from './account'; import type { Block } from './blocks'; +import type { ErrorEvent } from './errors'; import type { HighSecuritySet } from './high-security-set'; import type { MinerReward } from './miner-reward'; import type { ReversibleTransaction } from './reversible-transaction'; @@ -12,4 +13,5 @@ export interface SearchAllResponse { blocks: Pick[]; highSecuritySets: Pick[]; minerRewards: Pick[]; + errorEvents: Pick[]; }