From 38fea7684339344350c3ec9b2f578c8baacd41d4 Mon Sep 17 00:00:00 2001 From: David Mihal Date: Mon, 28 Mar 2022 12:08:17 +0300 Subject: [PATCH] Add structure from local-subgraphs --- components/SubgraphEditor/Editor.tsx | 127 +++++++++------------- components/SubgraphEditor/Tabs.tsx | 6 +- hooks/gql-compiler.tsx | 2 +- hooks/local-subgraphs.ts | 152 +++++++++------------------ pages/_app.tsx | 3 +- 5 files changed, 105 insertions(+), 185 deletions(-) diff --git a/components/SubgraphEditor/Editor.tsx b/components/SubgraphEditor/Editor.tsx index 1142c82..e23bec7 100644 --- a/components/SubgraphEditor/Editor.tsx +++ b/components/SubgraphEditor/Editor.tsx @@ -1,16 +1,12 @@ import React, { useState, useEffect, useRef } from 'react' import { ViewPort, Top, Fill, Bottom, BottomResizable, Right } from 'react-spaces' import { useRouter } from 'next/router' -import { LOG_LEVEL } from '@cryptostats/sdk' import { useWeb3React } from '@web3-react/core' import styled from 'styled-components' import { useENSName } from 'use-ens-name' import CodeEditor from 'components/CodeEditor' import ConnectionButton from 'components/ConnectionButton' -import { useSubgraph, newModule, getStorageItem, FileData } from 'hooks/local-subgraphs' -import { useCompiler } from 'hooks/compiler' -import { useConsole } from 'hooks/console' -import { emptyMapping, emptySchema } from 'resources/templates' +import { useLocalSubgraph, newSubgraph, DEFAULT_MAPPING } from 'hooks/local-subgraphs' import PrimaryFooter from './PrimaryFooter' import { Tabs, TabState } from './Tabs' import EditorModal from './EditorModal' @@ -18,12 +14,12 @@ import NewAdapterForm from './NewAdapterForm' import { MarkerSeverity } from './types' import ErrorPanel from './ErrorPanel' import { usePlausible } from 'next-plausible' -import { useEditorState } from '../../hooks/editor-state' import EditorControls from './EditorControls' import Console from './Console' import BottomTitleBar, { BottomView } from './BottomTitleBar' import SaveMessage from './SaveMessage' import ImageLibrary from './ImageLibrary/ImageLibrary' +import { useEditorState } from 'hooks/editor-state' const Header = styled(Top)` background-image: url('/editor_logo.png'); @@ -130,49 +126,38 @@ const PrimaryFill = styled(FillWithStyledResize)` } ` -const formatLog = (val: any) => { - if (val.toString() === '[object Object]') { - return JSON.stringify(val, null, 2) - } else if (typeof val === 'string') { - return `"${val}"` - } - return val.toString() -} +const SCHEMA_FILE_NAME = 'schema.graphql' const Editor: React.FC = () => { const router = useRouter() const plausible = usePlausible() - const [subgraphFiles, setSubgraphFiles] = useEditorState( - 'test', - [ - { type: 'schema', fileId: null, open: true, focused: true }, - { type: 'mapping', fileId: null, open: true, focused: false }, - ], - 'subgraph-editor-state' - ) - const { save } = useSubgraph() - - const openTabs = (subgraphFiles || []) - .filter(sgf => sgf.open) - .map(ot => ({ ...ot, ...(ot.fileId && { fileData: getStorageItem(ot.fileId) as FileData }) })) - const focusedTab = openTabs?.find(sgf => sgf.focused)! - - useEffect(() => { - setSubgraphFiles((prev: TabState[]) => - prev.map(pi => - pi.open && !pi.fileId - ? { - ...pi, - fileId: newModule( - pi?.type === 'schema' - ? { code: emptySchema, name: 'New Schema' } - : { code: emptyMapping, name: 'New Mapping' } - ), - } - : pi - ) - ) - }, []) + const [file, setFile] = useEditorState('subgraph-file') + const [tab, setTab] = useState(SCHEMA_FILE_NAME) + + const { saveSchema, saveMapping, subgraph } = useLocalSubgraph(file) + + const subgraphFiles: (TabState & { value: string })[] = subgraph + ? [ + { + type: 'schema', + name: 'Schema', + fileId: SCHEMA_FILE_NAME, + open: true, + focused: tab === SCHEMA_FILE_NAME, + value: subgraph.schema, + }, + { + type: 'mapping', + name: 'Mapping', + fileId: DEFAULT_MAPPING, + open: true, + focused: tab !== SCHEMA_FILE_NAME, + value: subgraph.mappings[DEFAULT_MAPPING], + }, + ] + : [] + + const focusedTab = subgraphFiles.find(sgf => sgf.focused)! const [started, setStarted] = useState(false) const [newAdapterModalOpen, setNewAdapterModalOpen] = useState(false) @@ -181,8 +166,6 @@ const Editor: React.FC = () => { const [bottomView, setBottomView] = useState(BottomView.NONE) const editorRef = useRef(null) - const { evaluate } = useCompiler() - const { addLine } = useConsole() const { account } = useWeb3React() const name = useENSName(account) @@ -207,9 +190,6 @@ const Editor: React.FC = () => { return null } - const saveCode = (code: string) => - save(focusedTab.fileId!, code, focusedTab.fileData!.name, focusedTab.fileData!.version) - return (
@@ -231,13 +211,9 @@ const Editor: React.FC = () => { - setSubgraphFiles(prev => - prev.map(p => ({ ...p, focused: p.fileId === fileId })) - ) - } + openTabs={subgraphFiles} + current={tab} + onSelect={fileId => setTab(fileId || SCHEMA_FILE_NAME)} /> @@ -246,36 +222,30 @@ const Editor: React.FC = () => { - {focusedTab?.fileData ? ( + {subgraph ? ( { editorRef.current = editor }} - onChange={saveCode} - onValidated={(code: string, markers: any[]) => { + onChange={(code: string) => + tab === SCHEMA_FILE_NAME ? saveSchema(code) : saveMapping(tab, code) + } + onValidated={(_code: string, markers: any[]) => { setMarkers(markers) if ( markers.filter((marker: any) => marker.severity === MarkerSeverity.Error) .length === 0 ) { - evaluate({ - code, - isTS: true, - onLog: (level: LOG_LEVEL, ...args: any[]) => - addLine({ - level: level.toString(), - value: args.map(formatLog).join(' '), - }), - }) + // Evaluate code } }} /> ) : ( -
test
+
Empty state
)}
@@ -304,9 +274,9 @@ const Editor: React.FC = () => { - {focusedTab?.fileData ? ( + {subgraph ? ( setBottomView(BottomView.ERRORS)} onConsoleClick={() => setBottomView(BottomView.CONSOLE)} @@ -333,19 +303,20 @@ const Editor: React.FC = () => { onClick: () => setNewAdapterModalOpen(false), }, { - label: 'Create Blank Adapter', + label: 'Create Blank Subgraph', onClick: () => { - plausible('new-adapter', { + plausible('new-subgraph', { props: { template: 'blank', }, }) - // setMappingFileName(newModule(emptyMapping)) + setFile(newSubgraph()) setNewAdapterModalOpen(false) }, }, - ]}> + ]} + > { // setMappingFileName(fileName) diff --git a/components/SubgraphEditor/Tabs.tsx b/components/SubgraphEditor/Tabs.tsx index 6032dd6..b54c782 100644 --- a/components/SubgraphEditor/Tabs.tsx +++ b/components/SubgraphEditor/Tabs.tsx @@ -1,7 +1,6 @@ import React from 'react' import styled from 'styled-components' import CloseIcon from 'components/CloseIcon' -import { FileData } from 'hooks/local-subgraphs' const TabRow = styled.div` display: flex; @@ -42,6 +41,7 @@ const CloseButton = styled.button` export interface TabState { type: 'schema' | 'mapping' + name?: string fileId?: string | null open: boolean focused: boolean @@ -51,7 +51,7 @@ interface TabsProps { current?: string | null onClose?: (fileId?: string) => void onSelect: (fileId?: string) => void - openTabs: (TabState & { fileData?: FileData })[] + openTabs: TabState[] } export const Tabs = ({ onClose, openTabs, onSelect }: TabsProps) => { @@ -59,7 +59,7 @@ export const Tabs = ({ onClose, openTabs, onSelect }: TabsProps) => { {openTabs.map(ot => ( onSelect(ot.fileId!)}> -
{ot.fileData?.name || ''}
+
{ot.name || ot.fileId || 'File'}
{ot.focused && onClose && ( onClose(ot.fileId!)}> diff --git a/hooks/gql-compiler.tsx b/hooks/gql-compiler.tsx index 65dc84e..07aebe8 100644 --- a/hooks/gql-compiler.tsx +++ b/hooks/gql-compiler.tsx @@ -58,7 +58,7 @@ export const useGqlCompiler = () => { const module = list.addAdaptersWithCode(compiledCode || code) setState({ ...DEFAULT_STATE, code, module, compiledCode, list }) - } catch (e) { + } catch (e: any) { console.warn(e) setState({ ...DEFAULT_STATE, error: e.message }) } diff --git a/hooks/local-subgraphs.ts b/hooks/local-subgraphs.ts index 80985cf..574bd32 100644 --- a/hooks/local-subgraphs.ts +++ b/hooks/local-subgraphs.ts @@ -1,29 +1,41 @@ import { useEffect, useState } from 'react' -// @ts-ignore -import sampleModule from '!raw-loader!../components/sample-module.txt' const storageKey = 'localSubgraphs' +export const DEFAULT_MAPPING = 'mapping.ts' + interface Publication { cid: string version: string } -export interface FileData { - code: string +export interface Contract { + name: string + addresses: { [chain: string]: string } + startBlocks: { [chain: string]: number } + abi: string + customAbi: boolean + events: { signature: string; handler: string }[] +} + +export interface SubgraphData { name: string | null + // For now, we only use one mapping (DEFAULT_MAPPING), but it's coded this way to be forward-compatable + mappings: { [name: string]: string } + schema: string version: string | null + contracts: Contract[] publications: Publication[] } -export interface AdapterWithID extends Adapter { +export interface SubgraphWithID extends SubgraphData { id: string } const getStorage = () => typeof window === 'undefined' ? {} : JSON.parse(window.localStorage.getItem(storageKey) || '{}') -export const getStorageItem = (id: string) => getStorage()[id] || null +const getStorageItem = (id: string) => getStorage()[id] || null const setStorageItem = (id: string, value: any) => { window.localStorage.setItem( @@ -40,7 +52,7 @@ const adapterListUpdaters: Function[] = [] const updateAdapterLists = () => adapterListUpdaters.map(updater => updater()) -export const useAdapterList = (): AdapterWithID[] => { +export const useSubgraphList = (): SubgraphWithID[] => { const _update = useState({})[1] const update = () => _update({}) @@ -60,126 +72,62 @@ export const useAdapterList = (): AdapterWithID[] => { const randomId = () => Math.floor(Math.random() * 1000000).toString(16) -interface NewModuleProps { - code: string - publications?: Publication[] - name?: string -} - -export const newModule = ({ - name = 'New Module', - code = sampleModule, - publications = [], -}: NewModuleProps) => { +export const newSubgraph = (mapping = '', schema = '', publications: Publication[] = []) => { const id = randomId() - const adapter: FileData = { - code, - name, + const subgraph: SubgraphData = { + mappings: { [DEFAULT_MAPPING]: mapping }, + schema, + name: 'New Subgraph', + contracts: [], publications, version: null, } - setStorageItem(id, adapter) + setStorageItem(id, subgraph) return id } -export const useSubgraph = () => { +export const useLocalSubgraph = (id?: string | null) => { const update = useState({})[1] - const save = (id: string, code: string, name: string | null, version: string | null) => { + const saveSchema = (schema: string) => { if (!id) { throw new Error('ID not set') } const adapter = getStorageItem(id) - const newAdapter: Adapter = { ...adapter, code, name, version } + const newAdapter: SubgraphData = { ...adapter, schema } setStorageItem(id, newAdapter) update({}) return id } - const publish = async ({ - // signature, - // hash, - // signer, - }: { signature?: string; signer?: string | null; hash?: string | null } = {}) => { - // if (!id) { - // throw new Error('ID not set') - // } - // const adapter = getStorageItem(id) - // const previousVersion = - // adapter?.publications && adapter.publications.length > 0 - // ? adapter.publications[adapter.publications.length - 1] - // : null - // if (previousVersion && adapter.version === previousVersion.version) { - // throw new Error(`Version ${adapter.version} is already published`) - // } - // const req = await fetch('/api/upload-adapter', { - // method: 'POST', - // headers: { - // 'Content-type': 'application/json', - // }, - // body: JSON.stringify({ - // code: adapter.code, - // version: adapter.version, - // previousVersion: previousVersion?.cid || null, - // language: 'typescript', - // signature, - // hash, - // signer, - // }), - // }) - // const response = await req.json() - // if (!response.success) { - // throw new Error(response.error) - // } - // const newAdapter: Adapter = { - // ...adapter, - // code: adapter.code, - // name: adapter.name, - // publications: [ - // ...(adapter.publications || []), - // { cid: response.codeCID, version: adapter.version, signature }, - // ], - // } - // setStorageItem(id, newAdapter) - // update({}) - // return response - } + const saveMapping = (fileName: string, mapping: string) => { + if (!id) { + throw new Error('ID not set') + } - const getSignableHash = async () => { - // if (!id) { - // throw new Error('ID not set') - // } - // const adapter = getStorageItem(id) - // const previousVersion = - // adapter?.publications && adapter.publications.length > 0 - // ? adapter.publications[adapter.publications.length - 1] - // : null - // if (previousVersion && adapter.version === previousVersion.version) { - // throw new Error(`Version ${adapter.version} is already published`) - // } - // const req = await fetch('/api/prepare-adapter', { - // method: 'POST', - // headers: { - // 'Content-type': 'application/json', - // }, - // body: JSON.stringify({ - // code: adapter.code, - // version: adapter.version, - // previousVersion: previousVersion?.cid || null, - // language: 'typescript', - // }), - // }) - // const { hash } = await req.json() - // const message = `CryptoStats Adapter Hash: ${hash}` - // return message + const adapter = getStorageItem(id) + + const newSubgraph: SubgraphData = { + ...adapter, + mappings: { ...adapter.mappings, [fileName]: mapping }, + } + + setStorageItem(id, newSubgraph) + update({}) + + return id } - const getDataFile = (id: string) => getStorageItem(id) + const subgraph = id ? (getStorageItem(id) as SubgraphData) : null - return { save, publish, getSignableHash, getDataFile } + return { + subgraph, + saveSchema, + saveMapping, + } } diff --git a/pages/_app.tsx b/pages/_app.tsx index 8a66232..ab51006 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -120,7 +120,8 @@ const App: React.FC = ({ Component, pageProps }) => { new ethers.providers.Web3Provider(provider)}> + getLibrary={(provider: any) => new ethers.providers.Web3Provider(provider)} + >