Ivan Asmer 4 years ago
parent
commit
19115630c6
2 changed files with 196 additions and 377 deletions
  1. 39 0
      src/App.css
  2. 157 377
      src/App.js

+ 39 - 0
src/App.css

@@ -37,6 +37,15 @@
     justify-content: space-between;
 }
 
+.GridHeaderItem {
+    width: 100%;
+    max-width: 100%;
+    overflow: hidden;
+    border: 1px solid #707070;
+    padding: 10px;
+    text-align: center;
+}
+
 .Row {
     display: flex;
     justify-content: space-between;
@@ -76,3 +85,33 @@ textarea {
     width: 100%;
     height: 100px;
 }
+
+aside {
+    /*float: left;*/
+    width: 25%;
+    text-align: left;
+    font-size: 1.5em;
+    display: flex;
+}
+
+aside div {
+    padding: 10px;
+}
+
+
+div.selected {
+    background-color: #FFA0A0;
+    border: 1px solid #707070;
+}
+
+content {
+    /*float: right;*/
+    width: 70%;
+    text-align: left;
+}
+
+input.Search {
+    width: 100%;
+}
+
+

+ 157 - 377
src/App.js

@@ -1,373 +1,21 @@
-import React, {useState, useEffect}  from 'react';
+import React, {useState, useEffect, useMemo, useRef}  from 'react';
 import logo from './logo.svg';
 import './App.css';
 
 import { GraphQLClient } from 'graphql-request';
 
+import createModels2 from './front-models'
+
 let gql = new GraphQLClient("/graphql", {headers: localStorage.authToken ? {Authorization: 'Bearer '+localStorage.authToken} : {}})
 
 
 //TODO: use graphql-tag to get types, or, at least detect query 
 //for proper binding between model objects and result of gql query
 
-async function createModels(gql, config={create: 'Upsert', update: 'Upsert', delete: "Delete",  findOne: "FindOne", find: 'Find', count: 'Count'}){
-    const getQuery = name => 
-    `query mutations{
-      __type(name: "${name}") {
-        name,
-        kind,
-        fields {
-          name,
-         type {
-            kind,
-            ofType{
-              name,
-                fields{
-                  name,
-                  type{
-                    name,
-                    kind
-                  }
-                }
-            }
-            name,
-            fields{
-              name,
-              type{
-                name,
-                kind
-              }
-            }
-          }
-          args{
-            name,
-            type{
-              name,
-                inputFields{
-                    name,
-                        type{
-                          name,
-                          kind,
-                          ofType{
-                            name
-                          }
-                        }
-                      }
-                    }
-                  }
-                }
-              }
-            }`
-    let  { __type: { fields: mutations }} = await gql.request(getQuery('Mutation'))
-    let  { __type: { fields: queries }}   = await gql.request(getQuery('Query'))
-    console.log(mutations)
-    console.log(queries)
-
-
-    let classes = {}
-
-    const createClass = (name, fields) => {
-        if (!(name in classes)) {
-            classes[name] = class {
-                constructor(data){
-                    Object.assign(this, data)
-                }
-
-                static get fields(){
-                    return fields
-                }
-            }
-            Object.defineProperty(classes[name], 'name', {value: name})
-        }
-        return classes[name]
-    }
-
-
-    for (let query of queries){
-        let className   = query.type.name   || query.type.ofType.name
-        if (className === 'Int'){ //TODO: fuck this
-            className = query.name.match(new RegExp(`([A-Za-z]+)(${Object.values(config).join('|')})`))[1]
-        }
-        let classFields = query.type.fields || query.type.ofType && query.type.ofType.fields
-
-
-        console.log(classFields)
-
-        let _class = createClass(className, classFields)
-
-        for (let [method, methodName] of Object.entries(config)){
-            if (!_class[method] && query.name.includes(methodName)){
-                let methods = {
-                    find(q, cursorCalls={}){
-                        let queryText = `query FMFind($q:${query.args[0].type.name}){
-                                ${query.name}(${query.args[0].name}: $q)
-                                {${classFields.filter(x => x.type.kind === 'SCALAR').map(x => x.name).join(',')}}
-                                }
-                            `
-                        console.log(queryText, {q: JSON.stringify([q, cursorCalls])})
-                        return gql.request(queryText, {q: JSON.stringify([q, cursorCalls])}).then(data =>{
-                            return data[query.name].map(record => new _class(record))
-                        })
-                    },
-                    findOne(query){
-
-                    },
-                    count(q, cursorCalls={}){
-                        let queryText = `query FMFind($q:${query.args[0].type.name}){
-                                ${query.name}(${query.args[0].name}: $q)
-                            }
-                            `
-                        return gql.request(queryText, {q: JSON.stringify([q, cursorCalls])}).then(data =>{
-                            return data[query.name]
-                        })
-                    }
-                }
-                _class[method] = methods[method]
-            }
-        }
-
-    }
-
-    return classes
-}
-
-
-async function createModels2(gql, config={create: 'Upsert', update: 'Upsert', delete: "Delete",  findOne: "FindOne", find: 'Find', count: 'Count'}){
-    const universeQuery = `query universe{
-  __schema{
-     types {
-        name,
-        kind,
-        inputFields{
-          name,
-         type {
-            kind,
-            ofType{
-              name,
-                fields{
-                  name,
-                  type{
-                    name,
-                    kind
-                  }
-                }
-            }
-            name,
-            fields{
-              name,
-              type{
-                name,
-                kind
-              }
-            }
-          }
-          
-                
-        }
-        fields {
-          name,
-         type {
-            kind,
-            ofType{
-              name,
-                fields{
-                  name,
-                  type{
-                    name,
-                    kind
-                  }
-                }
-            }
-            name,
-            fields{
-              name,
-              type{
-                name,
-                kind
-              }
-            }
-          }
-          args{
-            name,
-            type{
-              name,
-                inputFields{
-                    name,
-                        type{
-                          name,
-                          kind,
-                          ofType{
-                            name
-                          }
-                        }
-                      }
-                    }
-                  }
-                }
-              }
-  }}
-  `
-
-    const universe = await gql.request(universeQuery)
-    console.log(universe)
-    const types  = []
-    const inputs = []
-    for (let type of universe.__schema.types){
-        if (!["Query", "Mutation"].includes(type.name) && type.kind === "OBJECT" && !type.name.startsWith('__')) types.push(type)
-        if (!["Query", "Mutation"].includes(type.name) && type.kind === "INPUT_OBJECT" && !type.name.startsWith('__')) inputs.push(type)
-    }
-    console.log(types, inputs)
-
-    let classes = {}
-
-    const inTypes  = name => types.find(type => type.name === name)
-    const inInputs = name => inputs.find(type => type.name === name)
-
-    const projectionBuilder = (type, allFields=true) => {
-        if (!allFields && type.fields[0].name !== '_id') allFields = true
-        if (allFields)
-            return '{' + 
-                type.fields.map(field => { 
-                    return field.name + ((field.type.kind === 'OBJECT' && (inTypes(field.type.name)) && projectionBuilder(inTypes(field.type.name), false)) ||
-                    (field.type.kind === 'LIST'   && (inTypes(field.type.ofType.name)) && projectionBuilder(inTypes(field.type.ofType.name), false)) || '')
-                })
-                .join(',') 
-                + '}'
-        else return `{_id}`
-    }
 
-    const identityMap = {}
-
-
-    const createClass = (name, type, input) => {
-        if (!(name in classes)) {
-            classes[name] = class {
-                constructor(data={}, empty = false){
-                    if (data._id && data._id in identityMap)
-                        return identityMap[data._id]
-
-                    this.populate(data)
-
-                    this.empty = empty
-
-                    if (this._id) identityMap[this._id] = this
-                }
-
-                populate(data){
-                    type.fields.forEach(({name, type:{ ofType, kind, name:otherName}}) => {
-                        ({SCALAR(){
-                            if (data && typeof data === 'object' && name in data) this[name] = data[name]
-                        },
-                        LIST(){
-                            const otherType = inTypes(ofType.name)
-                            if (otherType  && data[name])
-                                this[name] = data[name].map(otherEntity => new classes[otherType.name](otherEntity, otherEntity._id && Object.keys(otherEntity).length === 1))
-                            else if (data && typeof data === 'object' && name in data) this[name] = data[name]
-                        },
-                        OBJECT(){
-                            const otherType = inTypes(otherName)
-                            if (otherType && data[name])
-                                this[name] = new classes[otherType.name](data[name], data[name]._id && Object.keys(data[name]).length === 1)
-                            else if (data && typeof data === 'object' && name in data) this[name] = data[name]
-                        }})[kind].call(this)
-                    })
-                }
-
-                get empty(){
-                    return this.then
-                }
-
-                set empty(value){
-                    if (value){
-                        this.then = async (onFullfilled, onRejected) => {
-                            const gqlQuery = `
-                                query ${name}FindOne($query: String){
-                                    ${name}FindOne(query: $query)
-                                        ${projectionBuilder(type)}
-                                    
-                                }
-                            `
-                            const data = await gql.request(gqlQuery, {query: JSON.stringify([{_id: this._id}])})
-                            this.populate(data[name + 'FindOne'])
-                            this.empty = false
-                            const thenResult = onFullfilled(this)
-                            if (thenResult && typeof thenResult === 'object' && typeof thenResult.then === 'function'){
-                                return await thenResult
-                            }
-                            return thenResult
-                        }
-                    }
-                    else delete this.then
-                }
-
-                static get type(){
-                    return type
-                }
-
-                static get input(){
-                    return input
-                }
-
-                async save(){
-                    if (this.empty) throw new ReferenceError('Cannot save empty object')
-                    const data = {}
-                    input.inputFields.forEach(({name, type:{ ofType, kind, name:otherName}}) => {
-                        ({SCALAR(){
-                            if (this[name]) data[name] = this[name] 
-                        },
-                        LIST(){
-                            const otherType = inInputs(ofType.name)
-                            if (otherType  && this[name] && this[name] instanceof Array)
-                                data[name] = this[name].map(otherEntity => (otherEntity._id ? {_id: otherEntity._id} : otherEntity))
-                        },
-                        INPUT_OBJECT(){
-                            const otherType = inInputs(otherName)
-                            if (otherType && this[name] && typeof this[name] === 'object')
-                                data[name] = (this[name]._id ? {_id: this[name]._id} : this[name])
-                        }})[kind].call(this)
-                    })
-                    const gqlQuery = `
-                                mutation ${name}Upsert($data: ${input.name}){
-                                    ${name}Upsert(${name.toLowerCase()}: $data)
-                                        ${projectionBuilder(type)}
-                                }
-                    `
-                    let result = await gql.request(gqlQuery, {data})
-                    this.populate(result)
-                }
-
-                static async find(query){
-                    const gqlQuery = `
-                        query ${name}Find($query: String){
-                            ${name}Find(query: $query)
-                                ${projectionBuilder(type)}
-                        }
-                    `
-                    let result = await gql.request(gqlQuery, {query: JSON.stringify(query)})
-                    return result[name + 'Find'].map(entity => new classes[name](entity))
-                }
-
-                static async count(query){
-                    const gqlQuery = `
-                        query ${name}Count($query: String){
-                            ${name}Count(query: $query)
-                        }
-                    `
-                    let result = await gql.request(gqlQuery, {query: JSON.stringify(query)})
-                    return result[name + 'Count']
-                }
-            }
-            Object.defineProperty(classes[name], 'name', {value: name})
-        }
-        return classes[name]
-    }
-
-    types.forEach((type) => createClass(type.name, type, inputs.find(input => input.name === `${type.name}Input`)))
-    return classes;
-}
 
 const dataReader = async () => {
-    const { PriceItem } = await createModels(gql)
+    const { PriceItem } = await createModels2(gql)
     let data            = await PriceItem.find({}, {limit: [10]})
     return {data, PriceItem }
 }
@@ -390,16 +38,8 @@ function ModelGrid({model, rowHeight, gridHeight, overload, query, cellDoubleCli
     const onScreenRowCount   = gridHeight/rowHeight
     const overloadedRowCount = overload * onScreenRowCount
 
+    const [totalCount, setTotalCount] = useState(-1)
 
-    const [oldQuery, setOldQuery] = useState()
-
-    const jsonQuery = JSON.stringify(query)
-
-    const [totalCount, setTotalCount] = useState()
-
-    if (!totalCount || oldQuery !== jsonQuery) { 
-        model && model.count(query).then(count => { setTotalCount(count); setOldQuery(jsonQuery)})
-    }
 
     const [gridContentRef, setGridContentRef] = useState()
     const [scroll, setScroll]                 = useState(0)
@@ -417,16 +57,21 @@ function ModelGrid({model, rowHeight, gridHeight, overload, query, cellDoubleCli
     const cursorCalls = {skip: [skip], limit: [overloadedRowCount]}
 
 
-    if (!model) return <>No model</>;
 
     if (sort.length) cursorCalls.sort = [sort]
 
-    if (!records || oldQuery !== jsonQuery) { 
-        model.find(query, cursorCalls)
-                                            .then(records => (setRecords(records), setOldQuery(jsonQuery)))
-    }
 
+    useEffect(() => {
+        if (totalCount > 0) { 
+            model.find(query, cursorCalls).then(records => setRecords(records))
+        }
+    },[model, query, sort, totalCount])
+
+    useEffect(() => {
+        model && model.count(query).then(count =>  setTotalCount(count))
+    },[model, query, totalCount])
 
+    if (!model) return <>No model</>;
     return(
         <>
             <div className='GridHeader'>
@@ -443,11 +88,11 @@ function ModelGrid({model, rowHeight, gridHeight, overload, query, cellDoubleCli
                 <div className='GridContent' 
                      style={{height: totalCount*rowHeight}}
                      ref={e => setGridContentRef(e)}>
-                    {records && records.map((record,i) => 
+                    {console.log(records), records && records.map((record,i) => 
                                                     <Row top={(skip)*rowHeight}>
                                                         {model.fields.map(field => 
                                                             <Cell onDoubleClick={e => cellDoubleClick(field.name, record[field.name], record._id)}>
-                                                                {record[field.name]}
+                                                                {record[field.name] === 'object' ? record[field.name].toString() :  record[field.name]}
                                                             </Cell>)}
                                                     </Row>)}
                 </div>
@@ -456,6 +101,141 @@ function ModelGrid({model, rowHeight, gridHeight, overload, query, cellDoubleCli
     );
 }
 
+const ModelListItem = "div"
+
+const ModelList = ({models, selected, onChange, Item=ModelListItem}) => {
+    return (
+        <aside>
+            {Object.entries(models).map(([name, model]) => {
+                return (
+                    <Item key={name} 
+                                   selected={name === selected}
+                                   className={name === selected && "selected"}
+                                   onClick = {() => onChange(name)}
+                                   >
+                        {name}
+                    </Item>
+                )
+            })}
+        </aside>
+    )
+}
+
+const Search = ({...props}) => 
+<input placeholder="Search..." {...props} className="Search"/>
+
+const Count = ({...props}) => <div className="Count" {...props} /> 
+
+const searchQueryBuilder = (search, model) => {
+    const queryRegexp = `/${search.trim().split(/\s+/).join('|')}/`
+    return {$or: model.fields.filter(field => field.type.kind == 'SCALAR').map(field => ({[field.name]: queryRegexp}))}
+}
+
+const GridHeaderItem = ({field, sort, ...props}) => 
+<div className="GridHeaderItem" {...props}>
+    {field.name in sort && sort[field.name] === -1 && '^ '}
+    {field.name}
+    {field.name in sort && sort[field.name] === 1 && ' v'}
+</div>
+
+
+
+const GridHeader = ({fields, sort, onSort}) => 
+<div className="GridHeader">
+    {fields.map(field => <GridHeaderItem field={field} sort={sort} onClick={() => onSort(field.name)}/>)}
+</div>
+
+const Grid = ({fields, sort, onSort}) => 
+    <div className='GridViewport' 
+         onScroll={e => {
+                if (Math.abs(e.target.scrollTop - scroll) > gridHeight*overload/3){  
+                    setScroll(e.target.scrollTop); 
+                    setRecords(null)
+                } }} 
+         style={{maxHeight: gridHeight, height: gridHeight}}>
+
+        <div className='GridContent' 
+             style={{height: totalCount*rowHeight}}
+             ref={e => setGridContentRef(e)}>
+            {console.log(records), records && records.map((record,i) => 
+                                            <Row top={(skip)*rowHeight}>
+                                                {model.fields.map(field => 
+                                                    <Cell onDoubleClick={e => cellDoubleClick(field.name, record[field.name], record._id)}>
+                                                        {record[field.name] === 'object' ? record[field.name].toString() :  record[field.name]}
+                                                    </Cell>)}
+                                            </Row>)}
+        </div>
+    </div>
+
+
+const ModelView = ({model, components:Components={Search, Count, GridHeader, Grid}, rowHeight, gridHeight, overload}) => {
+    const onScreenRowCount   = gridHeight/rowHeight
+    const overloadedRowCount = overload * onScreenRowCount
+    const [scroll, setScroll]                 = useState(0)
+
+
+
+    const  onScreenFirstRowIndex = Math.floor(scroll/rowHeight)
+    let skip                  =  onScreenFirstRowIndex  - (overloadedRowCount - onScreenRowCount)/2;
+
+    if (skip < 0) skip = 0
+
+    const [records, setRecords] = useState()
+
+
+    const [search, setSearch] = useState("")
+    const [count, setCount]   = useState()
+
+    const [query,  setQuery]  = useState({})
+    const [cursorCalls,  setCursorCalls]  = useState({sort:{}, skip: [skip], limit: [overloadedRowCount]})
+
+    console.log(cursorCalls)
+
+    const timeout = useRef(0)
+    useEffect(() => {
+        clearInterval(timeout.current)
+        timeout.current = setTimeout(() => {
+            setQuery(searchQueryBuilder(search, model))
+        }, 1000)
+    },[search, model])
+
+    useEffect(() => {
+        model.count(query, cursorCalls).then(count => setCount(count))
+        model.find(query, cursorCalls).then(records => setRecords(records))
+    }, [query, model, cursorCalls])
+
+    return (
+        <>
+                <Components.Search value={search} onChange={({target: {value}}) => setSearch(value)}/>
+                <Components.Count> 
+                    {count}
+                </Components.Count> 
+                <GridHeader fields={model.fields} 
+                            sort={cursorCalls.sort} 
+                            onSort={sort => setCursorCalls({...cursorCalls,
+                                sort: {[sort]: cursorCalls.sort[sort] === 1 ? -1 : 1}
+                            })}/>
+                <Grid records={records}/>
+        </>
+    )
+}
+
+const Admin = ({models, components:Components={ModelList, Search}}) => {
+    const [selected, setSelected] = useState()
+
+
+    return (
+        <>
+            <Components.ModelList models={models} onChange={(name) => setSelected(name)} selected={selected}/>
+            <content>
+                {selected && <ModelView model={models[selected]} /> }
+            </content>
+        </>
+    )
+}
+
+                //{selected && <ModelGrid key={selected} model={models[selected]} rowHeight={50} gridHeight={700} overload={5} query={query}/> }
+
 function App() {
     let [models, setModels] = useState()
     let [queryText,  setQueryText]  = useState('{manufacturerName: "TOYOTA"}')
@@ -466,15 +246,15 @@ function App() {
     const classes = models
     console.log(classes)
     if (classes && Object.keys(classes).length){
-        classes.Good.find([{}]).then(goods => console.log(goods))
-        classes.Image.count([{}]).then(count => console.log(count))
+        classes.Good.find().then(goods => console.log(goods))
+        classes.Image.find().then(images => console.log(images))
     }
 
     return (
         <div className="App">
-            {models && Object.entries(models).map(([key, model]) => <div>{key}</div>)}
-            {models && <ModelGrid model={models.jk} rowHeight={50} gridHeight={700} overload={5} query={query} 
-                        cellDoubleClick={(name, text, _id) => setQueryText(`{${name}:\`${text}\`}`)}/> }
+            {models && <Admin models={models} />}
+            {/*models && <ModelGrid model={models.Image} rowHeight={50} gridHeight={700} overload={5} query={query} 
+                        cellDoubleClick={(name, text, _id) => setQueryText(`{${name}:\`${text}\`}`)}/> */}
             <textarea onChange={e => setQueryText(e.target.value)} value={queryText} />
             <button onClick={() => setQuery(eval(`(${queryText})`))}>Run</button>
         </div>