|
1 | 1 | "use client"; |
2 | 2 |
|
| 3 | +import { Paginator } from "@pythnetwork/component-library/Paginator"; |
3 | 4 | import { SearchInput } from "@pythnetwork/component-library/SearchInput"; |
4 | | -import { Spinner } from "@pythnetwork/component-library/Spinner"; |
5 | | -import type { ColumnConfig } from "@pythnetwork/component-library/Table"; |
6 | | -import { Table } from "@pythnetwork/component-library/Table"; |
| 5 | +import { Table, type ColumnConfig } from "@pythnetwork/component-library/Table"; |
| 6 | +import { useQueryParamFilterPagination } from "@pythnetwork/component-library/useQueryParamsPagination"; |
7 | 7 | import { getPriceFeedAccountForProgram } from "@pythnetwork/pyth-solana-receiver"; |
8 | 8 | import { Callout } from "fumadocs-ui/components/callout"; |
9 | | -import { useState, useEffect, useRef, useCallback, useMemo } from "react"; |
| 9 | +import { matchSorter } from "match-sorter"; |
| 10 | +import { useEffect, useState } from "react"; |
10 | 11 | import { z } from "zod"; |
11 | | - |
12 | 12 | import CopyAddress from "../CopyAddress"; |
13 | 13 | import styles from "./index.module.scss"; |
14 | 14 |
|
15 | 15 | export const PriceFeedIdsCoreTable = () => { |
16 | | - const isLoading = useRef(false); |
17 | 16 | const [state, setState] = useState<State>(State.NotLoaded()); |
18 | | - |
19 | 17 | useEffect(() => { |
20 | | - if (!isLoading.current) { |
21 | | - setState(State.Loading()); |
22 | | - isLoading.current = true; |
23 | | - getFeeds() |
24 | | - .then((feeds) => { |
25 | | - setState(State.Loaded(feeds)); |
26 | | - }) |
27 | | - .catch((error: unknown) => { |
28 | | - setState(State.Failed(error)); |
29 | | - }); |
30 | | - } |
31 | | - }, [isLoading]); |
32 | | - |
33 | | - switch (state.type) { |
34 | | - case StateType.Loading: |
35 | | - case StateType.NotLoaded: { |
36 | | - return <Spinner label="Fetching price feed ids..." />; |
37 | | - } |
38 | | - case StateType.Error: { |
39 | | - return <Callout type="error">{errorToString(state.error)}</Callout>; |
40 | | - } |
41 | | - case StateType.Loaded: { |
42 | | - return <LoadedResults feeds={state.feeds} />; |
43 | | - } |
44 | | - } |
45 | | -}; |
| 18 | + setState(State.Loading()); |
| 19 | + getFeeds() |
| 20 | + .then((feeds) => { |
| 21 | + setState(State.Loaded(feeds)); |
| 22 | + }) |
| 23 | + .catch((error: unknown) => { |
| 24 | + setState(State.Failed(error)); |
| 25 | + }); |
| 26 | + }, []); |
46 | 27 |
|
47 | | -const LoadedResults = ({ |
48 | | - feeds, |
49 | | -}: { |
50 | | - feeds: Awaited<ReturnType<typeof getFeeds>>; |
51 | | -}) => { |
52 | 28 | const columns: ColumnConfig<Col>[] = [ |
53 | | - { id: "symbol", name: "Symbol" }, |
| 29 | + { id: "symbol", name: "Symbol", isRowHeader: true }, |
54 | 30 | { id: "stableFeedId", name: "Stable Price Feed ID" }, |
55 | 31 | { id: "betaFeedId", name: "Beta Price Feed ID" }, |
56 | 32 | { id: "solanaPriceFeedAccount", name: "Solana Price Feed Account" }, |
57 | 33 | ]; |
58 | | - const [search, setSearch] = useState(""); |
59 | | - const handleSearchChange = useCallback((value: string) => { |
60 | | - setSearch(value); |
61 | | - }, []); |
62 | | - const filteredFeeds = useMemo( |
63 | | - () => |
64 | | - feeds.filter((feed) => { |
65 | | - const searchLower = search.toLowerCase(); |
66 | | - return [ |
67 | | - feed.symbol, |
68 | | - feed.stableFeedId, |
69 | | - feed.betaFeedId, |
70 | | - feed.solanaPriceFeedAccount, |
71 | | - ].some((value) => value?.toLowerCase().includes(searchLower)); |
72 | | - }), |
73 | | - [feeds, search], |
| 34 | + |
| 35 | + const { |
| 36 | + search, |
| 37 | + sortDescriptor, |
| 38 | + page, |
| 39 | + pageSize, |
| 40 | + updateSearch, |
| 41 | + updateSortDescriptor, |
| 42 | + updatePage, |
| 43 | + updatePageSize, |
| 44 | + paginatedItems, |
| 45 | + numPages, |
| 46 | + mkPageLink, |
| 47 | + } = useQueryParamFilterPagination( |
| 48 | + state.type === StateType.Loaded ? state.feeds : [], |
| 49 | + () => true, |
| 50 | + () => 1, |
| 51 | + (items, searchString) => { |
| 52 | + return matchSorter(items, searchString, { |
| 53 | + keys: [ |
| 54 | + "symbol", |
| 55 | + "stableFeedId", |
| 56 | + "betaFeedId", |
| 57 | + "solanaPriceFeedAccount", |
| 58 | + ], |
| 59 | + }); |
| 60 | + }, |
| 61 | + { defaultSort: "symbol", defaultPageSize: 10 }, |
74 | 62 | ); |
75 | 63 |
|
| 64 | + if (state.type === StateType.Error) { |
| 65 | + return <Callout type="error">{errorToString(state.error)}</Callout>; |
| 66 | + } |
| 67 | + |
| 68 | + const isLoading = |
| 69 | + state.type === StateType.Loading || state.type === StateType.NotLoaded; |
| 70 | + |
| 71 | + const rows = paginatedItems.map((feed) => ({ |
| 72 | + id: feed.symbol, |
| 73 | + data: { |
| 74 | + symbol: feed.symbol, |
| 75 | + stableFeedId: feed.stableFeedId ? ( |
| 76 | + <CopyAddress maxLength={6} address={feed.stableFeedId} /> |
| 77 | + ) : undefined, |
| 78 | + betaFeedId: feed.betaFeedId ? ( |
| 79 | + <CopyAddress maxLength={6} address={feed.betaFeedId} /> |
| 80 | + ) : undefined, |
| 81 | + solanaPriceFeedAccount: feed.solanaPriceFeedAccount ? ( |
| 82 | + <CopyAddress maxLength={6} address={feed.solanaPriceFeedAccount} /> |
| 83 | + ) : undefined, |
| 84 | + }, |
| 85 | + })); |
| 86 | + |
76 | 87 | return ( |
77 | 88 | <> |
78 | 89 | <SearchInput |
79 | 90 | label="Search price feeds" |
80 | 91 | placeholder="Search by symbol or feed id" |
81 | 92 | value={search} |
82 | | - onChange={handleSearchChange} |
83 | | - width={480} |
| 93 | + onChange={updateSearch} |
| 94 | + className={styles.searchInput ?? ""} |
84 | 95 | /> |
| 96 | + |
85 | 97 | <Table<Col> |
| 98 | + {...(isLoading ? { isLoading: true } : { isLoading: false, rows })} |
86 | 99 | label="Price feed ids" |
87 | 100 | columns={columns} |
88 | | - rows={filteredFeeds.map((feed) => ({ |
89 | | - id: feed.symbol, |
90 | | - data: { |
91 | | - symbol: feed.symbol, |
92 | | - stableFeedId: feed.stableFeedId ? ( |
93 | | - <CopyAddress maxLength={6} address={feed.stableFeedId} /> |
94 | | - ) : undefined, |
95 | | - betaFeedId: feed.betaFeedId ? ( |
96 | | - <CopyAddress maxLength={6} address={feed.betaFeedId} /> |
97 | | - ) : undefined, |
98 | | - solanaPriceFeedAccount: feed.solanaPriceFeedAccount ? ( |
99 | | - <CopyAddress |
100 | | - maxLength={6} |
101 | | - address={feed.solanaPriceFeedAccount} |
102 | | - /> |
103 | | - ) : undefined, |
104 | | - }, |
105 | | - }))} |
106 | | - className={styles.table ?? ""} |
| 101 | + onSortChange={updateSortDescriptor} |
| 102 | + sortDescriptor={sortDescriptor} |
107 | 103 | stickyHeader="top" |
108 | 104 | fill |
109 | 105 | rounded |
110 | 106 | /> |
| 107 | + <Paginator |
| 108 | + numPages={numPages} |
| 109 | + currentPage={page} |
| 110 | + onPageChange={updatePage} |
| 111 | + pageSize={pageSize} |
| 112 | + onPageSizeChange={updatePageSize} |
| 113 | + mkPageLink={mkPageLink} |
| 114 | + className={styles.paginator ?? ""} |
| 115 | + /> |
111 | 116 | </> |
112 | 117 | ); |
113 | 118 | }; |
|
0 commit comments