Skip to content

Commit

Permalink
feat: use QueryAPI Indexer for Explorers and added pagination (#907)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Kevin101Zhang authored Jul 23, 2024
1 parent b29d8b6 commit c3439c3
Show file tree
Hide file tree
Showing 2 changed files with 227 additions and 76 deletions.
2 changes: 1 addition & 1 deletion frontend/src/pages/query-api-editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const QueryApiEditorPage = ({ router }) => {
);
}

if (accountId == 'test' || indexerName == 'test') {
if (accountId == 'test' && indexerName == 'test') {
return <GenerateCode />;
}

Expand Down
301 changes: 226 additions & 75 deletions frontend/widgets/src/QueryApi.IndexerExplorer.jsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,143 @@
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}`;

asyncFetch(url)
.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();
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -186,6 +335,8 @@ const Button = styled.button`
span {
color: #687076 !important;
}
`;

const Tabs = styled.div`
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -391,56 +550,40 @@ const LoadingSpinner = () => {

return (
<Wrapper>

<NavBarContainer>
<LeftGroup>
<NavBarLogo title="QueryApi">
<Widget
src="mob.near/widget/Image"
props={{
className: "d-inline-block align-text-top me-2",
image: metadata.image,
style: { height: "20px" }, // Smaller logo
fallbackUrl:
"https://upload.wikimedia.org/wikipedia/commons/8/86/Database-icon.svg",
alt: "the queryapi logo",
}}
/>
QueryApi
</NavBarLogo>
<ToggleWrapper>
<ToggleButton
onClick={() => setSelectedTab("my-indexers")}
selected={selectedTab === "my-indexers"}
>
My Indexers
</ToggleButton>
<ToggleButton
onClick={() => setSelectedTab("all")}
selected={selectedTab === "all"}
>
All Indexers
</ToggleButton>
</ToggleWrapper>

<SignUpLink target="_blank" href={`https://docs.near.org/build/data-infrastructure/query-api/intro`}>
(Documentation)
</SignUpLink>

<ButtonWrapper>
<ButtonLink
href={`/${REPL_ACCOUNT_ID}/widget/QueryApi.App/?view=create-new-indexer`}
onClick={() => {
setActiveTab("create-new-indexer");
setSelectedIndexerName("");
selectTab("create-new-indexer");
}}
>
Create New Indexer
</ButtonLink>
</ButtonWrapper>
</LeftGroup>

<ToggleWrapper>
<ToggleButton
onClick={() => setSelectedTab("my-indexers")}
selected={selectedTab === "my-indexers"}
>
My Indexers
</ToggleButton>
<ToggleButton
onClick={() => setSelectedTab("all")}
selected={selectedTab === "all"}
<ButtonWrapper>
<ButtonLink
href={`/${REPL_ACCOUNT_ID}/widget/QueryApi.App/?view=create-new-indexer`}
onClick={() => {
setActiveTab("create-new-indexer");
setSelectedIndexerName("");
selectTab("create-new-indexer");
}}
>
All Indexers
</ToggleButton>
</ToggleWrapper>
Create New Indexer
</ButtonLink>
</ButtonWrapper>
</NavBarContainer>

{error && <Text>{error}</Text>}
Expand All @@ -452,16 +595,24 @@ return (
<LoadingSpinner />
</Container>
) : (
<Items>
{allIndexers.map((indexer, i) => (
<Item key={i}>
<Widget
src={`${REPL_ACCOUNT_ID}/widget/QueryApi.IndexerCard`}
props={{ ...indexer, indexerMetadata }}
/>
</Item>
))}
</Items>
<>
<Items>
{indexers.map((indexer, i) => (
<Item key={i}>
<Widget
src={`${REPL_ACCOUNT_ID}/widget/QueryApi.IndexerCard`}
props={{ ...indexer, indexerMetadata }}
/>
</Item>
))}
</Items>
{isLoadingMore && <LoadingSpinner />}
<LoadMoreContainer>
<Button onClick={handleLoadMore} disabled={isLoadingMore || isFetching || error !== null || total === 0 || (total === indexers.length)}>
Load More
</Button>
</LoadMoreContainer>
</>
)}
</>
)}
Expand Down

0 comments on commit c3439c3

Please sign in to comment.