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[];
}