From 73d1fdd2e41f8a7ab18552ea9b799f90e317281d Mon Sep 17 00:00:00 2001 From: Kevin Zhang <42101107+Kevin101Zhang@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:00:40 -0400 Subject: [PATCH] feat: show wizard data on Editor UI from BOS (#936) When users start on launchpad/wizard and populate the form with a valid checkbox state, BOS will send a request that the nextjs frontend consumes to check if the current Editor UI is one with launchpad data. If this is the case, the editor will call the generate/api and display the wizard code on editor. --- .../Editor/EditorComponents/Editor.tsx | 59 +++++++++++++- frontend/src/pages/api/WizardCodeGenerator.ts | 10 +-- frontend/src/pages/api/generateCode.ts | 12 +-- frontend/widgets/src/QueryApi.Editor.jsx | 6 +- frontend/widgets/src/QueryApi.Launchpad.jsx | 81 ++++++++++--------- 5 files changed, 115 insertions(+), 53 deletions(-) diff --git a/frontend/src/components/Editor/EditorComponents/Editor.tsx b/frontend/src/components/Editor/EditorComponents/Editor.tsx index a11e8bde..64fec42d 100644 --- a/frontend/src/components/Editor/EditorComponents/Editor.tsx +++ b/frontend/src/components/Editor/EditorComponents/Editor.tsx @@ -1,5 +1,7 @@ import { request, useInitialPayload } from 'near-social-bridge'; import type { ReactElement } from 'react'; +import type { Method, Event } from '@/pages/api/generateCode'; + import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; import { Alert } from 'react-bootstrap'; import { useDebouncedCallback } from 'use-debounce'; @@ -33,6 +35,15 @@ const SCHEMA_TAB_NAME = 'schema.sql'; const originalSQLCode = formatSQL(defaultSchema); const originalIndexingCode = formatIndexingCode(defaultCode); const pgSchemaTypeGen = new PgSchemaTypeGen(); +interface WizardResponse { + wizardContractFilter: string; + wizardMethods: Method[]; + wizardEvents?: Event[]; +} + +const fetchWizardData = (req: string): Promise => { + return request('launchpad-create-indexer', req); +}; const Editor: React.FC = (): ReactElement => { const { indexerDetails, isCreateNewIndexer } = useContext(IndexerDetailsContext); @@ -105,6 +116,51 @@ const Editor: React.FC = (): ReactElement => { return; }; + const generateCode = async (contractFilter: string, selectedMethods: Method[], selectedEvents?: Event[]) => { + try { + const response = await fetch('/api/generateCode', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ contractFilter, selectedMethods, selectedEvents }), + }); + if (!response.ok) { + throw new Error('Network response was not ok'); + } + const data = await response.json(); + + if (!data.hasOwnProperty('jsCode') || !data.hasOwnProperty('sqlCode')) { + throw new Error('No code was returned from the server'); + } + + return data; + } catch (error) { + throw error; + } + }; + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetchWizardData(''); + const { wizardContractFilter, wizardMethods } = response; + + if (wizardContractFilter === 'noFilter') { + return; + } + + const codeResponse = await generateCode(wizardContractFilter, wizardMethods); + setIndexingCode(codeResponse.jsCode); + setSchema(codeResponse.sqlCode); + } catch (error: unknown) { + //todo: figure out best course of action for user if api fails + console.error(error); + } + }; + fetchData(); + }, []); + useEffect(() => { //* Load saved code from local storage if it exists else load code from context const savedCode = storageManager?.getIndexerCode(); @@ -117,7 +173,6 @@ const Editor: React.FC = (): ReactElement => { //* Load saved cursor position from local storage if it exists else set cursor to start const savedCursorPosition = storageManager?.getCursorPosition(); if (savedCursorPosition) setCursorPosition(savedCursorPosition); - if (monacoEditorRef.current && fileName === INDEXER_TAB_NAME) { monacoEditorRef.current.setPosition(savedCursorPosition || { lineNumber: 1, column: 1 }); monacoEditorRef.current.focus(); @@ -282,7 +337,7 @@ const Editor: React.FC = (): ReactElement => { `${primitives}}`, 'file:///node_modules/@near-lake/primitives/index.d.ts', ); - + monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ target: monaco.languages.typescript.ScriptTarget.ES2018, allowNonTsExtensions: true, diff --git a/frontend/src/pages/api/WizardCodeGenerator.ts b/frontend/src/pages/api/WizardCodeGenerator.ts index 4a6e8750..889798b7 100644 --- a/frontend/src/pages/api/WizardCodeGenerator.ts +++ b/frontend/src/pages/api/WizardCodeGenerator.ts @@ -58,11 +58,7 @@ const createColumn = (columnName: string, schema: Schema): Column => { }; export class WizardCodeGenerator { - constructor( - private contractFilter: string, - private selectedMethods: Method[], - private selectedEvents: Event[], - ) {} + constructor(private contractFilter: string, private selectedMethods: Method[], private selectedEvents?: Event[]) {} private getColumns(method: Method): Column[] { if (!method.schema.properties) { @@ -107,8 +103,8 @@ ${columns.map((c) => `-- CREATE INDEX "${tableName}_${c.name}_key" ON "${tableNa return ` // Extract and upsert ${methodName} function calls const callsTo${methodName} = extractFunctionCallEntity("${this.contractFilter}", "${methodName}", ${JSON.stringify( - columnNames, - )}); + columnNames, + )}); try { await context.db.${contextDbName}.upsert(callsTo${methodName}, ${JSON.stringify(primaryKeys)}, ${JSON.stringify( columnNames, diff --git a/frontend/src/pages/api/generateCode.ts b/frontend/src/pages/api/generateCode.ts index 58897252..cdde47b8 100644 --- a/frontend/src/pages/api/generateCode.ts +++ b/frontend/src/pages/api/generateCode.ts @@ -1,12 +1,13 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + import { createSchema } from 'genson-js'; import type { Schema } from 'genson-js/dist/types'; -import type { NextApiRequest, NextApiResponse } from 'next'; +import { WizardCodeGenerator } from './WizardCodeGenerator'; export type Method = { method_name: string; schema: Schema; }; -import { WizardCodeGenerator } from './WizardCodeGenerator'; export type Event = { event_name: string; @@ -16,7 +17,7 @@ export type Event = { export interface RequestBody { contractFilter: string | string[]; selectedMethods: Method[]; - selectedEvents: Event[]; + selectedEvents?: Event[]; } export const isStringOrArray = (value: any): value is string | string[] => @@ -36,8 +37,8 @@ export const validateRequestBody = (body: any): body is RequestBody => { return ( isStringOrArray(body.contractFilter) && Array.isArray(body.selectedMethods) && - body.selectedMethods.every(isValidMethod) //&& - // Array.isArray(body.selectedEvents) && + body.selectedMethods.every(isValidMethod) + // && Array.isArray(body.selectedEvents) && // body.selectedEvents.every(isValidEvent) ); }; @@ -77,6 +78,7 @@ export default function handler(req: NextApiRequest, res: NextApiResponse): void } const { contractFilter, selectedMethods, selectedEvents } = req.body; + const filterString = Array.isArray(contractFilter) ? contractFilter.join(', ') : contractFilter; const generator = new WizardCodeGenerator(filterString, selectedMethods, selectedEvents); diff --git a/frontend/widgets/src/QueryApi.Editor.jsx b/frontend/widgets/src/QueryApi.Editor.jsx index a58266c7..be383544 100644 --- a/frontend/widgets/src/QueryApi.Editor.jsx +++ b/frontend/widgets/src/QueryApi.Editor.jsx @@ -58,7 +58,9 @@ let deleteIndexer = (request) => { }; const getLaunchpadCode = (request, response) => { - return { wizardContractFilter, wizardMethods }; + const wizardContractFilter = wizardContractFilter ?? 'noFilter'; + const wizardMethods = wizardMethods; + response(request).send({ wizardContractFilter, wizardMethods }); } /** @@ -73,7 +75,7 @@ const requestHandler = (request, response) => { deleteIndexer(request, response); break; case "launchpad-create-indexer": - getLaunchpadCode(); + getLaunchpadCode(request, response); break case "default": console.log("default case"); diff --git a/frontend/widgets/src/QueryApi.Launchpad.jsx b/frontend/widgets/src/QueryApi.Launchpad.jsx index 12901b4d..737e064d 100644 --- a/frontend/widgets/src/QueryApi.Launchpad.jsx +++ b/frontend/widgets/src/QueryApi.Launchpad.jsx @@ -1,14 +1,4 @@ const { setActiveTab, activeTab, setSelectedIndexer, setWizardContractFilter, setWizardMethods } = props; -const AlertText = styled.p` -font-family: 'Mona Sans', sans-serif; -font-size: 14px; -line-height: 21px; -text-align: center; -color:red; -margin: 0; -padding: 0; -bg-color: #f9f9f9; -`; const NoQueryContainer = styled.div` display: flex; @@ -210,6 +200,7 @@ scrollbar-color: #888 #f1f1f1; `; const GenerateMethodsButton = styled.button` + margin-top: 16px; width: 100%; background-color: #37CD83; border: none; @@ -222,6 +213,12 @@ const GenerateMethodsButton = styled.button` justify-content: center; position:relative; z-index:10; + + &:disabled { + background-color: #F3F3F2; + color: #999; + cursor: not-allowed; + } ` const InputWrapper = styled.div` @@ -448,7 +445,6 @@ const [checkboxState, setCheckboxState] = useState(initialCheckboxState); const [methodCount, setMethodCount] = useState(0); const [contractInputMessage, setContractInputMessage] = useState(''); const [inputValue, setInputValue] = useState(''); -const [allIndexers, setAllIndexers] = useState([]); const [loading, setLoading] = useState(false); @@ -463,7 +459,6 @@ const initializeCheckboxState = (data) => { }); } }); - return initialState; }; @@ -474,24 +469,36 @@ useEffect(() => { const generateMethods = () => { const filteredData = checkBoxData.map(item => { const parentChecked = checkboxState[item.method_name]; - if (!item.schema || !item.schema.properties) return null; - - const filteredProperties = Object.keys(item.schema.properties).reduce((acc, property) => { - const childKey = `${item.method_name}::${property}`; - if (checkboxState[childKey]) { - acc[property] = item.schema.properties[property]; + if (!item.schema) return null; + + if (!item.schema.properties) { + if (parentChecked) { + return { + method_name: item.method_name, + schema: { + ...item.schema + } + }; } - return acc; - }, {}); - - if (parentChecked || Object.keys(filteredProperties).length > 0) { - return { - method_name: item.method_name, - schema: { - ...item.schema, - properties: filteredProperties + return null; + } else { + const result = Object.entries(item.schema.properties).reduce((acc, [property, details]) => { + const childKey = `${item.method_name}::${property}`; + if (checkboxState[childKey]) { + acc.filteredProperties[property] = details; } - }; + return acc; + }, { filteredProperties: {}, shouldReturn: parentChecked }); + + if (result.shouldReturn || Object.keys(result.filteredProperties).length > 0) { + return { + method_name: item.method_name, + schema: { + ...item.schema, + properties: result.filteredProperties + } + }; + } } return null; @@ -540,9 +547,8 @@ const handleFetchCheckboxData = async () => { setLoading(false); return; }; - - setCheckBoxData(data); - setMethodCount(data.length); + setCheckBoxData(data.methods); + setMethodCount(data.methods.length); setLoading(false); }).catch(error => { setLoading(false); @@ -551,7 +557,6 @@ const handleFetchCheckboxData = async () => { }; - const handleParentChange = (methodName) => { setCheckboxState(prevState => { const newState = !prevState[methodName]; @@ -585,9 +590,12 @@ const handleChildChange = (key) => { }); }; +const hasSelectedMethod = (checkboxState) => { + return Object.values(checkboxState).some(value => value === true); +} + return ( <> - Please note that this page is currently under development. Features may be incomplete or inaccurate @@ -623,7 +631,7 @@ return ( : ( -
+ {checkBoxData.length > 0 && ( Methods {methodCount} @@ -665,10 +673,9 @@ return ( ) } - Generate -
+ )} - + Generate