From 1318cb63c7b9b0c67119db27a6a6cae76fcea9f5 Mon Sep 17 00:00:00 2001 From: Kevin Zhang <42101107+Kevin101Zhang@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:29:13 -0400 Subject: [PATCH] feat: Enhance Schema Error Visualization with New Icons (#855) Optimized Editor Lifecycle methods and removed the 'janky' Alert that would insert itself between the developer tools and Editor component in favor of a small icon that is in the fileswitcher. --- .../Common/Icons/AlertSquareIcon.js | 10 + .../Common/Icons/CheckMarkSquareIcon.js | 17 ++ .../Editor/EditorComponents/Editor.tsx | 250 +++++++----------- .../Editor/EditorComponents/FileSwitcher.jsx | 33 ++- .../Editor/EditorView/DeveloperToolsView.jsx | 37 +-- .../Editor/EditorView/EditorMenuView.jsx | 4 +- .../EditorMenuContainer.tsx | 26 +- .../src/contexts/IndexerDetailsContext.tsx | 109 +++++--- frontend/src/pages/query-api-editor/index.js | 28 +- frontend/src/utils/formatters.js | 12 +- frontend/src/utils/validators.ts | 12 +- 11 files changed, 278 insertions(+), 260 deletions(-) create mode 100644 frontend/src/components/Common/Icons/AlertSquareIcon.js create mode 100644 frontend/src/components/Common/Icons/CheckMarkSquareIcon.js diff --git a/frontend/src/components/Common/Icons/AlertSquareIcon.js b/frontend/src/components/Common/Icons/AlertSquareIcon.js new file mode 100644 index 000000000..b3df85803 --- /dev/null +++ b/frontend/src/components/Common/Icons/AlertSquareIcon.js @@ -0,0 +1,10 @@ +import React from 'react'; + +const AlertSquareIcon = () => { + return ( +
+ ! +
+ ); +}; +export default AlertSquareIcon; diff --git a/frontend/src/components/Common/Icons/CheckMarkSquareIcon.js b/frontend/src/components/Common/Icons/CheckMarkSquareIcon.js new file mode 100644 index 000000000..2b25571aa --- /dev/null +++ b/frontend/src/components/Common/Icons/CheckMarkSquareIcon.js @@ -0,0 +1,17 @@ +import React from 'react'; + +const CheckMarkSquareIcon = () => { + return ( +
+ + + +
+ ); +}; + +export default CheckMarkSquareIcon; diff --git a/frontend/src/components/Editor/EditorComponents/Editor.tsx b/frontend/src/components/Editor/EditorComponents/Editor.tsx index c3a33c0a3..4daf53648 100644 --- a/frontend/src/components/Editor/EditorComponents/Editor.tsx +++ b/frontend/src/components/Editor/EditorComponents/Editor.tsx @@ -1,13 +1,11 @@ import { request, useInitialPayload } from 'near-social-bridge'; -import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; import type { ReactElement } from 'react'; +import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; import { Alert } from 'react-bootstrap'; import { useDebouncedCallback } from 'use-debounce'; import primitives from '!!raw-loader!./primitives.d.ts'; import { - CODE_FORMATTING_ERROR_MESSAGE, - CODE_GENERAL_ERROR_MESSAGE, FORMATTING_ERROR_TYPE, SCHEMA_FORMATTING_ERROR_MESSAGE, SCHEMA_TYPE_GENERATION_ERROR_MESSAGE, @@ -15,7 +13,6 @@ import { } from '@/constants/Strings'; import { IndexerDetailsContext } from '@/contexts/IndexerDetailsContext'; import { useModal } from '@/contexts/ModalContext'; -import { InfoModal } from '@/core/InfoModal'; import { defaultCode, defaultSchema, defaultSchemaTypes, formatIndexingCode, formatSQL } from '@/utils/formatters'; import { getLatestBlockHeight } from '@/utils/getLatestBlockHeight'; import IndexerRunner from '@/utils/indexerRunner'; @@ -30,24 +27,25 @@ import { FileSwitcher } from './FileSwitcher'; import { GlyphContainer } from './GlyphContainer'; import ResizableLayoutEditor from './ResizableLayoutEditor'; +declare const monaco: any; const INDEXER_TAB_NAME = 'indexer.js'; const SCHEMA_TAB_NAME = 'schema.sql'; -declare const monaco: any; +const originalSQLCode = formatSQL(defaultSchema); +const originalIndexingCode = formatIndexingCode(defaultCode); +const pgSchemaTypeGen = new PgSchemaTypeGen(); const Editor: React.FC = (): ReactElement => { - const { indexerDetails, debugMode, isCreateNewIndexer } = useContext(IndexerDetailsContext); + const { indexerDetails, isCreateNewIndexer } = useContext(IndexerDetailsContext); + const storageManager = useMemo(() => { if (indexerDetails.accountId && indexerDetails.indexerName) { return new QueryAPIStorageManager(indexerDetails.accountId, indexerDetails.indexerName); } else return null; }, [indexerDetails.accountId, indexerDetails.indexerName]); - const [error, setError] = useState(); + const [indexerError, setIndexerError] = useState(); + const [schemaError, setSchemaError] = useState(); const [fileName, setFileName] = useState(INDEXER_TAB_NAME); - - const [originalSQLCode, setOriginalSQLCode] = useState(formatSQL(defaultSchema)); - const [originalIndexingCode, setOriginalIndexingCode] = useState(formatIndexingCode(defaultCode)); - const [indexingCode, setIndexingCode] = useState(originalIndexingCode); const [schema, setSchema] = useState(originalSQLCode); const [cursorPosition, setCursorPosition] = useState<{ lineNumber: number; column: number }>({ @@ -61,10 +59,9 @@ const Editor: React.FC = (): ReactElement => { const initialHeights = storageManager ? storageManager.getDebugList() || [] : []; const [heights, setHeights] = useState(initialHeights); - const [debugModeInfoDisabled, setDebugModeInfoDisabled] = useState(false); const [diffView, setDiffView] = useState(false); const [blockView, setBlockView] = useState(false); - const { openModal, showModal, data, message, hideModal } = useModal(); + const { showModal } = useModal(); const [isExecutingIndexerFunction, setIsExecutingIndexerFunction] = useState(false); const { height, currentUserAccountId }: { height?: number; currentUserAccountId?: string } = @@ -80,69 +77,60 @@ const Editor: React.FC = (): ReactElement => { }; const indexerRunner = useMemo(() => new IndexerRunner(handleLog), []); - const pgSchemaTypeGen = new PgSchemaTypeGen(); const disposableRef = useRef(null); const monacoEditorRef = useRef(null); - const parseGlyphError = ( - error?: { message: string }, - line?: { start: { line: number; column: number }; end: { line: number; column: number } }, - ) => { - const { line: startLine, column: startColumn } = line?.start || { line: 1, column: 1 }; - const { line: endLine, column: endColumn } = line?.end || { line: 1, column: 1 }; - const displayedError = error?.message || 'No Errors'; - - monacoEditorRef.current.deltaDecorations( - [decorations], - [ - { - range: new monaco.Range(startLine, startColumn, endLine, endColumn), - options: { - isWholeLine: true, - glyphMarginClassName: error ? 'glyphError' : 'glyphSuccess', - glyphMarginHoverMessage: { value: displayedError }, - }, - }, - ], - ); - }; - const debouncedValidateSQLSchema = useDebouncedCallback((_schema: string) => { const { error, location } = validateSQLSchema(_schema); error ? parseGlyphError(error as any, location as any) : parseGlyphError(); - return; + schemaErrorHandler(error); }, 500); const debouncedValidateCode = useDebouncedCallback((_code: string) => { - const { error: codeError } = validateJSCode(_code); - codeError ? setError(CODE_FORMATTING_ERROR_MESSAGE) : setError(undefined); + const { error } = validateJSCode(_code); + console.log(error); + indexerErrorHandler(error); }, 500); + const schemaErrorHandler = (schemaError: any): void => { + if (schemaError?.type === FORMATTING_ERROR_TYPE) setSchemaError(SCHEMA_FORMATTING_ERROR_MESSAGE); + if (schemaError?.type === TYPE_GENERATION_ERROR_TYPE) setSchemaError(SCHEMA_TYPE_GENERATION_ERROR_MESSAGE); + if (!schemaError) setSchemaError(undefined); + return; + }; + + const indexerErrorHandler = (indexerError: any): void => { + if (indexerError) setIndexerError(indexerError); + if (!indexerError) setIndexerError(undefined); + return; + }; + useEffect(() => { - if (indexerDetails.code != null) { + //* Load saved code from local storage if it exists else load code from context + const savedCode = storageManager?.getIndexerCode(); + if (savedCode) setIndexingCode(savedCode); + else if (indexerDetails.code) { const { data: formattedCode, error: codeError } = validateJSCode(indexerDetails.code); + indexerErrorHandler(codeError); + formattedCode && setIndexingCode(formattedCode); + } + //* Load saved cursor position from local storage if it exists else set cursor to start + const savedCursorPosition = storageManager?.getCursorPosition(); + if (savedCursorPosition) setCursorPosition(savedCursorPosition); - if (codeError) { - setError(CODE_FORMATTING_ERROR_MESSAGE); - } - - if (formattedCode) { - setOriginalIndexingCode(formattedCode); - setIndexingCode(formattedCode); - } + if (monacoEditorRef.current && fileName === INDEXER_TAB_NAME) { + monacoEditorRef.current.setPosition(savedCursorPosition || { lineNumber: 1, column: 1 }); + monacoEditorRef.current.focus(); } }, [indexerDetails.code]); useEffect(() => { - if (indexerDetails.schema != null) { + //* Load saved schema from local storage if it exists else load code from context + const savedSchema = storageManager?.getSchemaCode(); + if (savedSchema) setSchema(savedSchema); + else if (indexerDetails.schema) { const { data: formattedSchema, error: schemaError } = validateSQLSchema(indexerDetails.schema); - - if (schemaError?.type === FORMATTING_ERROR_TYPE) { - setError(SCHEMA_FORMATTING_ERROR_MESSAGE); - } else if (schemaError?.type === TYPE_GENERATION_ERROR_TYPE) { - setError(SCHEMA_TYPE_GENERATION_ERROR_MESSAGE); - } - + schemaErrorHandler(schemaError); formattedSchema && setSchema(formattedSchema); } }, [indexerDetails.schema]); @@ -151,35 +139,14 @@ const Editor: React.FC = (): ReactElement => { const { error: schemaError } = validateSQLSchema(schema); const { error: codeError } = validateJSCode(indexingCode); - if (schemaError?.type === FORMATTING_ERROR_TYPE) { - setError(SCHEMA_FORMATTING_ERROR_MESSAGE); - } else if (schemaError?.type === TYPE_GENERATION_ERROR_TYPE) { - setError(SCHEMA_TYPE_GENERATION_ERROR_MESSAGE); - } else if (codeError) setError(CODE_GENERAL_ERROR_MESSAGE); - else { - setError(undefined); - handleCodeGen(); + if (schemaError || codeError) { + if (schemaError) schemaErrorHandler(schemaError); + if (codeError) indexerErrorHandler(codeError); + return; } - if (fileName === SCHEMA_TAB_NAME) debouncedValidateSQLSchema(schema); - }, [fileName]); - useEffect(() => { - if (storageManager) { - const savedSchema = storageManager.getSchemaCode(); - const savedIndexingCode = storageManager.getIndexerCode(); - const savedCursorPosition = storageManager.getCursorPosition(); - - if (savedSchema) setSchema(savedSchema); - if (savedIndexingCode) setIndexingCode(savedIndexingCode); - if (savedCursorPosition) setCursorPosition(savedCursorPosition); - - if (monacoEditorRef.current && fileName === INDEXER_TAB_NAME) { - monacoEditorRef.current.setValue(savedIndexingCode || ''); - monacoEditorRef.current.setPosition(savedCursorPosition || { lineNumber: 1, column: 1 }); - monacoEditorRef.current.focus(); - } - } - }, [indexerDetails.accountId, indexerDetails.indexerName]); + handleCodeGen(); + }, [fileName]); useEffect(() => { cacheToLocal(); @@ -212,7 +179,7 @@ const Editor: React.FC = (): ReactElement => { storageManager.setIndexerCode(indexingCode); const newCursorPosition = monacoEditorRef.current.getPosition(); - storageManager.setCursorPosition(newCursorPosition); + if (fileName === INDEXER_TAB_NAME) storageManager.setCursorPosition(newCursorPosition); }; const handleCursorChange = () => { @@ -250,23 +217,9 @@ const Editor: React.FC = (): ReactElement => { const reformatAll = (indexingCode: string, schema: string) => { const { data: validatedCode, error: codeError } = validateJSCode(indexingCode); const { data: validatedSchema, error: schemaError } = validateSQLSchema(schema); - - let formattedCode = validatedCode; - let formattedSchema = validatedSchema; - if (codeError) { - formattedCode = indexingCode; - setError(CODE_FORMATTING_ERROR_MESSAGE); - } else if (schemaError?.type === FORMATTING_ERROR_TYPE) { - formattedSchema = schema; - setError(SCHEMA_FORMATTING_ERROR_MESSAGE); - } else if (schemaError?.type === TYPE_GENERATION_ERROR_TYPE) { - formattedSchema = schema; - setError(SCHEMA_TYPE_GENERATION_ERROR_MESSAGE); - } else { - setError(undefined); - } - - return { formattedCode, formattedSchema }; + indexerErrorHandler(codeError); + schemaErrorHandler(schemaError); + return { validatedCode, validatedSchema }; }; const handleCodeGen = () => { @@ -275,14 +228,37 @@ const Editor: React.FC = (): ReactElement => { attachTypesToMonaco(); // Just in case schema types have been updated but weren't added to monaco } catch (_error) { console.error('Error generating types for saved schema.\n', _error); - setError(SCHEMA_TYPE_GENERATION_ERROR_MESSAGE); + setSchemaError(SCHEMA_TYPE_GENERATION_ERROR_MESSAGE); } }; const handleFormating = () => { - const { formattedCode, formattedSchema } = reformatAll(indexingCode, schema); - formattedCode && setIndexingCode(formattedCode); - formattedSchema && setSchema(formattedSchema); + const { validatedCode, validatedSchema } = reformatAll(indexingCode, schema); + validatedCode && setIndexingCode(validatedCode); + validatedSchema && setSchema(validatedSchema); + }; + + const parseGlyphError = ( + error?: { message: string }, + line?: { start: { line: number; column: number }; end: { line: number; column: number } }, + ) => { + const { line: startLine, column: startColumn } = line?.start || { line: 1, column: 1 }; + const { line: endLine, column: endColumn } = line?.end || { line: 1, column: 1 }; + const displayedError = error?.message || ''; + + monacoEditorRef.current.deltaDecorations( + [decorations], + [ + { + range: new monaco.Range(startLine, startColumn, endLine, endColumn), + options: { + isWholeLine: true, + glyphMarginClassName: error ? 'glyphError' : 'glyphSuccess', + glyphMarginHoverMessage: { value: displayedError }, + }, + }, + ], + ); }; const handleEditorWillMount = (editor: any, monaco: any) => { @@ -298,14 +274,20 @@ const Editor: React.FC = (): ReactElement => { ); monacoEditorRef.current = editor; setDecorations(decorations); - - editor.setPosition(fileName === INDEXER_TAB_NAME ? cursorPosition : { lineNumber: 1, column: 1 }); - editor.focus(); } + editor.setPosition(fileName === INDEXER_TAB_NAME ? cursorPosition : { lineNumber: 1, column: 1 }); + editor.focus(); + monaco.languages.typescript.typescriptDefaults.addExtraLib( `${primitives}}`, 'file:///node_modules/@near-lake/primitives/index.d.ts', ); + monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ + target: monaco.languages.typescript.ScriptTarget.ES2016, + allowNonTsExtensions: true, + moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, + }); + setMonacoMount(true); }; @@ -337,18 +319,16 @@ const Editor: React.FC = (): ReactElement => { const handleOnChangeSchema = (_schema: string) => { setSchema(_schema); + storageManager?.setSchemaCode(schema); debouncedValidateSQLSchema(_schema); }; const handleOnChangeCode = (_code: string) => { setIndexingCode(_code); + storageManager?.setIndexerCode(indexingCode); debouncedValidateCode(_code); }; - const handleRegisterIndexerWithErrors = (args: any) => { - request('register-function', args); - }; - return ( <>
{ isUserIndexer={indexerDetails.accountId === currentUserAccountId} handleDeleteIndexer={handleDeleteIndexer} isCreateNewIndexer={isCreateNewIndexer} - error={error} + schemaError={schemaError} //Fork Indexer Modal indexingCode={indexingCode} setIndexingCode={setIndexingCode} @@ -381,12 +361,10 @@ const Editor: React.FC = (): ReactElement => { //Reset Indexer Modal setSchema={setSchema} setSchemaTypes={setSchemaTypes} - setOriginalIndexingCode={setOriginalIndexingCode} - setOriginalSQLCode={setOriginalSQLCode} //Publish Modal actionButtonText={'publish'} schema={schema} - setError={setError} + setSchemaError={setSchemaError} showModal={showModal} /> { height: '100%', }} > - {error && ( - setError(undefined)} - className="px-4 py-3 mb-4 font-semibold text-red-700 text-sm text-center border border-red-300 bg-red-50 rounded-lg shadow-md" - variant="danger" - > - {error} - - )} - {debugMode && !debugModeInfoDisabled && ( - setDebugModeInfoDisabled(true)} - variant="info" - > - To debug, you will need to open your browser console window in order to see the logs. - - )} - + {/* @ts-ignore remove after refactoring Resizable Editor to ts*/} { )}
- handleRegisterIndexerWithErrors(data)} - onCancelButtonPressed={hideModal} - onClose={hideModal} - /> ); }; -export default Editor; +export default React.memo(Editor); diff --git a/frontend/src/components/Editor/EditorComponents/FileSwitcher.jsx b/frontend/src/components/Editor/EditorComponents/FileSwitcher.jsx index 4f11341f9..265d90902 100644 --- a/frontend/src/components/Editor/EditorComponents/FileSwitcher.jsx +++ b/frontend/src/components/Editor/EditorComponents/FileSwitcher.jsx @@ -1,25 +1,48 @@ import React, { useContext } from 'react'; import { IndexerDetailsContext } from '@/contexts/IndexerDetailsContext'; +import AlertSquareIcon from '@/components/Common/Icons/AlertSquareIcon'; +import CheckMarkSquareIcon from '@/components/Common/Icons/CheckMarkSquareIcon'; -export function FileSwitcher({ fileName, setFileName }) { +import CustomTooltip, { TooltipDirection } from '@/components/Common/CustomTooltip'; +const IndexerErrorMessage = 'There was an error with the Indexer.'; + +export function FileSwitcher({ fileName, setFileName, schemaError, indexerError }) { const { isCreateNewIndexer } = useContext(IndexerDetailsContext); return ( -
+
{!isCreateNewIndexer && (