Skip to content

Commit

Permalink
feat: show wizard data on Editor UI from BOS (#936)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Kevin101Zhang committed Jul 31, 2024
1 parent 3237c18 commit 73d1fdd
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 53 deletions.
59 changes: 57 additions & 2 deletions frontend/src/components/Editor/EditorComponents/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { request, useInitialPayload } from 'near-social-bridge';

Check warning on line 1 in frontend/src/components/Editor/EditorComponents/Editor.tsx

View workflow job for this annotation

GitHub Actions / lint

Run autofix to sort these imports!
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';
Expand Down Expand Up @@ -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<WizardResponse> => {
return request<WizardResponse>('launchpad-create-indexer', req);
};

const Editor: React.FC = (): ReactElement => {
const { indexerDetails, isCreateNewIndexer } = useContext(IndexerDetailsContext);
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 3 additions & 7 deletions frontend/src/pages/api/WizardCodeGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 7 additions & 5 deletions frontend/src/pages/api/generateCode.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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[] =>
Expand All @@ -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)
);
};
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 4 additions & 2 deletions frontend/widgets/src/QueryApi.Editor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}

/**
Expand All @@ -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");
Expand Down
81 changes: 44 additions & 37 deletions frontend/widgets/src/QueryApi.Launchpad.jsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -210,6 +200,7 @@ scrollbar-color: #888 #f1f1f1;
`;

const GenerateMethodsButton = styled.button`
margin-top: 16px;
width: 100%;
background-color: #37CD83;
border: none;
Expand All @@ -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`
Expand Down Expand Up @@ -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);


Expand All @@ -463,7 +459,6 @@ const initializeCheckboxState = (data) => {
});
}
});

return initialState;
};

Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -551,7 +557,6 @@ const handleFetchCheckboxData = async () => {

};


const handleParentChange = (methodName) => {
setCheckboxState(prevState => {
const newState = !prevState[methodName];
Expand Down Expand Up @@ -585,9 +590,12 @@ const handleChildChange = (key) => {
});
};

const hasSelectedMethod = (checkboxState) => {
return Object.values(checkboxState).some(value => value === true);
}

return (
<>
<AlertText>Please note that this page is currently under development. Features may be incomplete or inaccurate</AlertText>
<Hero>
<Container>
<HeadlineContainer>
Expand Down Expand Up @@ -623,7 +631,7 @@ return (
</NoQueryContainer>
</>
: (
<div>
<SubContainerContent>
{checkBoxData.length > 0 && (
<MethodsText>
Methods <MethodsSpan>{methodCount}</MethodsSpan>
Expand Down Expand Up @@ -665,10 +673,9 @@ return (
)
}
</ScrollableDiv>
<GenerateMethodsButton onClick={generateMethods}> Generate</GenerateMethodsButton>
</div>
</SubContainerContent>
)}

<GenerateMethodsButton onClick={generateMethods} disabled={!checkboxState || !hasSelectedMethod(checkboxState)}> Generate</GenerateMethodsButton>
</SubContainerContent>
</SubContainer>
</WidgetContainer>
Expand Down

0 comments on commit 73d1fdd

Please sign in to comment.