From c47756a27d93dc6914402021a297c2144bd05263 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 21 Jan 2026 16:40:59 +0800 Subject: [PATCH 1/7] feat: add errors event api --- src/api/errors.tsx | 164 ++++++++++++++++++++++++++++ src/api/index.tsx | 2 + src/constants/query-sorts/errors.ts | 41 +++++++ src/constants/query-sorts/index.ts | 1 + src/schemas/errors.ts | 29 +++++ src/schemas/index.ts | 1 + 6 files changed, 238 insertions(+) create mode 100644 src/api/errors.tsx create mode 100644 src/constants/query-sorts/errors.ts create mode 100644 src/schemas/errors.ts diff --git a/src/api/errors.tsx b/src/api/errors.tsx new file mode 100644 index 0000000..77df8d6 --- /dev/null +++ b/src/api/errors.tsx @@ -0,0 +1,164 @@ +import type { QueryHookOptions } from '@apollo/client'; +import { gql, useQuery } from '@apollo/client'; +import { endOfToday } from 'date-fns/endOfToday'; +import { startOfToday } from 'date-fns/startOfToday'; + +import type { ErrorEventWhereInput } from '@/__generated__/graphql'; +import { QUERY_DEFAULT_LIMIT } from '@/constants/query-default-limit'; +import { QUERY_RECENT_LIMIT } from '@/constants/query-recent-limit'; +import type { ErrorEventSorts } from '@/constants/query-sorts'; +import { ERROR_EVENT_SORTS } from '@/constants/query-sorts'; +import type { + ErrorEventListResponse, + ErrorEventResponse, + ErrorEventsStatsResponse, + RecentErrorEventsResponse +} from '@/schemas'; +import type { PaginatedQueryVariables } from '@/types/query'; + +export const errors = { + useGetAll: ( + config?: QueryHookOptions< + ErrorEventListResponse, + PaginatedQueryVariables + > + ) => { + const GET_ERROR_EVENTS = gql` + query GetErrorEvents( + $limit: Int + $offset: Int + $orderBy: [ErrorEventOrderByInput!] + $where: ErrorEventWhereInput + ) { + errorEvents( + limit: $limit + offset: $offset + orderBy: $orderBy + where: $where + ) { + errorDocs + errorModule + errorName + errorType + extrinsicHash + id + timestamp + block { + height + } + } + meta: errorEventsConnection(orderBy: id_ASC, where: $where) { + totalCount + } + } + `; + + return useQuery< + ErrorEventListResponse, + PaginatedQueryVariables + >(GET_ERROR_EVENTS, { + ...config, + variables: { + orderBy: config?.variables?.orderBy ?? ERROR_EVENT_SORTS.timestamp.DESC, + limit: config?.variables?.limit ?? QUERY_DEFAULT_LIMIT, + offset: config?.variables?.offset ?? 0, + where: config?.variables?.where + } + }); + }, + useGetRecent: ( + config?: Omit, 'variables'> + ) => { + const GET_RECENT_ERROR_EVENTS = gql` + query GetRecentErrorEvents( + $limit: Int + $offset: Int + $orderBy: [ErrorEventOrderByInput!] + $where: ErrorEventWhereInput + ) { + errorEvents( + limit: $limit + offset: $offset + orderBy: $orderBy + where: $where + ) { + errorDocs + errorModule + errorName + errorType + extrinsicHash + id + timestamp + block { + height + } + } + } + `; + + return useQuery< + RecentErrorEventsResponse, + PaginatedQueryVariables + >(GET_RECENT_ERROR_EVENTS, { + ...config, + variables: { + orderBy: ERROR_EVENT_SORTS.timestamp.DESC, + limit: QUERY_RECENT_LIMIT + } + }); + }, + useGetStats: ( + config?: Omit, 'variables'> + ) => { + const startDate = startOfToday().toISOString(); + const endDate = endOfToday().toISOString(); + + const GET_ERROR_EVENTS_STATS = gql` + query GetErrorEventsStats($startDate: DateTime!, $endDate: DateTime!) { + last24Hour: errorEventsConnection( + orderBy: id_ASC + where: { timestamp_gte: $startDate, timestamp_lte: $endDate } + ) { + totalCount + } + allTime: errorEventsConnection(orderBy: id_ASC) { + totalCount + } + } + `; + + return useQuery(GET_ERROR_EVENTS_STATS, { + ...config, + variables: { + startDate, + endDate + } + }); + }, + getById: () => { + const GET_ERROR_EVENT_BY_ID = gql` + query GetErrorEventById($id: String!) { + errorEvent: errorEventById(id: $id) { + errorDocs + errorModule + errorName + errorType + extrinsicHash + id + timestamp + block { + height + } + } + } + `; + + return { + useQuery: (id: string, config?: QueryHookOptions) => + useQuery(GET_ERROR_EVENT_BY_ID, { + ...config, + variables: { id } + }) + }; + } +}; diff --git a/src/api/index.tsx b/src/api/index.tsx index c5d78a8..059ed5a 100644 --- a/src/api/index.tsx +++ b/src/api/index.tsx @@ -3,6 +3,7 @@ import { useFetchClient } from '@/hooks/useFetchClient'; import { accounts } from './accounts'; import { blocks } from './blocks'; import { chainStatus } from './chain-status'; +import { errors } from './errors'; import { highSecuritySets } from './high-security-sets'; import { minerLeaderboard } from './miner-leaderboard'; import { minerRewards } from './miner-rewards'; @@ -16,6 +17,7 @@ const useApiClient = () => { const api = { accounts, chainStatus, + errors, transactions, reversibleTransactions, search: search(fetcher), diff --git a/src/constants/query-sorts/errors.ts b/src/constants/query-sorts/errors.ts new file mode 100644 index 0000000..76f5cdf --- /dev/null +++ b/src/constants/query-sorts/errors.ts @@ -0,0 +1,41 @@ +export const ERROR_EVENT_SORTS = { + id: { + ASC: 'id_ASC', + DESC: 'id_DESC' + }, + timestamp: { + ASC: 'timestamp_ASC', + DESC: 'timestamp_DESC' + }, + errorType: { + ASC: 'errorType_ASC', + DESC: 'errorType_DESC' + }, + errorModule: { + ASC: 'errorModule_ASC', + DESC: 'errorModule_DESC' + }, + errorName: { + ASC: 'errorName_ASC', + DESC: 'errorName_DESC' + }, + extrinsicHash: { + ASC: 'extrinsicHash_ASC', + DESC: 'extrinsicHash_DESC' + }, + blockHeight: { + ASC: 'block_height_ASC', + DESC: 'block_height_DESC' + } +} as const; + +export const ERROR_EVENT_SORTS_LITERALS = Object.values( + ERROR_EVENT_SORTS +).flatMap((sort) => Object.values(sort)); + +export const ERROR_EVENT_SORTS_KEY = Object.keys(ERROR_EVENT_SORTS); + +// Adding null, because we can sort by null to indicate no sorting +export type ErrorEventSorts = + | (typeof ERROR_EVENT_SORTS_LITERALS)[number] + | null; diff --git a/src/constants/query-sorts/index.ts b/src/constants/query-sorts/index.ts index 20ff909..c356fe1 100644 --- a/src/constants/query-sorts/index.ts +++ b/src/constants/query-sorts/index.ts @@ -1,5 +1,6 @@ export * from './accounts'; export * from './blocks'; +export * from './errors'; export * from './high-security-sets'; export * from './miner-rewards'; export * from './reversible-transactions'; diff --git a/src/schemas/errors.ts b/src/schemas/errors.ts new file mode 100644 index 0000000..b2abe6e --- /dev/null +++ b/src/schemas/errors.ts @@ -0,0 +1,29 @@ +import type * as gql from '../__generated__/graphql'; + +export interface ErrorEvent extends Omit { + block: Pick; +} + +export interface ErrorEventResponse { + errorEvent?: ErrorEvent | null; +} + +export interface ErrorEventListResponse { + errorEvents: ErrorEvent[]; + meta: { + totalCount: number; + }; +} + +export interface RecentErrorEventsResponse { + errorEvents: ErrorEvent[]; +} + +export interface ErrorEventsStatsResponse { + allTime: { + totalCount: number; + }; + last24Hour: { + totalCount: number; + }; +} diff --git a/src/schemas/index.ts b/src/schemas/index.ts index acb05d8..c305e67 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -1,6 +1,7 @@ export * from './account'; export * from './blocks'; export * from './chain-status'; +export * from './errors'; export * from './high-security-set'; export * from './miner-leaderboard'; export * from './miner-reward'; From ce0e49242e992e00f76335b0e43847b90cc90c38 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 21 Jan 2026 16:52:12 +0800 Subject: [PATCH 2/7] feat: add error events listing --- .../table-columns/ERROR_EVENT_COLUMNS.tsx | 56 +++++++ .../ErrorEventsHeading.tsx | 25 ++++ .../error-events-stats/ErrorEventsStats.tsx | 54 +++++++ .../error-events-table/ErrorEventsTable.tsx | 20 +++ .../error-events-table/hook.tsx | 141 ++++++++++++++++++ src/config/site-navigations.ts | 5 + src/routes/errors/index.tsx | 27 ++++ 7 files changed, 328 insertions(+) create mode 100644 src/components/common/table-columns/ERROR_EVENT_COLUMNS.tsx create mode 100644 src/components/features/error-events-listing/error-events-heading/ErrorEventsHeading.tsx create mode 100644 src/components/features/error-events-listing/error-events-stats/ErrorEventsStats.tsx create mode 100644 src/components/features/error-events-listing/error-events-table/ErrorEventsTable.tsx create mode 100644 src/components/features/error-events-listing/error-events-table/hook.tsx create mode 100644 src/routes/errors/index.tsx diff --git a/src/components/common/table-columns/ERROR_EVENT_COLUMNS.tsx b/src/components/common/table-columns/ERROR_EVENT_COLUMNS.tsx new file mode 100644 index 0000000..830f26e --- /dev/null +++ b/src/components/common/table-columns/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 { ErrorEvent } from '@/schemas'; +import { formatTxAddress } from '@/utils/formatter'; + +const columnHelper = createColumnHelper(); + +export const ERROR_EVENT_COLUMNS = [ + columnHelper.accessor('extrinsicHash', { + id: 'extrinsicHash', + header: 'Extrinsic Hash', + cell: (props) => + props.getValue() ? ( + + ) : ( + 'Is not available' + ), + enableSorting: false + }), + columnHelper.accessor('block.height', { + id: 'block_height', + header: 'Block', + cell: (props) => ( + + ), + enableSorting: true + }), + columnHelper.accessor('timestamp', { + id: 'timestamp', + header: 'Timestamp', + cell: (props) => , + enableSorting: true + }), + columnHelper.accessor('errorType', { + id: 'errorType', + header: 'Type', + cell: (props) => props.getValue(), + enableSorting: true + }), + columnHelper.accessor('errorName', { + id: 'errorName', + header: 'Name', + cell: (props) => props.getValue() ?? '-', + enableSorting: true + }) +]; diff --git a/src/components/features/error-events-listing/error-events-heading/ErrorEventsHeading.tsx b/src/components/features/error-events-listing/error-events-heading/ErrorEventsHeading.tsx new file mode 100644 index 0000000..2b948af --- /dev/null +++ b/src/components/features/error-events-listing/error-events-heading/ErrorEventsHeading.tsx @@ -0,0 +1,25 @@ +import { Link, useSearch } from '@tanstack/react-router'; +import * as React from 'react'; + +export interface ErrorEventsHeadingProps {} + +export const ErrorEventsHeading: React.FC = () => { + const { block } = useSearch({ + strict: false + }) as any; + + return ( +
+

Error Events

+ + {block && ( +
+ In block + + {block} + +
+ )} +
+ ); +}; diff --git a/src/components/features/error-events-listing/error-events-stats/ErrorEventsStats.tsx b/src/components/features/error-events-listing/error-events-stats/ErrorEventsStats.tsx new file mode 100644 index 0000000..dd7281c --- /dev/null +++ b/src/components/features/error-events-listing/error-events-stats/ErrorEventsStats.tsx @@ -0,0 +1,54 @@ +import { useSearch } from '@tanstack/react-router'; +import React from 'react'; + +import useApiClient from '@/api'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; +import { DATA_POOL_INTERVAL } from '@/constants/data-pool-interval'; + +export interface ErrorEventsStatsProps {} + +export const ErrorEventsStats: React.FC = () => { + const api = useApiClient(); + const { block } = useSearch({ + strict: false + }) as any; + + if (block) return null; + + const { loading, data, error } = api.errors.useGetStats({ + pollInterval: DATA_POOL_INTERVAL + }); + + const success = !loading && !error; + + return ( +
+ + + +

Total Error Events

+
+
+ + {success &&

{data?.allTime.totalCount}

} + {loading && } + {error &&

Error: {error.message}

} +
+
+ + + + +

Recent Error Events (24H)

+
+
+ + {success &&

{data?.last24Hour.totalCount}

} + {loading && } + {error &&

Error: {error.message}

} +
+
+
+ ); +}; diff --git a/src/components/features/error-events-listing/error-events-table/ErrorEventsTable.tsx b/src/components/features/error-events-listing/error-events-table/ErrorEventsTable.tsx new file mode 100644 index 0000000..6c39f51 --- /dev/null +++ b/src/components/features/error-events-listing/error-events-table/ErrorEventsTable.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import { DataTable } from '@/components/ui/composites/data-table/DataTable'; + +import { useErrorEventsTable } from './hook'; + +export const ErrorEventsTable = () => { + const { getStatus, table, error } = useErrorEventsTable(); + + return ( + Error: {error && error.message}

+ }} + withControls + /> + ); +}; diff --git a/src/components/features/error-events-listing/error-events-table/hook.tsx b/src/components/features/error-events-listing/error-events-table/hook.tsx new file mode 100644 index 0000000..f28f75e --- /dev/null +++ b/src/components/features/error-events-listing/error-events-table/hook.tsx @@ -0,0 +1,141 @@ +import { useSearch } from '@tanstack/react-router'; +import type { + OnChangeFn, + PaginationState, + SortingState +} from '@tanstack/react-table'; +import { getCoreRowModel, useReactTable } from '@tanstack/react-table'; +import { parseAsInteger, parseAsStringLiteral, useQueryState } from 'nuqs'; +import { useEffect, useMemo, useState } from 'react'; + +import useApiClient from '@/api'; +import { ERROR_EVENT_COLUMNS } from '@/components/common/table-columns/ERROR_EVENT_COLUMNS'; +import { DATA_POOL_INTERVAL } from '@/constants/data-pool-interval'; +import { QUERY_DEFAULT_LIMIT } from '@/constants/query-default-limit'; +import type { ErrorEventSorts } from '@/constants/query-sorts'; +import { ERROR_EVENT_SORTS_LITERALS } from '@/constants/query-sorts'; +import type { ErrorEvent } from '@/schemas'; +import { transformSortLiteral } from '@/utils/transform-sort'; + +export const useErrorEventsTable = () => { + const api = useApiClient(); + const { block } = useSearch({ + strict: false + }) as any; + + const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1)); + const [limit, setLimit] = useQueryState( + 'limit', + parseAsInteger.withDefault(QUERY_DEFAULT_LIMIT) + ); + const [sortBy, setSortBy] = useQueryState( + 'sortBy', + parseAsStringLiteral(ERROR_EVENT_SORTS_LITERALS) + ); + + const currentPageIndex = page - 1; + + const sortingValue: SortingState = transformSortLiteral(sortBy); + const paginationValue: PaginationState = { + pageSize: limit, + pageIndex: currentPageIndex + }; + + const handleChangeSorting: OnChangeFn = (sorting) => { + if (typeof sorting === 'function') { + const newValue = sorting(sortingValue); + + if (newValue[0]?.id) { + const key = newValue[0].id; + const order = newValue[0].desc ? 'DESC' : 'ASC'; + + const newSortBy = `${key}_${order}` as ErrorEventSorts; + + setSortBy(newSortBy); + } else { + setSortBy(null); + } + } else if (sorting[0]?.id) { + const key = sorting[0].id; + const order = sorting[0].desc ? 'DESC' : 'ASC'; + + const newSortBy = `${key}_${order}` as ErrorEventSorts; + + setSortBy(newSortBy); + } + }; + + const handleChangePagination: OnChangeFn = (pagination) => { + if (typeof pagination === 'function') { + const newPagination = pagination(paginationValue); + + setPage(newPagination.pageIndex + 1); + setLimit(newPagination.pageSize); + } else { + setPage(pagination.pageIndex + 1); + setLimit(pagination.pageSize); + } + }; + + const { + loading, + data, + error: fetchError + } = api.errors.useGetAll({ + pollInterval: DATA_POOL_INTERVAL, + variables: { + orderBy: sortBy, + limit, + offset: currentPageIndex * limit, + ...(block && { + where: { + block: { height_eq: Number(block) } + } + }) + } + }); + + const errorEventColumns = useMemo(() => ERROR_EVENT_COLUMNS, []); + const [rowCount, setRowCount] = useState(data?.meta.totalCount ?? 0); + + const table = useReactTable({ + data: data?.errorEvents ?? [], + columns: errorEventColumns, + getCoreRowModel: getCoreRowModel(), + state: { + sorting: sortingValue, + pagination: paginationValue + }, + rowCount, + onSortingChange: handleChangeSorting, + onPaginationChange: handleChangePagination, + manualSorting: true, + manualPagination: true + }); + + 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'; + } + }; + + useEffect(() => { + if (!loading && data?.meta.totalCount) setRowCount(data.meta.totalCount); + }, [loading, data?.meta.totalCount]); + + return { + table, + getStatus, + error + }; +}; diff --git a/src/config/site-navigations.ts b/src/config/site-navigations.ts index c43d95c..017d0bb 100644 --- a/src/config/site-navigations.ts +++ b/src/config/site-navigations.ts @@ -60,5 +60,10 @@ export const SITE_NAVIGATIONS: (SiteNavigation | ParentNavigation)[] = [ label: 'Blocks', path: '/blocks', longLabel: 'Blocks' + }, + { + label: 'Errors', + path: '/errors', + longLabel: 'Error Events' } ] as const; diff --git a/src/routes/errors/index.tsx b/src/routes/errors/index.tsx new file mode 100644 index 0000000..ee573f4 --- /dev/null +++ b/src/routes/errors/index.tsx @@ -0,0 +1,27 @@ +import { createFileRoute } from '@tanstack/react-router'; + +import { ErrorEventsHeading } from '@/components/features/error-events-listing/error-events-heading/ErrorEventsHeading'; +import { ErrorEventsStats } from '@/components/features/error-events-listing/error-events-stats/ErrorEventsStats'; +import { ErrorEventsTable } from '@/components/features/error-events-listing/error-events-table/ErrorEventsTable'; +import { ContentContainer } from '@/components/ui/content-container'; +import { SectionContainer } from '@/components/ui/section-container'; + +export const Route = createFileRoute('/errors/')({ + component: ErrorEvents +}); + +function ErrorEvents() { + return ( + + + + + + + + + + ); +} + +export default ErrorEvents; From 63a1699e7e053eb37b8d6c4c3c79700532130c42 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 21 Jan 2026 17:53:13 +0800 Subject: [PATCH 3/7] feat: add error event details --- .../ErrorEventInformation.tsx | 108 ++++++++++++++++++ src/constants/resources.ts | 3 +- src/routeTree.gen.ts | 42 +++++++ src/routes/errors/$id.tsx | 26 +++++ 4 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 src/components/features/error-event-details/error-event-information/ErrorEventInformation.tsx create mode 100644 src/routes/errors/$id.tsx diff --git a/src/components/features/error-event-details/error-event-information/ErrorEventInformation.tsx b/src/components/features/error-event-details/error-event-information/ErrorEventInformation.tsx new file mode 100644 index 0000000..99affd6 --- /dev/null +++ b/src/components/features/error-event-details/error-event-information/ErrorEventInformation.tsx @@ -0,0 +1,108 @@ +import { notFound } from '@tanstack/react-router'; +import * as React from 'react'; + +import useApiClient from '@/api'; +import { DataList } from '@/components/ui/composites/data-list/DataList'; +import { LinkWithCopy } from '@/components/ui/composites/link-with-copy/LinkWithCopy'; +import { TextWithCopy } from '@/components/ui/composites/text-with-copy/TextWithCopy'; +import { RESOURCES } from '@/constants/resources'; +import type { ErrorEvent } from '@/schemas'; +import { formatTimestamp } from '@/utils/formatter'; + +export interface ErrorEventInformationProps { + id: string; +} + +export const ErrorEventInformation: React.FC = ({ + id +}) => { + const api = useApiClient(); + const { data, loading } = api.errors.getById().useQuery(id); + + if (!loading && (!data || !data.errorEvent)) throw notFound(); + + const event = data?.errorEvent ?? undefined; + + const information: Partial[] = [ + { + id: event?.id, + timestamp: event?.timestamp, + block: event?.block, + extrinsicHash: event?.extrinsicHash, + errorType: event?.errorType, + errorModule: event?.errorModule, + errorName: event?.errorName, + errorDocs: event?.errorDocs + } + ]; + + return ( + > + loading={loading} + data={information} + fields={[ + { + label: 'Error Event ID', + key: 'id', + render: (value) => ( + + ) + }, + { + label: 'Timestamp', + key: 'timestamp', + render: (value) => formatTimestamp(value, true) + }, + { + label: 'Block', + key: 'block', + render: (value) => ( + + ) + }, + { + label: 'Extrinsic Hash', + key: 'extrinsicHash', + render: (value) => + value ? ( + + ) : ( + 'Is not available' + ) + }, + { + label: 'Error Type', + key: 'errorType' + }, + { + label: 'Error Module', + key: 'errorModule', + render: (value) => (value ? (value as string) : '-') + }, + { + label: 'Error Name', + key: 'errorName', + render: (value) => (value ? (value as string) : '-') + }, + { + label: 'Error Docs', + key: 'errorDocs', + render: (value) => + value ? ( + + ) : ( + '-' + ) + } + ]} + /> + ); +}; diff --git a/src/constants/resources.ts b/src/constants/resources.ts index b34e986..b209911 100644 --- a/src/constants/resources.ts +++ b/src/constants/resources.ts @@ -4,5 +4,6 @@ export const RESOURCES = { accounts: '/accounts', blocks: '/blocks', minerRewards: '/miner-rewards', - highSecuritySets: '/high-security-sets' + highSecuritySets: '/high-security-sets', + errors: '/errors' } as const; diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 5ce4779..a98d3f7 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -15,12 +15,14 @@ import { Route as MinerRewardsIndexRouteImport } from './routes/miner-rewards/in import { Route as MinerLeaderboardIndexRouteImport } from './routes/miner-leaderboard/index' import { Route as ImmediateTransactionsIndexRouteImport } from './routes/immediate-transactions/index' import { Route as HighSecuritySetsIndexRouteImport } from './routes/high-security-sets/index' +import { Route as ErrorsIndexRouteImport } from './routes/errors/index' import { Route as BlocksIndexRouteImport } from './routes/blocks/index' import { Route as AccountsIndexRouteImport } from './routes/accounts/index' import { Route as ReversibleTransactionsHashRouteImport } from './routes/reversible-transactions/$hash' import { Route as MinerRewardsHashRouteImport } from './routes/miner-rewards/$hash' import { Route as ImmediateTransactionsHashRouteImport } from './routes/immediate-transactions/$hash' import { Route as HighSecuritySetsHashRouteImport } from './routes/high-security-sets/$hash' +import { Route as ErrorsIdRouteImport } from './routes/errors/$id' import { Route as BlocksIdRouteImport } from './routes/blocks/$id' import { Route as AccountsIdRouteImport } from './routes/accounts/$id' @@ -56,6 +58,11 @@ const HighSecuritySetsIndexRoute = HighSecuritySetsIndexRouteImport.update({ path: '/high-security-sets/', getParentRoute: () => rootRouteImport, } as any) +const ErrorsIndexRoute = ErrorsIndexRouteImport.update({ + id: '/errors/', + path: '/errors/', + getParentRoute: () => rootRouteImport, +} as any) const BlocksIndexRoute = BlocksIndexRouteImport.update({ id: '/blocks/', path: '/blocks/', @@ -88,6 +95,11 @@ const HighSecuritySetsHashRoute = HighSecuritySetsHashRouteImport.update({ path: '/high-security-sets/$hash', getParentRoute: () => rootRouteImport, } as any) +const ErrorsIdRoute = ErrorsIdRouteImport.update({ + id: '/errors/$id', + path: '/errors/$id', + getParentRoute: () => rootRouteImport, +} as any) const BlocksIdRoute = BlocksIdRouteImport.update({ id: '/blocks/$id', path: '/blocks/$id', @@ -103,12 +115,14 @@ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/accounts/$id': typeof AccountsIdRoute '/blocks/$id': typeof BlocksIdRoute + '/errors/$id': typeof ErrorsIdRoute '/high-security-sets/$hash': typeof HighSecuritySetsHashRoute '/immediate-transactions/$hash': typeof ImmediateTransactionsHashRoute '/miner-rewards/$hash': typeof MinerRewardsHashRoute '/reversible-transactions/$hash': typeof ReversibleTransactionsHashRoute '/accounts': typeof AccountsIndexRoute '/blocks': typeof BlocksIndexRoute + '/errors': typeof ErrorsIndexRoute '/high-security-sets': typeof HighSecuritySetsIndexRoute '/immediate-transactions': typeof ImmediateTransactionsIndexRoute '/miner-leaderboard': typeof MinerLeaderboardIndexRoute @@ -119,12 +133,14 @@ export interface FileRoutesByTo { '/': typeof IndexRoute '/accounts/$id': typeof AccountsIdRoute '/blocks/$id': typeof BlocksIdRoute + '/errors/$id': typeof ErrorsIdRoute '/high-security-sets/$hash': typeof HighSecuritySetsHashRoute '/immediate-transactions/$hash': typeof ImmediateTransactionsHashRoute '/miner-rewards/$hash': typeof MinerRewardsHashRoute '/reversible-transactions/$hash': typeof ReversibleTransactionsHashRoute '/accounts': typeof AccountsIndexRoute '/blocks': typeof BlocksIndexRoute + '/errors': typeof ErrorsIndexRoute '/high-security-sets': typeof HighSecuritySetsIndexRoute '/immediate-transactions': typeof ImmediateTransactionsIndexRoute '/miner-leaderboard': typeof MinerLeaderboardIndexRoute @@ -136,12 +152,14 @@ export interface FileRoutesById { '/': typeof IndexRoute '/accounts/$id': typeof AccountsIdRoute '/blocks/$id': typeof BlocksIdRoute + '/errors/$id': typeof ErrorsIdRoute '/high-security-sets/$hash': typeof HighSecuritySetsHashRoute '/immediate-transactions/$hash': typeof ImmediateTransactionsHashRoute '/miner-rewards/$hash': typeof MinerRewardsHashRoute '/reversible-transactions/$hash': typeof ReversibleTransactionsHashRoute '/accounts/': typeof AccountsIndexRoute '/blocks/': typeof BlocksIndexRoute + '/errors/': typeof ErrorsIndexRoute '/high-security-sets/': typeof HighSecuritySetsIndexRoute '/immediate-transactions/': typeof ImmediateTransactionsIndexRoute '/miner-leaderboard/': typeof MinerLeaderboardIndexRoute @@ -154,12 +172,14 @@ export interface FileRouteTypes { | '/' | '/accounts/$id' | '/blocks/$id' + | '/errors/$id' | '/high-security-sets/$hash' | '/immediate-transactions/$hash' | '/miner-rewards/$hash' | '/reversible-transactions/$hash' | '/accounts' | '/blocks' + | '/errors' | '/high-security-sets' | '/immediate-transactions' | '/miner-leaderboard' @@ -170,12 +190,14 @@ export interface FileRouteTypes { | '/' | '/accounts/$id' | '/blocks/$id' + | '/errors/$id' | '/high-security-sets/$hash' | '/immediate-transactions/$hash' | '/miner-rewards/$hash' | '/reversible-transactions/$hash' | '/accounts' | '/blocks' + | '/errors' | '/high-security-sets' | '/immediate-transactions' | '/miner-leaderboard' @@ -186,12 +208,14 @@ export interface FileRouteTypes { | '/' | '/accounts/$id' | '/blocks/$id' + | '/errors/$id' | '/high-security-sets/$hash' | '/immediate-transactions/$hash' | '/miner-rewards/$hash' | '/reversible-transactions/$hash' | '/accounts/' | '/blocks/' + | '/errors/' | '/high-security-sets/' | '/immediate-transactions/' | '/miner-leaderboard/' @@ -203,12 +227,14 @@ export interface RootRouteChildren { IndexRoute: typeof IndexRoute AccountsIdRoute: typeof AccountsIdRoute BlocksIdRoute: typeof BlocksIdRoute + ErrorsIdRoute: typeof ErrorsIdRoute HighSecuritySetsHashRoute: typeof HighSecuritySetsHashRoute ImmediateTransactionsHashRoute: typeof ImmediateTransactionsHashRoute MinerRewardsHashRoute: typeof MinerRewardsHashRoute ReversibleTransactionsHashRoute: typeof ReversibleTransactionsHashRoute AccountsIndexRoute: typeof AccountsIndexRoute BlocksIndexRoute: typeof BlocksIndexRoute + ErrorsIndexRoute: typeof ErrorsIndexRoute HighSecuritySetsIndexRoute: typeof HighSecuritySetsIndexRoute ImmediateTransactionsIndexRoute: typeof ImmediateTransactionsIndexRoute MinerLeaderboardIndexRoute: typeof MinerLeaderboardIndexRoute @@ -260,6 +286,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof HighSecuritySetsIndexRouteImport parentRoute: typeof rootRouteImport } + '/errors/': { + id: '/errors/' + path: '/errors' + fullPath: '/errors' + preLoaderRoute: typeof ErrorsIndexRouteImport + parentRoute: typeof rootRouteImport + } '/blocks/': { id: '/blocks/' path: '/blocks' @@ -302,6 +335,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof HighSecuritySetsHashRouteImport parentRoute: typeof rootRouteImport } + '/errors/$id': { + id: '/errors/$id' + path: '/errors/$id' + fullPath: '/errors/$id' + preLoaderRoute: typeof ErrorsIdRouteImport + parentRoute: typeof rootRouteImport + } '/blocks/$id': { id: '/blocks/$id' path: '/blocks/$id' @@ -323,12 +363,14 @@ const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, AccountsIdRoute: AccountsIdRoute, BlocksIdRoute: BlocksIdRoute, + ErrorsIdRoute: ErrorsIdRoute, HighSecuritySetsHashRoute: HighSecuritySetsHashRoute, ImmediateTransactionsHashRoute: ImmediateTransactionsHashRoute, MinerRewardsHashRoute: MinerRewardsHashRoute, ReversibleTransactionsHashRoute: ReversibleTransactionsHashRoute, AccountsIndexRoute: AccountsIndexRoute, BlocksIndexRoute: BlocksIndexRoute, + ErrorsIndexRoute: ErrorsIndexRoute, HighSecuritySetsIndexRoute: HighSecuritySetsIndexRoute, ImmediateTransactionsIndexRoute: ImmediateTransactionsIndexRoute, MinerLeaderboardIndexRoute: MinerLeaderboardIndexRoute, diff --git a/src/routes/errors/$id.tsx b/src/routes/errors/$id.tsx new file mode 100644 index 0000000..c96a9fc --- /dev/null +++ b/src/routes/errors/$id.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/react-router'; +import * as React from 'react'; + +import { ErrorEventInformation } from '@/components/features/error-event-details/error-event-information/ErrorEventInformation'; +import { ContentContainer } from '@/components/ui/content-container'; +import { SectionContainer } from '@/components/ui/section-container'; + +export const Route = createFileRoute('/errors/$id')({ + component: ErrorEventDetails +}); + +function ErrorEventDetails() { + const { id } = Route.useParams(); + + return ( + + +

Error Event Details

+ + +
+
+ ); +} + +export default ErrorEventDetails; From df293a03bdcbb2047d28c842bbe162ec73954821 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 21 Jan 2026 18:06:33 +0800 Subject: [PATCH 4/7] fix: error event details integration --- src/api/errors.tsx | 14 +++---- .../table-columns/ERROR_EVENT_COLUMNS.tsx | 2 +- .../ErrorEventInformation.tsx | 38 ++++++++----------- src/config/site-navigations.ts | 19 ++++++---- src/schemas/errors.ts | 2 +- 5 files changed, 36 insertions(+), 39 deletions(-) diff --git a/src/api/errors.tsx b/src/api/errors.tsx index 77df8d6..6d43fb6 100644 --- a/src/api/errors.tsx +++ b/src/api/errors.tsx @@ -135,10 +135,10 @@ export const errors = { } }); }, - getById: () => { - const GET_ERROR_EVENT_BY_ID = gql` - query GetErrorEventById($id: String!) { - errorEvent: errorEventById(id: $id) { + getByHash: () => { + const GET_ERROR_EVENT_BY_HASH = gql` + query GetErrorEventByHash($hash: String!) { + errorEvents: errorEvents(where: { extrinsicHash_eq: $hash }) { errorDocs errorModule errorName @@ -154,10 +154,10 @@ export const errors = { `; return { - useQuery: (id: string, config?: QueryHookOptions) => - useQuery(GET_ERROR_EVENT_BY_ID, { + useQuery: (hash: string, config?: QueryHookOptions) => + useQuery(GET_ERROR_EVENT_BY_HASH, { ...config, - variables: { id } + variables: { hash } }) }; } diff --git a/src/components/common/table-columns/ERROR_EVENT_COLUMNS.tsx b/src/components/common/table-columns/ERROR_EVENT_COLUMNS.tsx index 830f26e..a27c5eb 100644 --- a/src/components/common/table-columns/ERROR_EVENT_COLUMNS.tsx +++ b/src/components/common/table-columns/ERROR_EVENT_COLUMNS.tsx @@ -15,7 +15,7 @@ export const ERROR_EVENT_COLUMNS = [ cell: (props) => props.getValue() ? ( diff --git a/src/components/features/error-event-details/error-event-information/ErrorEventInformation.tsx b/src/components/features/error-event-details/error-event-information/ErrorEventInformation.tsx index 99affd6..9a2e785 100644 --- a/src/components/features/error-event-details/error-event-information/ErrorEventInformation.tsx +++ b/src/components/features/error-event-details/error-event-information/ErrorEventInformation.tsx @@ -17,15 +17,14 @@ export const ErrorEventInformation: React.FC = ({ id }) => { const api = useApiClient(); - const { data, loading } = api.errors.getById().useQuery(id); + const { data, loading } = api.errors.getByHash().useQuery(id); - if (!loading && (!data || !data.errorEvent)) throw notFound(); + if (!loading && (!data || data.errorEvents.length <= 0)) throw notFound(); - const event = data?.errorEvent ?? undefined; + const event = data?.errorEvents[0]; const information: Partial[] = [ { - id: event?.id, timestamp: event?.timestamp, block: event?.block, extrinsicHash: event?.extrinsicHash, @@ -42,11 +41,18 @@ export const ErrorEventInformation: React.FC = ({ data={information} fields={[ { - label: 'Error Event ID', - key: 'id', - render: (value) => ( - - ) + label: 'Extrinsic Hash', + key: 'extrinsicHash', + render: (value) => + value ? ( + + ) : ( + 'Is not available' + ) }, { label: 'Timestamp', @@ -64,20 +70,6 @@ export const ErrorEventInformation: React.FC = ({ /> ) }, - { - label: 'Extrinsic Hash', - key: 'extrinsicHash', - render: (value) => - value ? ( - - ) : ( - 'Is not available' - ) - }, { label: 'Error Type', key: 'errorType' diff --git a/src/config/site-navigations.ts b/src/config/site-navigations.ts index 017d0bb..aac8b88 100644 --- a/src/config/site-navigations.ts +++ b/src/config/site-navigations.ts @@ -58,12 +58,17 @@ export const SITE_NAVIGATIONS: (SiteNavigation | ParentNavigation)[] = [ }, { label: 'Blocks', - path: '/blocks', - longLabel: 'Blocks' - }, - { - label: 'Errors', - path: '/errors', - longLabel: 'Error Events' + children: [ + { + label: 'Blocks', + path: '/blocks', + longLabel: 'Blocks' + }, + { + label: 'Errors', + path: '/errors', + longLabel: 'Error Events' + } + ] } ] as const; diff --git a/src/schemas/errors.ts b/src/schemas/errors.ts index b2abe6e..85da080 100644 --- a/src/schemas/errors.ts +++ b/src/schemas/errors.ts @@ -5,7 +5,7 @@ export interface ErrorEvent extends Omit { } export interface ErrorEventResponse { - errorEvent?: ErrorEvent | null; + errorEvents: [ErrorEvent]; } export interface ErrorEventListResponse { From cf5fadb12f9f0b8a10d5f923c45e61b8fadaabbf Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 21 Jan 2026 18:20:57 +0800 Subject: [PATCH 5/7] feat: add recent error to homepage --- .../features/landing/data-tabs/DataTabs.tsx | 11 ++-- .../error-events-table/ErrorEventsTable.tsx | 17 ++++++ .../landing/error-events-table/hook.tsx | 53 +++++++++++++++++++ .../recent-error-events/RecentErrorEvents.tsx | 21 ++++++++ .../RecentTransactions.tsx | 21 ++++++++ 5 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 src/components/features/landing/error-events-table/ErrorEventsTable.tsx create mode 100644 src/components/features/landing/error-events-table/hook.tsx create mode 100644 src/components/features/landing/recent-error-events/RecentErrorEvents.tsx create mode 100644 src/components/features/landing/recent-error-events/RecentTransactions.tsx diff --git a/src/components/features/landing/data-tabs/DataTabs.tsx b/src/components/features/landing/data-tabs/DataTabs.tsx index f2016c0..a7b9307 100644 --- a/src/components/features/landing/data-tabs/DataTabs.tsx +++ b/src/components/features/landing/data-tabs/DataTabs.tsx @@ -14,6 +14,7 @@ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { formatOption } from '@/utils/formatter'; import { RecentBlocks } from '../recent-blocks/RecentBlocks'; +import { RecentErrorEvents } from '../recent-error-events/RecentErrorEvents'; import { RecentHighSecuritySets } from '../recent-high-security-sets/RecentHighSecuritySets'; import { RecentMinerRewards } from '../recent-miner-rewards/RecentMinerRewards'; import { RecentReversibleTransactions } from '../recent-reversible-transactions/RecentReversibleTransactions'; @@ -26,7 +27,8 @@ const TAB_OPTIONS = { reversible: 'reversible-transactions', blocks: 'blocks', miners: 'miner-rewards', - highSecuritySets: 'high-security-sets' + highSecuritySets: 'high-security-sets', + errors: 'errors' } as const; const TAB_LIST = Object.values(TAB_OPTIONS); @@ -38,7 +40,7 @@ export const DataTabs: React.FC = () => { - + {TAB_LIST.map((val) => ( = () => { + + + diff --git a/src/components/features/landing/error-events-table/ErrorEventsTable.tsx b/src/components/features/landing/error-events-table/ErrorEventsTable.tsx new file mode 100644 index 0000000..316e406 --- /dev/null +++ b/src/components/features/landing/error-events-table/ErrorEventsTable.tsx @@ -0,0 +1,17 @@ +import { DataTable } from '@/components/ui/composites/data-table/DataTable'; + +import { useErrorEventsTable } from './hook'; + +export const ErrorEventsTable = () => { + const { getStatus, table, error } = useErrorEventsTable(); + + return ( + Error : {error && error.message}

+ }} + /> + ); +}; diff --git a/src/components/features/landing/error-events-table/hook.tsx b/src/components/features/landing/error-events-table/hook.tsx new file mode 100644 index 0000000..76e6cf3 --- /dev/null +++ b/src/components/features/landing/error-events-table/hook.tsx @@ -0,0 +1,53 @@ +import { getCoreRowModel, useReactTable } from '@tanstack/react-table'; +import { useMemo } from 'react'; + +import useApiClient from '@/api'; +import { ERROR_EVENT_COLUMNS } from '@/components/common/table-columns/ERROR_EVENT_COLUMNS'; +import { DATA_POOL_INTERVAL } from '@/constants/data-pool-interval'; +import type { ErrorEvent } from '@/schemas'; + +export const useErrorEventsTable = () => { + const api = useApiClient(); + + const { + loading, + data, + error: fetchError + } = api.errors.useGetRecent({ + pollInterval: DATA_POOL_INTERVAL + }); + + const errorEventColumns = useMemo(() => ERROR_EVENT_COLUMNS, []); + + const table = useReactTable({ + data: data?.errorEvents ?? [], + columns: errorEventColumns, + getCoreRowModel: getCoreRowModel(), + enableSorting: false, + rowCount: data?.errorEvents?.length ?? 0, + manualPagination: true, + manualSorting: true + }); + + 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/recent-error-events/RecentErrorEvents.tsx b/src/components/features/landing/recent-error-events/RecentErrorEvents.tsx new file mode 100644 index 0000000..703618f --- /dev/null +++ b/src/components/features/landing/recent-error-events/RecentErrorEvents.tsx @@ -0,0 +1,21 @@ +import { Link } from '@tanstack/react-router'; + +import { Button } from '@/components/ui/button'; +import { ContentContainer } from '@/components/ui/content-container'; +import { RESOURCES } from '@/constants/resources'; + +import { ErrorEventsTable } from '../error-events-table/ErrorEventsTable'; + +export const RecentErrorEvents = () => { + return ( + +

Recent Error Events

+ + + + +
+ ); +}; diff --git a/src/components/features/landing/recent-error-events/RecentTransactions.tsx b/src/components/features/landing/recent-error-events/RecentTransactions.tsx new file mode 100644 index 0000000..4958237 --- /dev/null +++ b/src/components/features/landing/recent-error-events/RecentTransactions.tsx @@ -0,0 +1,21 @@ +import { Link } from '@tanstack/react-router'; + +import { Button } from '@/components/ui/button'; +import { ContentContainer } from '@/components/ui/content-container'; +import { RESOURCES } from '@/constants/resources'; + +import { TransactionsTable } from '../transactions-table/TransactionsTable'; + +export const RecentTransactions = () => { + return ( + +

Recent Immediate Transactions

+ + + + +
+ ); +}; From 99e2d4919fb4f88fd3468fd15534bdaa8fccc15e Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 21 Jan 2026 18:23:44 +0800 Subject: [PATCH 6/7] fix: error event details extrinsic --- .../ErrorEventInformation.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/components/features/error-event-details/error-event-information/ErrorEventInformation.tsx b/src/components/features/error-event-details/error-event-information/ErrorEventInformation.tsx index 9a2e785..fa6ffe7 100644 --- a/src/components/features/error-event-details/error-event-information/ErrorEventInformation.tsx +++ b/src/components/features/error-event-details/error-event-information/ErrorEventInformation.tsx @@ -43,16 +43,9 @@ export const ErrorEventInformation: React.FC = ({ { label: 'Extrinsic Hash', key: 'extrinsicHash', - render: (value) => - value ? ( - - ) : ( - 'Is not available' - ) + render: (value) => ( + + ) }, { label: 'Timestamp', From 7418fd4e8073298e163ab680396ac51352261500 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 21 Jan 2026 18:26:41 +0800 Subject: [PATCH 7/7] chore: update placeholder --- src/components/features/landing/hero/Hero.tsx | 2 +- src/components/layout/header/Topbar.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/features/landing/hero/Hero.tsx b/src/components/features/landing/hero/Hero.tsx index d484e0c..fc8874a 100644 --- a/src/components/features/landing/hero/Hero.tsx +++ b/src/components/features/landing/hero/Hero.tsx @@ -40,7 +40,7 @@ export const Hero = (props: HeroProps) => { ref={inputRef} onFocus={handleInputFocus} onKeyDown={handleKeyDown} - placeholder="Search transaction hash, account id, block number/hash, or miner rewards hash" + placeholder="Search by hash, id, or height" onKeywordChange={handleKeywordChange} /> diff --git a/src/components/layout/header/Topbar.tsx b/src/components/layout/header/Topbar.tsx index be9f6dc..0c969f7 100644 --- a/src/components/layout/header/Topbar.tsx +++ b/src/components/layout/header/Topbar.tsx @@ -56,7 +56,7 @@ export const Topbar: React.FC = ({ ref={inputRef} onFocus={handleInputFocus} onKeyDown={handleKeyDown} - placeholder="Search transaction hash, account id, or block number/hash" + placeholder="Search by hash, id, or height" onKeywordChange={handleKeywordChange} inputClassName="h-8" buttonClassName="size-7"