Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show wizard data on Editor UI from BOS #936

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -29,10 +31,19 @@

declare const monaco: any;
const INDEXER_TAB_NAME = 'indexer.js';
const SCHEMA_TAB_NAME = 'schema.sql';

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

View workflow job for this annotation

GitHub Actions / lint

'SCHEMA_TAB_NAME' is assigned a value but never used. Allowed unused vars must match /^_/u
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 @@ -60,7 +71,7 @@
const [heights, setHeights] = useState<number[]>(initialHeights);

const [diffView, setDiffView] = useState<boolean>(false);
const [blockView, setBlockView] = useState<boolean>(false);

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

View workflow job for this annotation

GitHub Actions / lint

'setBlockView' is assigned a value but never used. Allowed unused vars must match /^_/u
const { showModal } = useModal();

const [isExecutingIndexerFunction, setIsExecutingIndexerFunction] = useState<boolean>(false);
Expand Down Expand Up @@ -105,6 +116,51 @@
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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we validate the data returned as well? To make sure it has jsCode and sqlCode fields? And how do we handle a failure here visually? If something goes wrong, what would be the next best customer experience? Being redirected to a page which lets them explore existing indexers? The Create new indexer page?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I can validate it to check for a correct response. Im not sure what the correct user response would be if the generateAPI fails. we have already forcefully already navigated them to the editor component outside of BOS.


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,12 +173,11 @@
//* 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();
}
}, [indexerDetails.code]);

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

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has missing dependencies: 'fileName' and 'storageManager'. Either include them or remove the dependency array

useEffect(() => {
//* Load saved schema from local storage if it exists else load code from context
Expand All @@ -133,7 +188,7 @@
schemaErrorHandler(schemaError);
formattedSchema && setSchema(formattedSchema);
}
}, [indexerDetails.schema]);

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

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'storageManager'. Either include it or remove the dependency array

useEffect(() => {
const { error: schemaError } = validateSQLSchema(schema);
Expand All @@ -146,11 +201,11 @@
}

handleCodeGen();
}, [fileName]);

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

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has missing dependencies: 'handleCodeGen', 'indexingCode', and 'schema'. Either include them or remove the dependency array

useEffect(() => {
cacheToLocal();
}, [indexingCode, schema]);

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

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'cacheToLocal'. Either include it or remove the dependency array

useEffect(() => {
if (!monacoEditorRef.current) return;
Expand All @@ -161,16 +216,16 @@
return () => {
editorInstance.dispose();
};
}, [monacoEditorRef.current]);

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

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'handleCursorChange'. Either include it or remove the dependency array. Mutable values like 'monacoEditorRef.current' aren't valid dependencies because mutating them doesn't re-render the component

useEffect(() => {
storageManager?.setSchemaTypes(schemaTypes);
handleCodeGen();
}, [schemaTypes, monacoMount]);

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

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has missing dependencies: 'handleCodeGen' and 'storageManager'. Either include them or remove the dependency array

useEffect(() => {
storageManager?.setDebugList(heights);
}, [heights]);

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

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'storageManager'. Either include it or remove the dependency array

const cacheToLocal = () => {
if (!storageManager || !monacoEditorRef.current) return;
Expand Down Expand Up @@ -282,7 +337,7 @@
`${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;
Copy link
Collaborator

@darunrs darunrs Jul 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have an if statement like this, you don't need to have item.schema checks in following if statements. You can leave it out as you know following code is guaranteed to have item.schema. The same follows for any other things you check. If you want that to be readily apparent, you can leave in the else block that's omitted here. All subsequent code lives in the else instead. This leads to a lot of tabbing, which can then be a signal that maybe a refactor is needed. You can maybe try something like the following:

  1. Perform checks which return null
  2. Store combined conditionals as boolean with a name. E.g. parentChecked || Object.keys(filteredProperties).length > 0 gets stored as a variable called hasPropertiesFromParent or something. As is, I have no clue what that if statement checks.
  3. Move filter logic to its own function called filterSchemaProperties()
  4. Move some conditional logic to return such as ...(var && { var }) which only spreads if var exists.

Something like that. In any case, my main point is this code is confusing. Can you refactor this code to make its logic more apparent at first glance?


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
Loading