From cef18036c6d1f7cd3eff46a5d863359ed69eb9ad Mon Sep 17 00:00:00 2001 From: Roshaan Siddiqui Date: Wed, 8 Nov 2023 16:21:40 +0000 Subject: [PATCH 1/9] add indexer tables feature to queryapi --- frontend/package.json | 1 + .../src/components/Editor/EditorButtons.jsx | 23 +- frontend/src/components/Logs/IndexerLogs.jsx | 285 ++++++++++++++++++ frontend/src/components/Logs/LogButtons.jsx | 136 +++++++++ .../src/contexts/IndexerDetailsContext.js | 7 +- frontend/src/pages/indexer-logs/index.js | 35 +++ frontend/src/pages/query-api-editor/index.js | 14 +- 7 files changed, 494 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/Logs/IndexerLogs.jsx create mode 100644 frontend/src/components/Logs/LogButtons.jsx create mode 100644 frontend/src/pages/indexer-logs/index.js diff --git a/frontend/package.json b/frontend/package.json index 7ce4a1c14..2c437e6b5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,7 @@ "eslint-config-next": "13.1.6", "graphiql": "3.0.6", "graphql": "^16.6.0", + "gridjs": "6.0.6", "near-api-js": "1.1.0", "near-social-bridge": "^1.4.1", "next": "13.1.6", diff --git a/frontend/src/components/Editor/EditorButtons.jsx b/frontend/src/components/Editor/EditorButtons.jsx index 212390f6e..1208428fc 100644 --- a/frontend/src/components/Editor/EditorButtons.jsx +++ b/frontend/src/components/Editor/EditorButtons.jsx @@ -19,7 +19,8 @@ import { TrashFill, XCircle, NodePlus, - Code + Code, + FileText, } from "react-bootstrap-icons"; import { BlockPicker } from "./BlockPicker"; import { IndexerDetailsContext } from '../../contexts/IndexerDetailsContext'; @@ -49,7 +50,9 @@ const EditorButtons = ({ debugMode, isCreateNewIndexer, indexerNameField, - setIndexerNameField + setIndexerNameField, + setShowLogsView, + showLogsView, } = useContext(IndexerDetailsContext); const removeHeight = (index) => { @@ -185,6 +188,22 @@ const EditorButtons = ({ + {!isCreateNewIndexer && ( + Open Logs} + > + + + )} {(!isUserIndexer && !isCreateNewIndexer) ? ( { + const { indexerDetails, debugMode, setLogsView } = useContext( + IndexerDetailsContext + ); + const functionName = `${indexerDetails.accountId}/${indexerDetails.indexerName}`; + + const DEBUG_LIST_STORAGE_KEY = `QueryAPI:debugList:${indexerDetails.accountId}#${indexerDetails.indexerName} `; + + const { height, selectedTab, currentUserAccountId } = useInitialPayload(); + const [heights, setHeights] = useState( + localStorage.getItem(DEBUG_LIST_STORAGE_KEY) || [] + ); + useEffect(() => { + localStorage.setItem(DEBUG_LIST_STORAGE_KEY, heights); + }, [heights]); + + const indexerLogsRef = useRef(null); + const indexerStateRef = useRef(null); + + function formatTimestamp(timestamp) { + const date = new Date(timestamp); + + const options = { + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "2-digit", + hour12: true, + }; + const formattedDate = date.toLocaleDateString(undefined, options); + + const now = new Date(); + const diffInSeconds = Math.round((now - date) / 1000); + let relativeTime = undefined; + if (diffInSeconds < 60) { + relativeTime = "(just now)"; + } else if (diffInSeconds < 3600) { + relativeTime = `(${Math.floor(diffInSeconds / 60)} minutes ago)`; + } else if (diffInSeconds < 86400) { + relativeTime = `(${Math.floor(diffInSeconds / 3600)} hours ago)`; + } + + return `${formattedDate} ${relativeTime ?? ""}`; + } + + function formatTimestampToReadableLocal(timestamp) { + const date = new Date(timestamp); + + const options = { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + hour12: true, + }; + const formattedDate = date.toLocaleString(undefined, options); + + return formattedDate; + } + + const processLogs = (data) => { + const logEntries = data.indexer_log_entries.map((row) => ({ + block_height: row.block_height, + timestamp: row.timestamp, + message: row.message, + })); + + const groupedEntries = new Map(); + + logEntries.forEach(({ block_height, timestamp, message }) => { + if (!groupedEntries.has(block_height)) { + groupedEntries.set(block_height, []); + } + groupedEntries.get(block_height).push({ timestamp, message }); + }); + + const mergedEntries = Array.from(groupedEntries).map( + ([block_height, entries]) => { + const messages = entries + .map( + (e) => + `Timestamp: ${e.timestamp + }(${formatTimestampToReadableLocal( + e.timestamp + )}): \n

${e.message}

` + ) + .join("
"); + + const minTimestamp = entries.reduce( + (min, e) => (e.timestamp < min ? e.timestamp : min), + entries[0].timestamp + ); + + const formattedMinTimstamp = formatTimestamp(minTimestamp); + const humanReadableStamp = formatTimestampToReadableLocal(minTimestamp); + + return { + block_height, + timestamp: { humanReadableStamp, formattedMinTimstamp }, + messages, + }; + } + ); + + return mergedEntries; + }; + + useEffect(() => { + const grid = new Grid({ + columns: [ + { + name: "Current Block Height", + formatter: (cell) => html(`
${cell}
`) + }, + { + name: "Current Historical Block Height", + formatter: (cell) => html(`
${cell}
`) + }, + { + name: "Status", + formatter: (cell) => html(`
${cell}
`) + }, + ], + search: false, + sort: false, + resizable: true, + fixedHeader: true, + server: { + url: `${process.env.NEXT_PUBLIC_HASURA_ENDPOINT}/api/rest/queryapi/logs/?_functionName=${functionName}`, + headers: { + "x-hasura-role": "append", + }, + then: (data) => { + return data.indexer_state.map((state) => [ + state.current_block_height, + state.current_historical_block_height, + state.status, + ]); + }, + }, + style: { + container: { + "font-family": '"Roboto Mono", monospace', + }, + table: {}, + th: { + "text-align": "center", + "max-width": "950px", + width: "800px", + }, + td: { + "text-align": "center", + "font-size": "11px", + "background-color": "rgb(255, 255, 255)", + "max-height": "400px", + padding: "5px", + }, + }, + }); + + grid.render(indexerStateRef.current); + // }; + // fetchData(); + }, []); + + useEffect(() => { + const grid = new Grid({ + columns: [ + { + name: "Block Height", + formatter: (cell) => html(`
${cell}
`) + }, + "Timestamp", + { + name: "Message", + formatter: (cell) => html(`
${cell}
`), + sort: false, + }, + ], + search: { + server: { + url: (prev, keyword) => `${process.env.NEXT_PUBLIC_HASURA_ENDPOINT}/api/rest/queryapi/logsByBlock/?_functionName=${functionName}&_blockHeight=${keyword}`, + then: (data) => { + const logs = processLogs(data).map((log) => [ + log.block_height, + log.timestamp.formattedMinTimstamp, + log.messages, + ]); + return logs; + }, + debounceTimeout: 2000, + } + }, + sort: true, + resizable: true, + fixedHeader: true, + pagination: { + limit: 30, + server: { + url: (prev, page, limit) => { + return prev + "&limit=" + limit + "&offset=" + page * limit; + }, + }, + }, + server: { + url: `${process.env.NEXT_PUBLIC_HASURA_ENDPOINT}/api/rest/queryapi/logs/?_functionName=${functionName}`, + headers: { + "x-hasura-role": "append", + }, + then: (data) => { + const logs = processLogs(data).map((log) => [ + log.block_height, + log.timestamp.formattedMinTimstamp, + log.messages, + ]); + return logs; + }, + total: (data) => data.indexer_log_entries_aggregate.aggregate.count, + }, + style: { + container: { + "font-family": '"Roboto Mono", monospace', + }, + table: {}, + th: { + "text-align": "center", + "max-width": "950px", + width: "800px", + }, + td: { + "text-align": "left", + "font-size": "11px", + "background-color": "rgb(255, 255, 255)", + "max-height": "400px", + padding: "5px", + }, + }, + language: { + search: { + placeholder: "🔍 Search by Block Height...", + }, + pagination: { + results: () => "Indexer Logs", + }, + }, + }); + + grid.render(indexerLogsRef.current); + }, []); + + return ( + <> +
+ +
+
+
+ + ); +}; + +export default IndexerLogsComponent; diff --git a/frontend/src/components/Logs/LogButtons.jsx b/frontend/src/components/Logs/LogButtons.jsx new file mode 100644 index 000000000..c831e118e --- /dev/null +++ b/frontend/src/components/Logs/LogButtons.jsx @@ -0,0 +1,136 @@ +import React, { useContext } from "react"; +import { + Breadcrumb, + Button, + Form, + InputGroup, + Navbar, + Container, + Col, + Row, + ButtonGroup, + OverlayTrigger, + Tooltip, + Badge, +} from "react-bootstrap"; +import { + ArrowCounterclockwise, + Justify, + TrashFill, + XCircle, + NodePlus, + Code, +} from "react-bootstrap-icons"; +import { IndexerDetailsContext } from "../../contexts/IndexerDetailsContext"; + +const LogButtons = ({ + currentUserAccountId, + heights, + setHeights, + latestHeight, + isUserIndexer, +}) => { + const { + indexerName, + accountId, + indexerDetails, + debugMode, + setShowLogsView, + showLogsView, + } = useContext(IndexerDetailsContext); + + const removeHeight = (index) => { + setHeights(heights.filter((_, i) => i !== index)); + }; + + return ( + <> + + + + + + + {accountId} + + {indexerName && ( + + {indexerName} + + )} + + { + + Contract Filter + + + } + + + + <> + Open Editor} + > + + + + + + + {debugMode && heights.length > 0 && ( + +
+ {heights.map((height, index) => ( + + {height} + removeHeight(index)} + /> + + ))} +
+
+ )} +
+
+ + ); +}; + +export default LogButtons; diff --git a/frontend/src/contexts/IndexerDetailsContext.js b/frontend/src/contexts/IndexerDetailsContext.js index 10358857b..e536deb01 100644 --- a/frontend/src/contexts/IndexerDetailsContext.js +++ b/frontend/src/contexts/IndexerDetailsContext.js @@ -39,6 +39,8 @@ export const IndexerDetailsContext = React.createContext({ indexerName: undefined, setIndexerName: () => { }, setIndexerDetails: () => { }, + showLogsView: false, + setShowLogsView: () => { }, }); export const IndexerDetailsProvider = ({ children }) => { @@ -49,6 +51,7 @@ export const IndexerDetailsProvider = ({ children }) => { const [showPublishModal, setShowPublishModal] = useState(false); const [showForkIndexerModal, setShowForkIndexerModal] = useState(false); const [debugMode, setDebugMode] = useState(false); + const [showLogsView, setShowLogsView] = useState(false); const [latestHeight, setLatestHeight] = useState(0); const [isCreateNewIndexer, setIsCreateNewIndexer] = useState(false); @@ -117,7 +120,9 @@ export const IndexerDetailsProvider = ({ children }) => { setDebugMode, latestHeight, isCreateNewIndexer, - setIsCreateNewIndexer + setIsCreateNewIndexer, + showLogsView, + setShowLogsView, }} > {children} diff --git a/frontend/src/pages/indexer-logs/index.js b/frontend/src/pages/indexer-logs/index.js new file mode 100644 index 000000000..fd3cbe107 --- /dev/null +++ b/frontend/src/pages/indexer-logs/index.js @@ -0,0 +1,35 @@ +import React, { useEffect, useContext } from "react"; + +import IndexerLogs from "../../components/Logs/IndexerLogs"; +import { withRouter } from 'next/router' +import { Alert } from 'react-bootstrap'; +// import { EditorContext } from '../../contexts/EditorContext'; +import { IndexerDetailsContext } from '../../contexts/IndexerDetailsContext'; + +const IndexerLogsPage = ({ router }) => { + const { accountId, indexerName } = router.query + // const { setAccountId, setIndexerName } = useContext(EditorContext); + const { setAccountId, setIndexerName } = useContext(IndexerDetailsContext); + useEffect(() => { + if (accountId == undefined || indexerName == undefined) { + return; + } + setAccountId(accountId); + setIndexerName(indexerName); + }, [accountId, indexerName, setAccountId, setIndexerName]); + + if (accountId == undefined || indexerName == undefined) { + return ( + <> + + Both accountId and IndexerName need to be specified in the URL. + + + ) + } + return ( + + ); +}; + +export default withRouter(QueryApiEditorPage); diff --git a/frontend/src/pages/query-api-editor/index.js b/frontend/src/pages/query-api-editor/index.js index d9d844719..36356f337 100644 --- a/frontend/src/pages/query-api-editor/index.js +++ b/frontend/src/pages/query-api-editor/index.js @@ -3,13 +3,13 @@ import React, { useEffect, useContext } from "react"; import Editor from "../../components/Editor"; import { withRouter } from 'next/router' import { Alert } from 'react-bootstrap'; -// import { EditorContext } from '../../contexts/EditorContext'; import { IndexerDetailsContext } from '../../contexts/IndexerDetailsContext'; +import IndexerLogs from "../../components/Logs/IndexerLogs"; const QueryApiEditorPage = ({ router }) => { const { accountId, indexerName } = router.query - // const { setAccountId, setIndexerName } = useContext(EditorContext); - const { setAccountId, setIndexerName } = useContext(IndexerDetailsContext); + const { setAccountId, setIndexerName, showLogsView } = useContext(IndexerDetailsContext); + useEffect(() => { if (accountId == undefined || indexerName == undefined) { return; @@ -28,7 +28,13 @@ const QueryApiEditorPage = ({ router }) => { ) } return ( - + <> + {showLogsView ? ( + + ) : ( + + )} + ); }; From a11a6cbf0b48dbbb33fabc88b0c4b1c638adf449 Mon Sep 17 00:00:00 2001 From: Roshaan Siddiqui Date: Thu, 9 Nov 2023 11:38:14 +0000 Subject: [PATCH 2/9] feat: changes in widgets --- .../src/contexts/IndexerDetailsContext.js | 7 ++ frontend/widgets/src/QueryApi.App.jsx | 2 + frontend/widgets/src/QueryApi.Dashboard.jsx | 80 +++++++------------ frontend/widgets/src/QueryApi.Editor.jsx | 2 + frontend/widgets/src/QueryApi.IndexerCard.jsx | 6 +- 5 files changed, 45 insertions(+), 52 deletions(-) diff --git a/frontend/src/contexts/IndexerDetailsContext.js b/frontend/src/contexts/IndexerDetailsContext.js index e536deb01..29ab66ecc 100644 --- a/frontend/src/contexts/IndexerDetailsContext.js +++ b/frontend/src/contexts/IndexerDetailsContext.js @@ -6,6 +6,7 @@ import { wrapCode, } from "../utils/formatters"; +import { useInitialPayload } from "near-social-bridge"; import { getLatestBlockHeight } from "../utils/getLatestBlockHeight"; // interface IndexerDetails { // accountId: String, @@ -55,6 +56,12 @@ export const IndexerDetailsProvider = ({ children }) => { const [latestHeight, setLatestHeight] = useState(0); const [isCreateNewIndexer, setIsCreateNewIndexer] = useState(false); + const { activeView } = useInitialPayload(); + + useEffect(() => { + if (activeView == 'status') setShowLogsView(true) + }, []) + const requestIndexerDetails = async () => { const data = await queryIndexerFunctionDetails(accountId, indexerName); if (data) { diff --git a/frontend/widgets/src/QueryApi.App.jsx b/frontend/widgets/src/QueryApi.App.jsx index 033ce17bf..63313b733 100644 --- a/frontend/widgets/src/QueryApi.App.jsx +++ b/frontend/widgets/src/QueryApi.App.jsx @@ -1,6 +1,7 @@ const view = props.view; const path = props.path; const tab = props.tab; +const activeIndexerView = props.activeIndexerView; const selectedIndexerPath = props.selectedIndexerPath; return ( @@ -11,6 +12,7 @@ return ( path, tab, selectedIndexerPath, + activeIndexerView }} /> ); diff --git a/frontend/widgets/src/QueryApi.Dashboard.jsx b/frontend/widgets/src/QueryApi.Dashboard.jsx index 23a937c91..6b81c14bc 100644 --- a/frontend/widgets/src/QueryApi.Dashboard.jsx +++ b/frontend/widgets/src/QueryApi.Dashboard.jsx @@ -4,11 +4,13 @@ const [selected_accountId, selected_indexerName] = props.selectedIndexerPath : [undefined, undefined]; const activeTab = props.view ?? "indexers"; +const activeIndexerView = props.activeIndexerView ?? "editor"; const limit = 7; let totalIndexers = 0; State.init({ activeTab: activeTab, + activeIndexerView: activeIndexerView, my_indexers: [], all_indexers: [], selected_indexer: undefined, @@ -53,6 +55,15 @@ const Subheading = styled.h2` outline: none; `; +const Editor = styled.div` + // visibility: ${state.activeIndexerView === "status" ? "hidden" : "visible"}; + // order: ${state.activeIndexerView === "status" ? 2 : 1}; +`; +const Status = styled.div` + // visibility: ${state.activeIndexerView === "editor" ? "hidden" : "visible"}; + // order: ${state.activeIndexerView === "editor" ? 1 : 2}; +`; + const Wrapper = styled.div` margin-top: calc(var(--body-top-padding) * -1); `; @@ -310,9 +321,15 @@ const selectTab = (tabName) => { }); }; +const selectIndexerPage = (viewName) => { + Storage.privateSet("queryapi:activeIndexerTabView", viewName); + State.update({ + activeIndexerView: viewName, + }); +}; const indexerView = (accountId, indexerName) => { - const editUrl = `https://near.org/#/${REPL_ACCOUNT_ID}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}&view=editor-window`; - const statusUrl = `https://near.org/#/${REPL_ACCOUNT_ID}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}&view=indexer-status`; + const editUrl = `https://near.org/#/${REPL_ACCOUNT_ID}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}&view=indexer&activeIndexerView=editor`; + const statusUrl = `https://near.org/#/${REPL_ACCOUNT_ID}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}&view=indexer&activeIndexerView=status`; const playgroundLink = `https://cloud.hasura.io/public/graphiql?endpoint=${REPL_GRAPHQL_ENDPOINT}/v1/graphql&header=x-hasura-role%3A${accountId.replaceAll( ".", "_" @@ -344,10 +361,10 @@ const indexerView = (accountId, indexerName) => { - selectTab("indexer-status")}> + selectIndexerPage("status")}> View Status - selectTab("editor-window")}> + selectIndexerPage("editor")}> {accountId === context.accountId ? "Edit Indexer" : "View Indexer"} @@ -366,7 +383,7 @@ return ( onClick={() => selectTab("indexers")} selected={state.activeTab === "indexers"} > - Indexers + Explore Indexers {state.activeTab == "create-new-indexer" && ( selectTab("editor-window")} - selected={state.activeTab === "editor-window"} - > - Indexer Editor - - - selectTab("indexer-status")} - selected={state.activeTab === "indexer-status"} + onClick={() => selectTab("indexer")} + selected={state.activeTab === "indexer"} > - Indexer Status + Indexer ({props.selectedIndexerPath}) )} @@ -466,37 +475,9 @@ return (
)} -
- {state.activeTab === "indexer-status" && ( -
- {state.indexers.length > 0 && - (state.selected_indexer != "" ? ( -

{state.selected_indexer}

- ) : ( -

{state.indexers[0].indexerName}

- ))} - {indexerView( - selected_accountId ?? state.indexers[0].accountId, - selected_indexerName ?? state.indexers[0].indexerName - )} - -
- )} -
-
- {state.activeTab === "editor-window" && ( -
+
+ {state.activeIndexerView === "editor" && ( + {state.indexers.length > 0 && (state.selected_indexer != undefined ? (

{state.selected_indexer}

@@ -511,11 +492,12 @@ return ( accountId: selected_accountId ?? state.indexers[0].accountId, path: "query-api-editor", tab: props.tab, + activeIndexerView: state.activeIndexerView }} /> -
+ )} - {state.activeTab === "create-new-indexer" && ( + {state.activeIndexerView === "create-new-indexer" && (
{state.indexers.length > 0 && (state.selected_indexer != undefined ? ( diff --git a/frontend/widgets/src/QueryApi.Editor.jsx b/frontend/widgets/src/QueryApi.Editor.jsx index 8deadca58..92b32d9e8 100644 --- a/frontend/widgets/src/QueryApi.Editor.jsx +++ b/frontend/widgets/src/QueryApi.Editor.jsx @@ -1,5 +1,6 @@ const path = props.path || "query-api-editor"; const tab = props.tab || ""; +const activeView = props.activeView || "editor"; let accountId = props.accountId || context.accountId; let externalAppUrl = `${REPL_EXTERNAL_APP_URL}/${path}?accountId=${accountId}`; @@ -14,6 +15,7 @@ if (!context.accountId) { const initialPayload = { height: Near.block("optimistic").header.height, selectedTab: tab, + activeView: activeView, currentUserAccountId: context.accountId, }; diff --git a/frontend/widgets/src/QueryApi.IndexerCard.jsx b/frontend/widgets/src/QueryApi.IndexerCard.jsx index f64545ba3..bbb6bed95 100644 --- a/frontend/widgets/src/QueryApi.IndexerCard.jsx +++ b/frontend/widgets/src/QueryApi.IndexerCard.jsx @@ -1,7 +1,7 @@ const accountId = props.accountId || context.accountId; const indexerName = props.indexerName; -const editUrl = `https://near.org/#/${REPL_ACCOUNT_ID}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}&view=editor-window`; -const statusUrl = `https://near.org/#/${REPL_ACCOUNT_ID}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}&view=indexer-status`; +const editUrl = `https://near.org/#/${REPL_ACCOUNT_ID}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}&view=indexer&activeIndexerView=editor`; +const statusUrl = `https://near.org/#/${REPL_ACCOUNT_ID}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}&view=indexer&activeIndexerView=status`; const playgroundLink = `https://cloud.hasura.io/public/graphiql?endpoint=${REPL_GRAPHQL_ENDPOINT}/v1/graphql&header=x-hasura-role%3A${accountId.replaceAll( ".", "_" @@ -178,7 +178,7 @@ return ( href={editUrl} onClick={() => State.update({ - activeTab: "editor-window", + activeTab: "editor", }) } > From f05b5216f17c23687c9d48b6f3a45c892b79315a Mon Sep 17 00:00:00 2001 From: Roshaan Siddiqui Date: Fri, 10 Nov 2023 16:14:50 +0000 Subject: [PATCH 3/9] feat: fix-routing, make widget avalaible even if not logged in --- frontend/widgets/src/QueryApi.Dashboard.jsx | 22 ++++++++------------- frontend/widgets/src/QueryApi.Editor.jsx | 5 +---- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/frontend/widgets/src/QueryApi.Dashboard.jsx b/frontend/widgets/src/QueryApi.Dashboard.jsx index 6b81c14bc..3875d0d39 100644 --- a/frontend/widgets/src/QueryApi.Dashboard.jsx +++ b/frontend/widgets/src/QueryApi.Dashboard.jsx @@ -3,7 +3,7 @@ const [selected_accountId, selected_indexerName] = props.selectedIndexerPath ? props.selectedIndexerPath.split("/") : [undefined, undefined]; -const activeTab = props.view ?? "indexers"; +const activeTab = props.view ? props.view : (props.selectedIndexerPath ? "indexer" : "explore") const activeIndexerView = props.activeIndexerView ?? "editor"; const limit = 7; let totalIndexers = 0; @@ -56,12 +56,8 @@ const Subheading = styled.h2` `; const Editor = styled.div` - // visibility: ${state.activeIndexerView === "status" ? "hidden" : "visible"}; - // order: ${state.activeIndexerView === "status" ? 2 : 1}; `; const Status = styled.div` - // visibility: ${state.activeIndexerView === "editor" ? "hidden" : "visible"}; - // order: ${state.activeIndexerView === "editor" ? 1 : 2}; `; const Wrapper = styled.div` @@ -376,12 +372,12 @@ const indexerView = (accountId, indexerName) => { }; return ( - + selectTab("indexers")} - selected={state.activeTab === "indexers"} + onClick={() => selectTab("explore")} + selected={state.activeTab === "explore"} > Explore Indexers @@ -408,11 +404,11 @@ return ( )}
-
+
selectTab("indexers")} + onClick={() => selectTab("explore")} >
- {state.activeIndexerView === "editor" && ( {state.indexers.length > 0 && (state.selected_indexer != undefined ? ( @@ -492,12 +487,11 @@ return ( accountId: selected_accountId ?? state.indexers[0].accountId, path: "query-api-editor", tab: props.tab, - activeIndexerView: state.activeIndexerView + activeView: state.activeIndexerView }} /> - )} - {state.activeIndexerView === "create-new-indexer" && ( + {state.activeTab === "create-new-indexer" && (
{state.indexers.length > 0 && (state.selected_indexer != undefined ? ( diff --git a/frontend/widgets/src/QueryApi.Editor.jsx b/frontend/widgets/src/QueryApi.Editor.jsx index 92b32d9e8..8e37a34f6 100644 --- a/frontend/widgets/src/QueryApi.Editor.jsx +++ b/frontend/widgets/src/QueryApi.Editor.jsx @@ -8,14 +8,11 @@ if (props.indexerName) { externalAppUrl += `&indexerName=${props.indexerName}`; } const initialViewHeight = 1000; -if (!context.accountId) { - return "Please sign in to use this widget."; -} const initialPayload = { height: Near.block("optimistic").header.height, selectedTab: tab, - activeView: activeView, + activeView, currentUserAccountId: context.accountId, }; From b3935a9181dfbc3f74fcaa733527331723727b8d Mon Sep 17 00:00:00 2001 From: Roshaan Siddiqui Date: Tue, 14 Nov 2023 07:34:25 -0600 Subject: [PATCH 4/9] Indexer Status component + styling --- frontend/package.json | 4 +- .../src/components/Editor/EditorButtons.jsx | 89 +++++++-------- frontend/src/components/Logs/IndexerLogs.jsx | 91 ++++------------ frontend/src/components/Logs/Status.jsx | 103 ++++++++++++++++++ frontend/src/pages/_app.tsx | 13 ++- frontend/widgets/src/QueryApi.Dashboard.jsx | 4 +- frontend/widgets/src/QueryApi.IndexerCard.jsx | 2 +- 7 files changed, 188 insertions(+), 118 deletions(-) create mode 100644 frontend/src/components/Logs/Status.jsx diff --git a/frontend/package.json b/frontend/package.json index 2c437e6b5..13281bc26 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "lint": "next lint" }, "dependencies": { + "@apollo/client": "^3.8.7", "@graphiql/plugin-code-exporter": "0.3.5", "@graphiql/plugin-explorer": "0.3.5", "@monaco-editor/react": "^4.1.3", @@ -25,7 +26,8 @@ "eslint": "8.34.0", "eslint-config-next": "13.1.6", "graphiql": "3.0.6", - "graphql": "^16.6.0", + "graphql": "^16.8.1", + "graphql-ws": "^5.14.2", "gridjs": "6.0.6", "near-api-js": "1.1.0", "near-social-bridge": "^1.4.1", diff --git a/frontend/src/components/Editor/EditorButtons.jsx b/frontend/src/components/Editor/EditorButtons.jsx index 1208428fc..83ac5dba8 100644 --- a/frontend/src/components/Editor/EditorButtons.jsx +++ b/frontend/src/components/Editor/EditorButtons.jsx @@ -72,7 +72,6 @@ const EditorButtons = ({ > - {accountId} @@ -110,7 +109,7 @@ const EditorButtons = ({ )} )} - Reset Changes To Code} - > - - - - Format Code} - > - - - - Generate Types} - > - - {!isCreateNewIndexer && ( )} + + + Reset Changes To Code} + > + + + Format Code} + > + + + Generate Types} + > + + + + {debugMode && heights.length > 0 && ( diff --git a/frontend/src/components/Logs/IndexerLogs.jsx b/frontend/src/components/Logs/IndexerLogs.jsx index b2550052c..6b13b9ad8 100644 --- a/frontend/src/components/Logs/IndexerLogs.jsx +++ b/frontend/src/components/Logs/IndexerLogs.jsx @@ -4,11 +4,12 @@ import "gridjs/dist/theme/mermaid.css"; import { IndexerDetailsContext } from "../../contexts/IndexerDetailsContext"; import LogButtons from "./LogButtons"; import { useInitialPayload } from "near-social-bridge"; +import Status from "./Status"; const LIMIT = 100; const IndexerLogsComponent = () => { - const { indexerDetails, debugMode, setLogsView } = useContext( + const { indexerDetails, debugMode, setLogsView, latestHeight } = useContext( IndexerDetailsContext ); const functionName = `${indexerDetails.accountId}/${indexerDetails.indexerName}`; @@ -115,70 +116,15 @@ const IndexerLogsComponent = () => { return mergedEntries; }; - useEffect(() => { - const grid = new Grid({ - columns: [ - { - name: "Current Block Height", - formatter: (cell) => html(`
${cell}
`) - }, - { - name: "Current Historical Block Height", - formatter: (cell) => html(`
${cell}
`) - }, - { - name: "Status", - formatter: (cell) => html(`
${cell}
`) - }, - ], - search: false, - sort: false, - resizable: true, - fixedHeader: true, - server: { - url: `${process.env.NEXT_PUBLIC_HASURA_ENDPOINT}/api/rest/queryapi/logs/?_functionName=${functionName}`, - headers: { - "x-hasura-role": "append", - }, - then: (data) => { - return data.indexer_state.map((state) => [ - state.current_block_height, - state.current_historical_block_height, - state.status, - ]); - }, - }, - style: { - container: { - "font-family": '"Roboto Mono", monospace', - }, - table: {}, - th: { - "text-align": "center", - "max-width": "950px", - width: "800px", - }, - td: { - "text-align": "center", - "font-size": "11px", - "background-color": "rgb(255, 255, 255)", - "max-height": "400px", - padding: "5px", - }, - }, - }); - - grid.render(indexerStateRef.current); - // }; - // fetchData(); - }, []); - useEffect(() => { const grid = new Grid({ columns: [ { name: "Block Height", - formatter: (cell) => html(``) + formatter: (cell) => + html( + `` + ), }, "Timestamp", { @@ -189,17 +135,18 @@ const IndexerLogsComponent = () => { ], search: { server: { - url: (prev, keyword) => `${process.env.NEXT_PUBLIC_HASURA_ENDPOINT}/api/rest/queryapi/logsByBlock/?_functionName=${functionName}&_blockHeight=${keyword}`, + url: (prev, keyword) => + `${process.env.NEXT_PUBLIC_HASURA_ENDPOINT}/api/rest/queryapi/logsByBlock/?_functionName=${functionName}&_blockHeight=${keyword}`, then: (data) => { - const logs = processLogs(data).map((log) => [ - log.block_height, - log.timestamp.formattedMinTimstamp, - log.messages, - ]); - return logs; - }, + const logs = processLogs(data).map((log) => [ + log.block_height, + log.timestamp.formattedMinTimstamp, + log.messages, + ]); + return logs; + }, debounceTimeout: 2000, - } + }, }, sort: true, resizable: true, @@ -240,6 +187,7 @@ const IndexerLogsComponent = () => { td: { "text-align": "left", "font-size": "11px", + "vertical-align": "text-top", "background-color": "rgb(255, 255, 255)", "max-height": "400px", padding: "5px", @@ -275,7 +223,10 @@ const IndexerLogsComponent = () => { latestHeight={height} isUserIndexer={indexerDetails.accountId === currentUserAccountId} /> -
+
diff --git a/frontend/src/components/Logs/Status.jsx b/frontend/src/components/Logs/Status.jsx new file mode 100644 index 000000000..b2880f2d8 --- /dev/null +++ b/frontend/src/components/Logs/Status.jsx @@ -0,0 +1,103 @@ +import React from "react"; +import { + Card, + Badge, + ListGroup, + ProgressBar, + OverlayTrigger, + Tooltip, +} from "react-bootstrap"; +import { useQuery, gql } from "@apollo/client"; + +const Status = ({ functionName, latestHeight }) => { + const GET_STATUS = gql` + query GetState($_functionName: String!) { + indexer_state(where: { function_name: { _eq: $_functionName } }) { + status + function_name + current_block_height + current_historical_block_height + } + } + `; + const { loading, error, data } = useQuery(GET_STATUS, { + variables: { + _functionName: functionName, + }, + }); + + // const statusVariant = status === "running" ? "success" : "danger"; + // const progressPercentage = (currentBlockHeight / latestHeight) * 100; + + if (loading) return

Loading...

; + if (error) return

Error : {error.message}

; + return ( +
+ {data && + data.indexer_state.map((item, index) => ( + + + Indexer Status: {item.function_name} + + + The Current Block Height is {latestHeight}. Your indexer has a gap of {latestHeight - item.current_block_height} Blocks} + > + + + + Current Block Height:{" "} + {item.current_block_height} + + + Historical Block Height:{" "} + {item.current_historical_block_height} + + + {item.status === "RUNNING" ? "Indexer is operating normally" : "Indexer stopped due to errors. Check Logs for more details."}} + > + <> + Status:{" "} + + {item.status} + + + + + + + ))} +
+ ); +}; + +export default Status; diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index eecf66fe2..4033f8daa 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -8,15 +8,26 @@ import { } from "near-social-bridge"; import { IndexerDetailsProvider } from '../contexts/IndexerDetailsContext'; import 'regenerator-runtime/runtime'; +import { ApolloClient, InMemoryCache, ApolloProvider, gql } from '@apollo/client'; overrideLocalStorage(); export default function App({ Component, pageProps }: AppProps) { + const client = new ApolloClient({ + uri: `${process.env.NEXT_PUBLIC_HASURA_ENDPOINT}/v1/graphql`, + cache: new InMemoryCache(), + options: { + headers: { + "x-hasura-role": "append" + } + } + }); return ( }> + + ); } - diff --git a/frontend/widgets/src/QueryApi.Dashboard.jsx b/frontend/widgets/src/QueryApi.Dashboard.jsx index 3875d0d39..6e08df8ef 100644 --- a/frontend/widgets/src/QueryApi.Dashboard.jsx +++ b/frontend/widgets/src/QueryApi.Dashboard.jsx @@ -114,7 +114,7 @@ const Title = styled.h1` const TabsButton = styled.button` font-weight: 600; - font-size: 12px; + font-size: 14px; line-height: 16px; padding: 0 12px; position: relative; @@ -324,7 +324,7 @@ const selectIndexerPage = (viewName) => { }); }; const indexerView = (accountId, indexerName) => { - const editUrl = `https://near.org/#/${REPL_ACCOUNT_ID}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}&view=indexer&activeIndexerView=editor`; + const editUrl = `https://near.org/#/${REPL_ACCOUNT_ID}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}`; const statusUrl = `https://near.org/#/${REPL_ACCOUNT_ID}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}&view=indexer&activeIndexerView=status`; const playgroundLink = `https://cloud.hasura.io/public/graphiql?endpoint=${REPL_GRAPHQL_ENDPOINT}/v1/graphql&header=x-hasura-role%3A${accountId.replaceAll( ".", diff --git a/frontend/widgets/src/QueryApi.IndexerCard.jsx b/frontend/widgets/src/QueryApi.IndexerCard.jsx index bbb6bed95..191f688a1 100644 --- a/frontend/widgets/src/QueryApi.IndexerCard.jsx +++ b/frontend/widgets/src/QueryApi.IndexerCard.jsx @@ -1,6 +1,6 @@ const accountId = props.accountId || context.accountId; const indexerName = props.indexerName; -const editUrl = `https://near.org/#/${REPL_ACCOUNT_ID}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}&view=indexer&activeIndexerView=editor`; +const editUrl = `https://near.org/#/${REPL_ACCOUNT_ID}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}`; const statusUrl = `https://near.org/#/${REPL_ACCOUNT_ID}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}&view=indexer&activeIndexerView=status`; const playgroundLink = `https://cloud.hasura.io/public/graphiql?endpoint=${REPL_GRAPHQL_ENDPOINT}/v1/graphql&header=x-hasura-role%3A${accountId.replaceAll( ".", From cba15a65658a841e83546032b9b41298591d1180 Mon Sep 17 00:00:00 2001 From: Roshaan Siddiqui Date: Tue, 14 Nov 2023 07:53:52 -0600 Subject: [PATCH 5/9] choose 'indexer' as default path --- frontend/widgets/src/QueryApi.Dashboard.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/widgets/src/QueryApi.Dashboard.jsx b/frontend/widgets/src/QueryApi.Dashboard.jsx index 6e08df8ef..2771a3f62 100644 --- a/frontend/widgets/src/QueryApi.Dashboard.jsx +++ b/frontend/widgets/src/QueryApi.Dashboard.jsx @@ -3,7 +3,7 @@ const [selected_accountId, selected_indexerName] = props.selectedIndexerPath ? props.selectedIndexerPath.split("/") : [undefined, undefined]; -const activeTab = props.view ? props.view : (props.selectedIndexerPath ? "indexer" : "explore") +const activeTab = props.view == "create-new-indexer" ? "create-new-indexer" : props.selectedIndexerPath ? "indexer" : "explore" const activeIndexerView = props.activeIndexerView ?? "editor"; const limit = 7; let totalIndexers = 0; From ca420894cf24c25def9f42ec42b9eacc10f4e27b Mon Sep 17 00:00:00 2001 From: Roshaan Siddiqui Date: Tue, 14 Nov 2023 08:52:32 -0600 Subject: [PATCH 6/9] chore: add warning if indexer is not found --- frontend/src/components/Editor/Editor.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/src/components/Editor/Editor.js b/frontend/src/components/Editor/Editor.js index 0da8175c4..f8a7fc2e3 100644 --- a/frontend/src/components/Editor/Editor.js +++ b/frontend/src/components/Editor/Editor.js @@ -381,6 +381,12 @@ const Editor = ({ height: "85vh", }} > + {!indexerDetails.code && ( + + Indexer Function could not be found. Are you sure this indexer exists? + + )} + {indexerDetails.code && <>
+ }
); }; From 0c68e4d37679bd84bf6934f87db961256ec9faf7 Mon Sep 17 00:00:00 2001 From: Roshaan Siddiqui Date: Tue, 14 Nov 2023 08:53:00 -0600 Subject: [PATCH 7/9] chore: remove progress bar in status for current iteration --- frontend/src/components/Logs/Status.jsx | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/Logs/Status.jsx b/frontend/src/components/Logs/Status.jsx index b2880f2d8..6f32f1b78 100644 --- a/frontend/src/components/Logs/Status.jsx +++ b/frontend/src/components/Logs/Status.jsx @@ -3,7 +3,6 @@ import { Card, Badge, ListGroup, - ProgressBar, OverlayTrigger, Tooltip, } from "react-bootstrap"; @@ -26,9 +25,6 @@ const Status = ({ functionName, latestHeight }) => { }, }); - // const statusVariant = status === "running" ? "success" : "danger"; - // const progressPercentage = (currentBlockHeight / latestHeight) * 100; - if (loading) return

Loading...

; if (error) return

Error : {error.message}

; return ( @@ -55,24 +51,13 @@ const Status = ({ functionName, latestHeight }) => { > The Current Block Height is {latestHeight}. Your indexer has a gap of {latestHeight - item.current_block_height} Blocks} + overlay={Near's Current Block Height is {latestHeight}. Your indexer has a gap of {latestHeight - item.current_block_height} Blocks} > - - Current Block Height:{" "} {item.current_block_height} + Historical Block Height:{" "} {item.current_historical_block_height} From 78c3e8c10322f142d714ac8cd75d07f09b43ddf4 Mon Sep 17 00:00:00 2001 From: Roshaan Siddiqui Date: Tue, 14 Nov 2023 09:16:13 -0600 Subject: [PATCH 8/9] update queryapi link to reflect link in EMS --- frontend/src/components/Playground/graphiql.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Playground/graphiql.jsx b/frontend/src/components/Playground/graphiql.jsx index c8955f6f3..bf22f29cc 100644 --- a/frontend/src/components/Playground/graphiql.jsx +++ b/frontend/src/components/Playground/graphiql.jsx @@ -9,7 +9,7 @@ import '@graphiql/plugin-code-exporter/dist/style.css'; import '@graphiql/plugin-explorer/dist/style.css'; const HASURA_ENDPOINT = - process.env.NEXT_PUBLIC_HASURA_ENDPOINT || + `${process.env.NEXT_PUBLIC_HASURA_ENDPOINT}/v1/graphql` || "https://near-queryapi.dev.api.pagoda.co/v1/graphql"; const graphQLFetcher = async (graphQLParams, accountId) => { From 22aeec13cd696f43b8d04df9fe7c315b896f740a Mon Sep 17 00:00:00 2001 From: Roshaan Siddiqui Date: Tue, 14 Nov 2023 09:16:19 -0600 Subject: [PATCH 9/9] remove graphql-ws --- frontend/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 13281bc26..5a83207d9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,7 +27,6 @@ "eslint-config-next": "13.1.6", "graphiql": "3.0.6", "graphql": "^16.8.1", - "graphql-ws": "^5.14.2", "gridjs": "6.0.6", "near-api-js": "1.1.0", "near-social-bridge": "^1.4.1",