From c3439c34f045b5641a93fa72b102381d5c7291e2 Mon Sep 17 00:00:00 2001 From: Kevin Zhang <42101107+Kevin101Zhang@users.noreply.github.com> Date: Tue, 23 Jul 2024 16:28:17 -0400 Subject: [PATCH] feat: use QueryAPI Indexer for Explorers and added pagination (#907) QueryAPI Indexer's explore page now fetches from Indexers and will default to NEAR RPC request if something goes wrong. Added Pagination/Loading more for All Indexers --- frontend/src/pages/query-api-editor/index.js | 2 +- .../widgets/src/QueryApi.IndexerExplorer.jsx | 301 +++++++++++++----- 2 files changed, 227 insertions(+), 76 deletions(-) diff --git a/frontend/src/pages/query-api-editor/index.js b/frontend/src/pages/query-api-editor/index.js index 87750c60..ec622a31 100644 --- a/frontend/src/pages/query-api-editor/index.js +++ b/frontend/src/pages/query-api-editor/index.js @@ -25,7 +25,7 @@ const QueryApiEditorPage = ({ router }) => { ); } - if (accountId == 'test' || indexerName == 'test') { + if (accountId == 'test' && indexerName == 'test') { return ; } diff --git a/frontend/widgets/src/QueryApi.IndexerExplorer.jsx b/frontend/widgets/src/QueryApi.IndexerExplorer.jsx index abb9ca61..1562f8f4 100644 --- a/frontend/widgets/src/QueryApi.IndexerExplorer.jsx +++ b/frontend/widgets/src/QueryApi.IndexerExplorer.jsx @@ -1,33 +1,135 @@ const myAccountId = context.accountId; +const PAGE_SIZE = 50; +const TABLE_NAME = "dataplatform_near_queryapi_indexer_indexers"; +const GET_ALL_ACTIVE_INDEXERS = ` +query getAllActiveIndexers($limit: Int!, $offset: Int!) { + ${TABLE_NAME}( + where: {is_removed: {_eq: false}} + limit: $limit + offset: $offset + ) { + author_account_id + indexer_name + } + ${TABLE_NAME}_aggregate( + where: {is_removed: {_eq: false}} + ) { + aggregate { + count + } + } +} +`; + +const GET_MY_ACTIVE_INDEXERS = ` +query getMyActiveIndexers($authorAccountId: String!) { + ${TABLE_NAME}( + where: { + is_removed: { _eq: false } + author_account_id: { _eq: $authorAccountId } + } + ) { + author_account_id + indexer_name + } +} +`; + const [selectedTab, setSelectedTab] = useState(props.tab && props.tab !== "all" ? props.tab : "all"); -const [myIndexers, setMyIndexers] = useState([]); -const [allIndexers, setAllIndexers] = useState([]); -const [error, setError] = useState(null); const [indexerMetadata, setIndexerMetaData] = useState(new Map()); + +const [indexers, setIndexers] = useState([]); +const [total, setTotal] = useState(0); +const [myIndexers, setMyIndexers] = useState([]); +const [page, setPage] = useState(0); + const [loading, setLoading] = useState(false); +const [isLoadingMore, setIsLoadingMore] = useState(false); +const [isFetching, setIsFetching] = useState(false); +const [error, setError] = useState(null); + +const fetchGraphQL = (operationsDoc, operationName, variables) => { + const graphQLEndpoint = `${REPL_GRAPHQL_ENDPOINT}`; + return asyncFetch(`${graphQLEndpoint}/v1/graphql`, { + method: "POST", + headers: { + "x-hasura-role": "dataplatform_near", + }, + body: JSON.stringify({ + query: operationsDoc, + variables: variables, + operationName: operationName, + }), + }); +} -const fetchIndexerData = () => { +const fetchMyIndexerData = () => { setLoading(true); - Near.asyncView(`${REPL_REGISTRY_CONTRACT_ID}`, "list_all").then((data) => { - const allIndexers = []; - const myIndexers = []; - Object.keys(data).forEach((accId) => { - Object.keys(data[accId]).forEach((functionName) => { - const indexer = { - accountId: accId, - indexerName: functionName, - }; - if (accId === myAccountId) myIndexers.push(indexer); - allIndexers.push(indexer); - }); + + fetchGraphQL(GET_MY_ACTIVE_INDEXERS, 'getMyActiveIndexers', { + authorAccountId: myAccountId, + }) + .then((result) => { + if (result.status === 200) { + const data = result?.body?.data?.[TABLE_NAME]; + if (Array.isArray(data)) { + const newIndexers = data.map(({ author_account_id, indexer_name }) => ({ + accountId: author_account_id, + indexerName: indexer_name, + })); + setMyIndexers(newIndexers); + } else { + throw new Error('Data is not an array:', data); + } + } else { + throw new Error('Failed to fetch data:', result); + } + setLoading(false); + }) + .catch((error) => { + setError('An error occurred while retrieving indexer data. Attempting to fetch from NEAR RPC...', error); + backupNearRPCRequest(); }); - setMyIndexers(myIndexers); - setAllIndexers(allIndexers); - setLoading(false); - }); } +const fetchIndexerData = (page, append) => { + if (isFetching) return; + setIsFetching(true); + append ? setIsLoadingMore(true) : setLoading(true); + + fetchGraphQL(GET_ALL_ACTIVE_INDEXERS, 'getAllActiveIndexers', { + limit: PAGE_SIZE, + offset: page * PAGE_SIZE, + }) + .then((result) => { + if (result.status === 200) { + const data = result?.body?.data?.[TABLE_NAME]; + const totalCount = result?.body?.data?.[`${TABLE_NAME}_aggregate`]?.aggregate?.count; + if (Array.isArray(data)) { + const newIndexers = data.map(({ author_account_id, indexer_name }) => ({ + accountId: author_account_id, + indexerName: indexer_name, + })); + setTotal(totalCount); + setIndexers((prevIndexers) => append ? [...prevIndexers, ...newIndexers] : newIndexers); + } else { + throw new Error('Data is not an array:', data); + } + } else { + throw new Error('Failed to fetch data:', result); + } + append ? setIsLoadingMore(false) : setLoading(false); + setIsFetching(false); + }) + .catch((error) => { + setError('An error occurred while retrieving indexer data. Attempting to fetch from NEAR RPC...', error); + append ? setIsLoadingMore(false) : setLoading(false); + setIsFetching(false); + backupNearRPCRequest(); + }); +}; + const storeIndexerMetaData = () => { const url = `${REPL_QUERY_API_USAGE_URL}`; @@ -35,7 +137,7 @@ const storeIndexerMetaData = () => { .then(response => { if (!response.ok) { setError('There was an error fetching the data'); - throw new Error(`HTTP error! status: ${response.status}`); + return; } const { data } = JSON.parse(response.body); const map = new Map(); @@ -55,15 +157,55 @@ const storeIndexerMetaData = () => { }); }); setIndexerMetaData(map); - setError(null); }) } useEffect(() => { - fetchIndexerData(); storeIndexerMetaData(); }, []); +useEffect(() => { + fetchIndexerData(page, page > 0); +}, [page]); + +useEffect(() => { + if (selectedTab === "my-indexers") { + if (myIndexers.length <= 0) fetchMyIndexerData(); + } + if (selectedTab === "all") { + if (indexers.length <= 0) fetchIndexerData(page, page > 0); + } +}, [selectedTab]); + +const handleLoadMore = () => { + if (!isLoadingMore) setPage((prevPage) => prevPage + 1); +}; + +const backupNearRPCRequest = () => { + console.log('Retrieving data from Near RPC..'); + setLoading(true); + Near.asyncView(`${REPL_REGISTRY_CONTRACT_ID}`, "list_all").then((data) => { + const allIndexers = []; + const myIndexers = []; + + Object.keys(data).forEach((accId) => { + Object.keys(data[accId]).forEach((functionName) => { + const indexer = { + accountId: accId, + indexerName: functionName, + }; + if (accId === myAccountId) myIndexers.push(indexer); + allIndexers.push(indexer); + }); + }); + setMyIndexers(myIndexers); + setIndexers(allIndexers); + setTotal(0); + setLoading(false); + setError(null); + }); +} + const Container = styled.div` display: flex; justify-content: center; @@ -176,6 +318,13 @@ const Button = styled.button` color: #11181c !important; margin: 0; + &:disabled { + background-color: #cccccc; + color: #666666; + cursor: not-allowed; + opacity: 0.6; + } + &:hover, &:focus { background: #ecedee; @@ -186,6 +335,8 @@ const Button = styled.button` span { color: #687076 !important; } + + `; const Tabs = styled.div` @@ -290,7 +441,7 @@ const SignUpLink = styled.a` color: #0070f3; text-decoration: none; font-size: 0.75rem; - margin-right: 1rem; + margin-left: 1rem; `; const ButtonWrapper = styled.div` @@ -356,6 +507,14 @@ const ToggleButton = styled.button` } `; +const LoadMoreContainer = styled.div` + display: flex; + justify-content: center; + align-items: flex-end; + margin-top: 16px; + padding: 16px; +`; + const LoadingSpinner = () => { const spinnerStyle = { width: '40px', @@ -391,56 +550,40 @@ const LoadingSpinner = () => { return ( - - - - QueryApi - + + setSelectedTab("my-indexers")} + selected={selectedTab === "my-indexers"} + > + My Indexers + + setSelectedTab("all")} + selected={selectedTab === "all"} + > + All Indexers + + (Documentation) - - - { - setActiveTab("create-new-indexer"); - setSelectedIndexerName(""); - selectTab("create-new-indexer"); - }} - > - Create New Indexer - - - - setSelectedTab("my-indexers")} - selected={selectedTab === "my-indexers"} - > - My Indexers - - setSelectedTab("all")} - selected={selectedTab === "all"} + + { + setActiveTab("create-new-indexer"); + setSelectedIndexerName(""); + selectTab("create-new-indexer"); + }} > - All Indexers - - + Create New Indexer + + {error && {error}} @@ -452,16 +595,24 @@ return ( ) : ( - - {allIndexers.map((indexer, i) => ( - - - - ))} - + <> + + {indexers.map((indexer, i) => ( + + + + ))} + + {isLoadingMore && } + + + + )} )}