Browse Source

working on different things

Tomi Cvetic 5 years ago
parent
commit
790ef95c52

File diff suppressed because it is too large
+ 356 - 320
backend/database/generated/prisma-client/index.d.ts


+ 0 - 4
backend/database/generated/prisma-client/index.js

@@ -40,10 +40,6 @@ var models = [
     name: "InstrumentCommand",
     embedded: false
   },
-  {
-    name: "InstrumentSubsystem",
-    embedded: false
-  },
   {
     name: "Instrument",
     embedded: false

File diff suppressed because it is too large
+ 293 - 323
backend/database/generated/prisma-client/prisma-schema.js


File diff suppressed because it is too large
+ 450 - 125
backend/database/generated/prisma.graphql


+ 17 - 16
backend/datamodel.prisma

@@ -1,8 +1,12 @@
 # Generic Types
 
+"Multi-purpose meta information field"
 type Meta {
+  "Unique id in the database"
   id: ID! @unique
+  "Key for the meta information"
   key: String!
+  "Value of the meta information"
   value: String!
 }
 
@@ -13,6 +17,7 @@ type User {
   abbreviation: String!
   password: String!
   images: [File]!
+  comments: [Comment]! @relation(name: "CommentAuthor", onDelete: CASCADE)
 }
 
 type File {
@@ -22,6 +27,7 @@ type File {
   description: String
   filename: String!
   mimetype: String!
+  encoding: String!
   size: Int!
 }
 
@@ -29,7 +35,7 @@ type Comment {
   id: ID! @unique
   text: String
   createdAt: DateTime!
-  previousVersion: Comment
+  author: User! @relation(name: "CommentAuthor", onDelete: SET_NULL)
 }
 
 type Event {
@@ -68,6 +74,8 @@ type InstrumentParameter {
   description: String
   type: String!
   values: String
+  instrument: Instrument! @relation(name: "Parameters", onDelete: SET_NULL)
+  commands: [InstrumentCommand]! @relation(name: "CommandParameters", onDelete: SET_NULL)
 }
 
 type InstrumentCommand {
@@ -75,35 +83,28 @@ type InstrumentCommand {
   tag: String!
   name: String
   description: String!
-  instrument: Instrument!
   readString: String
   writeString: String
-  parameters: [InstrumentParameter]!
-}
-
-type InstrumentSubsystem {
-  id: ID! @unique
-  name: String
-  description: String!
-  commands: [InstrumentCommand]!
-  parameters: [InstrumentParameter]!
-  subsystems: [InstrumentSubsystem]!
+  subsystem: String
+  parameters: [InstrumentParameter]! @relation(name: "CommandParameters", onDelete: SET_NULL)
 }
 
 type Instrument {
   id: ID! @unique
   name: String!
+  manufacturer: String!
   description: String
-  documents: [File]!
+  picture: ID
+  documents: [File]! 
   interfaces: [String]!
   commands: [InstrumentCommand]!
-  parameters: [InstrumentParameter]!
-  subsystems: [InstrumentSubsystem]!
+  parameters: [InstrumentParameter]! @relation(name: "Parameters", onDelete: CASCADE)
+  instances: [InstrumentInstance]! @relation(name: "InstrumentInstances", onDelete: CASCADE)
 }
 
 type InstrumentInstance {
   id: ID! @unique
-  instrument: Instrument!
+  instrument: Instrument! @relation(name: "InstrumentInstances", onDelete: SET_NULL)
   identifier: String!
   label: String
   location: String

+ 13 - 0
backend/package-lock.json

@@ -5770,6 +5770,11 @@
       "dev": true,
       "optional": true
     },
+    "nanoid": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.0.1.tgz",
+      "integrity": "sha512-k1u2uemjIGsn25zmujKnotgniC/gxQ9sdegdezeDiKdkDW56THUMqlz3urndKCXJxA6yPzSZbXx/QCMe/pxqsA=="
+    },
     "nanomatch": {
       "version": "1.2.13",
       "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@@ -7691,6 +7696,14 @@
       "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
       "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
     },
+    "shortid": {
+      "version": "2.2.14",
+      "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.14.tgz",
+      "integrity": "sha512-4UnZgr9gDdA1kaKj/38IiudfC3KHKhDc1zi/HSxd9FQDR0VLwH3/y79tZJLsVYPsJgIjeHjqIWaWVRJUj9qZOQ==",
+      "requires": {
+        "nanoid": "^2.0.0"
+      }
+    },
     "signal-exit": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",

+ 1 - 0
backend/package.json

@@ -22,6 +22,7 @@
     "prisma-client-lib": "^1.30.1",
     "python-shell": "^1.0.7",
     "semaphore-async-await": "^1.5.1",
+    "shortid": "^2.2.14",
     "standard": "^12.0.1"
   },
   "devDependencies": {

+ 5 - 3
backend/pdfScan.js

@@ -6,9 +6,11 @@ const options = {
   type: 'text' // or 'ocr'
 }
 
-function getSCPICommands (pages) {
-  const scpiCommon = /(\*\w+\??)/g
-  // const header = ``
+const
+
+function getSCPICommands(pages) {
+  const scpiCommon = `\*\w+\??`
+  const header = `:?(?:\[\w+:\])?(?:)`
   const scpi = /((?:\*\w+|(?:\[?\w+\]?)(?=:\w+)\]?)(?:\[?:\w+\]?)*\??)(?:\s+(<?\w+>?)(?:[,|]\s*(<?\w+>?))*)?/g
 
   const scpiLines = []

+ 29 - 0
backend/schema.graphql

@@ -1,15 +1,44 @@
 # import * from './database/generated/prisma.graphql'
 scalar Upload
 
+input InstrumentParameterInput {
+  id: ID
+  tag: String!
+  name: String
+  description: String
+  type: String!
+  values: String
+}
+
+input InstrumentCommandInput {
+  id: ID
+  tag: String!
+  name: String
+  description: String!
+  readString: String
+  writeString: String
+  subsystem: String
+  parameters: [ID]!
+}
+
+input FileUpload {
+  id: ID
+  name: String!
+  description: String! 
+  file: Upload!
+}
+
 type Query {
   projects(where: ProjectWhereInput, orderBy: ProjectOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Project]!
   projectVersions(where: ProjectVersionWhereInput, orderBy: ProjectVersionOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [ProjectVersion]!
   instruments(where: InstrumentWhereInput, orderBy: InstrumentOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Instrument]!
+  instrument(id: ID!): Instrument
   uploads: [File]
   me: User!
 }
 
 type Mutation {
+  createInstrument(id: ID, name: String!, manufacturer: String!, description: String, picture: ID, interfaces: [String]!, documents: [FileUpload]!, commands: [InstrumentCommandInput]!): Instrument!
   createCharacterization(name: String!, projectVersion: ID!): Characterization!
   createUser(name: String!, email: String!, abbreviation: String!, password: String!): User!
   createProject(name: String!, abbreviation: String!, description: String): Project!

+ 99 - 52
backend/src/interfaces.js

@@ -55,8 +55,6 @@ const typeDefs = `
     exitCode: Int
     spawnfile: String!
     spawnargs: [String]!
-    error: [String]!
-    data: [String]!
   }
 
   type Option {
@@ -83,7 +81,12 @@ const typeDefs = `
     workerProcess: WorkerProcess
   }
 
+  type Interface {
+    name: String!
+  }
+
   extend type Query {
+    interfaces: [Interface]!
     workers: [Worker]!
     worker(id: ID, interfaceName: String): Worker!
     ports(interfaceName: String, force: Boolean): [Port]!
@@ -110,7 +113,9 @@ async function findWorkers () {
 
   // 2. Find all files in ./python_workers that end in _worker.py
   const fileNames = await readdir(WORKER_DIR)
-  const workerFiles = fileNames.filter(fileName => fileName.includes('_worker.py'))
+  const workerFiles = fileNames.filter(fileName =>
+    fileName.includes('_worker.py')
+  )
 
   // 3. For every worker script
   const workerPromises = workerFiles.map(async workerFile => {
@@ -118,7 +123,9 @@ async function findWorkers () {
     const workerScript = `${WORKER_DIR}/${workerFile}`
     // a. Find out if it was modified.
     const { mtime } = await stat(workerScript)
-    const foundWorker = state.workers.find(worker => worker.interfaceName === interfaceName)
+    const foundWorker = state.workers.find(
+      worker => worker.interfaceName === interfaceName
+    )
     if (foundWorker) {
       // b. If it was modified, save the modification time.
       if (foundWorker.mtime < mtime) foundWorker.updated = mtime
@@ -156,12 +163,37 @@ async function workers (parent, args, context, info) {
 async function worker (parent, args, context, info) {
   await findWorkers()
   const { id, interfaceName } = args
-  if (!id && !interfaceName) throw new Error('Either id or interfaceName needs to be provided!')
-  const worker = state.workers.find(worker => worker.id === id || worker.interfaceName === interfaceName)
-  if (!worker) throw new Error(`Worker id=${id}, interfaceName=${interfaceName} not found`)
+  if (!id && !interfaceName) {
+    throw new Error('Either id or interfaceName needs to be provided!')
+  }
+  const worker = state.workers.find(
+    worker => worker.id === id || worker.interfaceName === interfaceName
+  )
+  if (!worker) {
+    throw new Error(`Worker id=${id}, interfaceName=${interfaceName} not found`)
+  }
   return worker
 }
 
+function workerProcess (parent, args, ctx, info) {
+  const {
+    killed,
+    exitCode,
+    signalCode,
+    spawnargs,
+    spawnfile,
+    pid
+  } = parent.pythonShell
+  return {
+    pid,
+    killed,
+    exitCode,
+    signalCode,
+    spawnfile,
+    spawnargs
+  }
+}
+
 /**
  * PORTS SECTION
  */
@@ -174,13 +206,15 @@ async function findPorts (interfaceName) {
   // if (state.lastScan.ports + 1000 > Date.now()) return null
   // state.lastScan.ports = Date.now()
 
-  const portsPromises = state.workers.map(worker => {
-
-  })
+  const portsPromises = state.workers.map(worker => {})
   // Generate all ports for the interface
-  const iface = state.interfaces.find(iface => iface.interfaceName === interfaceName)
+  const iface = state.interfaces.find(
+    iface => iface.interfaceName === interfaceName
+  )
 
-  const { data, error, pythonError } = await iface.worker.send({ type: 'ports' })
+  const { data, error, pythonError } = await iface.worker.send({
+    type: 'ports'
+  })
   if (error) throw new Error(error)
   if (pythonError) throw new Error(pythonError)
   console.log(data)
@@ -191,7 +225,7 @@ async function findPorts (interfaceName) {
     const newPort = {
       id,
       interfaceName,
-      host: os.hostname(),
+      host: HOST,
       ...port
     }
     state.ports.push(newPort)
@@ -215,15 +249,23 @@ async function ports (parent, args, ctx, info) {
   }
 }
 
+/**
+ * OPTIONS SECTION
+ */
 async function findOptions (interfaceName) {
-  const iface = state.interfaces.find(iface => iface.interfaceName === interfaceName)
-  const { data, error, pythonError } = await iface.worker.send({ type: 'options' })
+  const iface = state.interfaces.find(
+    iface => iface.interfaceName === interfaceName
+  )
+  const { data, error, pythonError } = await iface.worker.send({
+    type: 'options'
+  })
   if (error) throw new Error(error)
   if (pythonError) throw new Error(pythonError)
   iface.options.push(...data)
 }
 
 async function options (parent, args, ctx, info) {
+  if (!parent) throw new Error('Parent needed.')
   const iface = state.interfaces.find(iface => iface.interfaceName === parent)
 
   // Try to find options if necessary
@@ -231,40 +273,15 @@ async function options (parent, args, ctx, info) {
   return iface.options
 }
 
-function workerProcess (parent, args, ctx, info) {
-  const { killed, exitCode, signalCode, spawnargs, spawnfile, pid } = parent.pythonShell
-  const { error, data } = parent
-  return {
-    pid,
-    killed,
-    exitCode,
-    signalCode,
-    spawnfile,
-    spawnargs,
-    error,
-    data
-  }
-}
-
-async function connections (parent, args, ctx, info) {
-  if (parent) {
-    const iface = state.interfaces.find(iface => iface.interfaceName === parent)
-    return iface.connections
-  } else {
-    return state.connections
-  }
-}
-
-async function connection (parent, args, context, info) {
-  const connection = state.connections.find(connection => connection.id === args.id)
-  return connection
-}
-
 async function connect (parent, args, ctx, info) {
   const { interfaceName, device } = args
-  const iface = state.interfaces.find(iface => iface.interfaceName === interfaceName)
+  const iface = state.interfaces.find(
+    iface => iface.interfaceName === interfaceName
+  )
   const id = md5(interfaceName + device)
-  if (iface.connections.find(connection => connection.id === id)) throw new Error('already connected.')
+  if (iface.connections.find(connection => connection.id === id)) {
+    throw new Error('already connected.')
+  }
   const pythonWorker = new PythonWorker(iface.workerScript)
   const spawnData = await pythonWorker.spawn()
   if (spawnData.error) throw new Error(spawnData.error)
@@ -272,11 +289,15 @@ async function connect (parent, args, ctx, info) {
     id,
     device,
     interfaceName,
-    host: os.hostname(),
+    host: HOST,
     worker: pythonWorker,
-    workerProcess: (parent, args, context, info) => workerProcess(pythonWorker, args, context, info)
+    workerProcess: (parent, args, context, info) =>
+      workerProcess(pythonWorker, args, context, info)
   }
-  const connectionData = await connection.worker.send({ type: 'connect', device })
+  const connectionData = await connection.worker.send({
+    type: 'connect',
+    device
+  })
   if (connectionData.error) throw new Error(connectionData.error)
   if (connectionData.pythonError) throw new Error(connectionData.pythonError)
   iface.connections.push(connection)
@@ -284,10 +305,32 @@ async function connect (parent, args, ctx, info) {
   return connection
 }
 
+async function connections (parent, args, ctx, info) {
+  if (parent) {
+    const iface = state.interfaces.find(iface => iface.interfaceName === parent)
+    return iface.connections
+  } else {
+    return state.connections
+  }
+}
+
+async function connection (parent, args, context, info) {
+  const connection = state.connections.find(
+    connection => connection.id === args.id
+  )
+  return connection
+}
+
 async function connectionCommand (parent, args, ctx, info) {
   const { connectionId, type, string, options } = args
-  const connection = state.connections.find(connection => connection.id === connectionId)
-  const { data, error, pythonError } = await connection.worker.send({ type, string, options })
+  const connection = state.connections.find(
+    connection => connection.id === connectionId
+  )
+  const { data, error, pythonError } = await connection.worker.send({
+    type,
+    string,
+    options
+  })
   if (error) throw new Error(JSON.stringify(error))
   if (pythonError) throw new Error(JSON.stringify(pythonError))
   return data.response
@@ -310,7 +353,10 @@ async function spawnWorker (parent, args, ctx, info) {
   console.log(data, error, pythonError)
   if (error) throw new Error(JSON.stringify(error))
   if (pythonError) throw new Error(JSON.stringify(pythonError))
-  const connectionData = await connection.worker.send({ type: 'connect', device: connection.device })
+  const connectionData = await connection.worker.send({
+    type: 'connect',
+    device: connection.device
+  })
   if (connectionData.error) throw new Error(connectionData.error)
   if (connectionData.pythonError) throw new Error(connectionData.pythonError)
   return connection.workerProcess()
@@ -328,6 +374,7 @@ async function killWorker (parent, args, ctx, info) {
 
 const resolvers = {
   Query: {
+    interfaces: () => [{ name: 'serial' }, { name: 'usbtmc' }],
     workers,
     worker,
     ports,

+ 22 - 19
backend/src/pythonWorker.js

@@ -14,32 +14,26 @@ function PythonWorker (workerScript, shellOptions) {
   this.pythonShell = null
 
   this.transaction = async (func, args) => {
-    console.log('transaction.')
     // 1. Block new commands
     await this.commandLock.acquire()
     // 2. Send data to Python shell (lock will be released by data/error event)
     await this.pythonLock.acquire()
-    console.log('function call')
     func(args)
-    console.log('function ended', this.pythonLock)
     // 3. Wait for data from the Python shell
     await this.pythonLock.acquire()
     const pythonError = this.error.pop()
     const workerResult = this.data.pop()
     this.pythonLock.release()
-    console.log('results', { pythonError, ...workerResult })
     // 4. Unblock new commands
     this.commandLock.release()
     // 5. Return result
-    console.log('transaction done.', { ...workerResult, pythonError })
     return { ...workerResult, pythonError }
   }
 
-  this.isProcessRunning = () => (
+  this.isProcessRunning = () =>
     this.pythonShell &&
     !this.pythonShell.killed &&
-    (this.pythonShell.exitCode !== null)
-  )
+    this.pythonShell.exitCode !== null
 
   // Use send a command to the python worker.
   this.send = command => {
@@ -54,9 +48,7 @@ function PythonWorker (workerScript, shellOptions) {
   }
 
   this.spawn = async () => {
-    console.log('spawning?')
     if (this.isProcessRunning()) return { error: 'Process already running' }
-    console.log('spawning!')
 
     return this.transaction(() => {
       this.pythonShell = spawn(
@@ -65,32 +57,35 @@ function PythonWorker (workerScript, shellOptions) {
       )
       this.pythonShell.stdout.on('data', message => {
         // The python worker returns JSON {data, error}
-        console.log('[DATA]', JSON.parse(message))
         this.data.push(JSON.parse(message))
         this.pythonLock.release()
       })
       this.pythonShell.stderr.on('data', error => {
-        console.log('[APPERROR]', error)
         this.error.push(serializeError(new Error(error)))
         this.pythonLock.release()
       })
       this.pythonShell.on('close', error => {
-        console.log('[CLOSE]', error)
         this.error.push(serializeError(new Error(error)))
         this.pythonLock.release()
       })
       this.pythonShell.on('error', error => {
-        console.log('[ERROR]', error)
         this.error.push(serializeError(error))
         this.pythonLock.release()
       })
-      console.log('spawned.')
     })
   }
 
   this.end = async () => {
-    if (this.pythonShell.killed) return { error: `process ${this.pythonShell.pid} already killed.` }
-    if (this.pythonShell.exitCode !== null) return { error: `process ${this.pythonShell.pid} already exited with code ${this.pythonShell.exitCode}` }
+    if (this.pythonShell.killed) {
+      return { error: `process ${this.pythonShell.pid} already killed.` }
+    }
+    if (this.pythonShell.exitCode !== null) {
+      return {
+        error: `process ${this.pythonShell.pid} already exited with code ${
+          this.pythonShell.exitCode
+        }`
+      }
+    }
     await this.commandLock.acquire()
     await this.pythonLock.acquire()
     this.pythonShell.stdin.end()
@@ -108,8 +103,16 @@ function PythonWorker (workerScript, shellOptions) {
   }
 
   this.kill = async () => {
-    if (this.pythonShell.killed) return { error: `process ${this.pythonShell.pid} already killed.` }
-    if (this.pythonShell.exitCode !== null) return { error: `process ${this.pythonShell.pid} already exited with code ${this.pythonShell.exitCode}` }
+    if (this.pythonShell.killed) {
+      return { error: `process ${this.pythonShell.pid} already killed.` }
+    }
+    if (this.pythonShell.exitCode !== null) {
+      return {
+        error: `process ${this.pythonShell.pid} already exited with code ${
+          this.pythonShell.exitCode
+        }`
+      }
+    }
     await this.commandLock.acquire()
     await this.pythonLock.acquire()
     const pythonError = this.error.pop()

+ 115 - 35
backend/src/resolvers.js

@@ -1,29 +1,69 @@
 const { forwardTo } = require('prisma-binding')
 const bcrypt = require('bcryptjs')
 const jwt = require('jsonwebtoken')
+const fs = require('fs')
+const shortid = require('shortid')
+
+const UPLOAD_DIR = './static/uploads'
 
 const Query = {
   instruments: forwardTo('db'),
+  instrument: (parent, args, context, info) => {
+    const { id } = args
+    return context.db.query.instrument({ where: { id } }, info)
+  },
   projects: forwardTo('db'),
   projectVersions: forwardTo('db'),
   me: (parent, args, context, info) => {
     if (!context.request.userId) throw new Error('Not logged in.')
-    return context.db.query.user({ where: { id: context.request.userId } }, info)
+    return context.db.query.user(
+      { where: { id: context.request.userId } },
+      info
+    )
   }
 }
 
+const storeFS = ({ stream, filename }) => {
+  const id = shortid.generate()
+  const path = `${UPLOAD_DIR}/${id}-${filename}`
+  return new Promise((resolve, reject) =>
+    stream
+      .on('error', error => {
+        if (stream.truncated) fs.unlink(path)
+        reject(error)
+      })
+      .pipe(fs.createWriteStream(path))
+      .on('error', error => reject(error))
+      .on('finish', () => resolve({ id, path }))
+  )
+}
+
+const processUpload = async upload => {
+  const { createReadStream, filename, mimetype, encoding } = await upload
+  const stream = createReadStream()
+  let size = 0
+  stream.on('data', chunk => {
+    size += chunk.length
+  })
+  const { id, path } = await storeFS({ stream, filename })
+  return { filename, mimetype, encoding, path, size }
+}
+
 const Mutation = {
   createUser: async (parent, args, context, info) => {
     const email = args.email.toLowerCase()
     const password = await bcrypt.hash(args.password, 10)
     console.log(email, password)
-    const user = await context.db.mutation.createUser({
-      data: {
-        ...args,
-        email,
-        password
-      }
-    }, info)
+    const user = await context.db.mutation.createUser(
+      {
+        data: {
+          ...args,
+          email,
+          password
+        }
+      },
+      info
+    )
     const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET)
     context.response.cookie('token', token, {
       httpOnly: true,
@@ -38,10 +78,15 @@ const Mutation = {
     const valid = await bcrypt.compare(password, user.password)
     if (!valid) throw new Error('Invalid password')
     const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET)
-    context.response.cookie('token', token, {
-      httpOnly: true,
-      maxAge: 7 * 24 * 3600 * 1000
-    }, info)
+    context.response.cookie(
+      'token',
+      token,
+      {
+        httpOnly: true,
+        maxAge: 7 * 24 * 3600 * 1000
+      },
+      info
+    )
     return user
   },
   userLogout: async (parent, args, context, info) => {
@@ -49,49 +94,84 @@ const Mutation = {
     return 'Logged out.'
   },
   createCharacterization: async (parent, args, context, info) => {
-    const characterization = await context.db.mutation.createCharacterization({
-      data: {
-        name: args.name,
-        projectVersion: {
-          connect: { id: args.projectVersion }
+    const characterization = await context.db.mutation.createCharacterization(
+      {
+        data: {
+          name: args.name,
+          projectVersion: {
+            connect: { id: args.projectVersion }
+          }
         }
-      }
-    }, info)
+      },
+      info
+    )
     return characterization
   },
   createProject: async (parent, args, context, info) => {
-    const project = await context.db.mutation.createProject({
-      data: { ...args }
-    }, info)
+    const project = await context.db.mutation.createProject(
+      {
+        data: { ...args }
+      },
+      info
+    )
     return project
   },
   createProjectVersion: async (parent, args, context, info) => {
     console.log(args)
-    const projectVersion = await context.db.mutation.createProjectVersion({
-      data: {
-        ...args,
-        date: new Date(args.date).toISOString(),
-        project: {
-          connect: {
-            id: args.project
+    const projectVersion = await context.db.mutation.createProjectVersion(
+      {
+        data: {
+          ...args,
+          date: new Date(args.date).toISOString(),
+          project: {
+            connect: {
+              id: args.project
+            }
+          },
+          changes: {
+            set: args.changes
           }
-        },
-        changes: {
-          set: args.changes
         }
-      }
-    }, info)
+      },
+      info
+    )
     return projectVersion
   },
   uploadFiles: async (parent, args, context, info) => {
     console.log(args)
-    const { name, description, files } = args
+    const { name, description, file } = args
     let res
     for (const file of files) {
       res = await file
       console.log(res)
     }
     return res.filename
+  },
+  createInstrument: async (parent, args, context, info) => {
+    const { interfaces, documents, commands } = args
+    const { id, ...instrumentData } = args
+    const files = []
+    for (const document of documents) {
+      const { file, id, ...formParams } = document
+      const fileParams = await processUpload(file)
+      files.push({ ...formParams, ...fileParams })
+    }
+    const dbCommands = commands.map(command => {
+      const { id, ...dbCommand } = command
+      return dbCommand
+    })
+    const instrument = await context.db.mutation.createInstrument(
+      {
+        data: {
+          ...instrumentData,
+          interfaces: { set: interfaces },
+          documents: { create: files },
+          commands: { create: dbCommands }
+        }
+      },
+      info
+    )
+    return instrument
   }
 }
 

+ 41 - 14
frontend/components/File.js

@@ -44,17 +44,34 @@ const FileFields = {
 const FileFormFields = props => {
   const { state, onChange } = props
 
+  const toState = event => {
+    if (event.target.type === 'file') {
+      onChange({ ...state, [event.target.name]: event.target.files[0] })
+    } else {
+      onChange({ ...state, [event.target.name]: event.target.value })
+    }
+  }
+
   return (
     <fieldset>
       <label htmlFor='name'>Name</label>
-      <input type='text' name='name' id='name' placeholder='Name'
+      <input
+        type='text'
+        name='name'
+        id='name'
+        placeholder='Name'
         value={state.name}
-        onChange={onChange} />
+        onChange={toState}
+      />
       <label htmlFor='description'>Description</label>
-      <textarea name='description' id='description' placeholder='Description'
+      <textarea
+        name='description'
+        id='description'
+        placeholder='Description'
         value={state.description}
-        onChange={onChange} />
-      <input type='file' name='file' onChange={onChange} />
+        onChange={toState}
+      />
+      <input type='file' required name='file' onChange={toState} />
     </fieldset>
   )
 }
@@ -67,19 +84,29 @@ const File = props => {
       {mimetype.startsWith('image/') ? (
         <img src={downloadPath} alt={description} />
       ) : (
-          <a href={downloadPath}>
-            <img src={`${endpoint}/static/document-download-solid.png`} alt={description} width={75} />
-          </a>
-        )}
+        <a href={downloadPath}>
+          <img
+            src={`${endpoint}/static/document-download-solid.png`}
+            alt={description}
+            width={75}
+          />
+        </a>
+      )}
       <h2>{name}</h2>
       <p className='description'>{description}</p>
-      <p>Path</p><p><a href={downloadPath}>{downloadPath}</a></p>
-      <p>Filename</p><p>{filename}</p>
-      <p>MIME Type</p><p>{mimetype}</p>
-      <p>Size</p><p>{size}</p>
+      <p>Path</p>
+      <p>
+        <a href={downloadPath}>{downloadPath}</a>
+      </p>
+      <p>Filename</p>
+      <p>{filename}</p>
+      <p>MIME Type</p>
+      <p>{mimetype}</p>
+      <p>Size</p>
+      <p>{size}</p>
     </FileStyle>
   )
 }
 
 export default File
-export { FileFormFields }
+export { FileFormFields, FileFields }

+ 15 - 7
frontend/components/FileUpload.js

@@ -11,7 +11,15 @@ const DO_LOCAL = gql`
 
 const UPLOAD_FILE = gql`
   mutation UPLOAD_FILE($files: [Upload!]!, $name: String!, $description: String!) {
-    uploadFiles(files: $files, name: $name, description: $description)
+    uploadFiles(files: $files, name: $name, description: $description) {
+      id
+      path
+      name
+      description
+      filename
+      mimetype
+      size
+    }
   }
 `
 
@@ -50,7 +58,7 @@ const TestForm = props => {
   )
 }
 
-const FileFields = props => {
+const FileFormFields = props => {
   const [state, setState] = useState({ ...FileState })
 
   const updateState = async event => {
@@ -75,15 +83,15 @@ const FileFields = props => {
   )
 }
 
-const FileState = {
-  file: null,
+const FileFields = {
+  id: null,
+  path: '',
   name: '',
   description: '',
   filename: '',
   mimetype: '',
   size: '',
-  path: '',
-  uploading: false
+  file: null
 }
 
 async function uploadFile(file) {
@@ -154,4 +162,4 @@ class FileUpload extends React.Component {
 }
 
 export default FileUpload
-export { FileFields, FileState, uploadFile, TestForm }
+export { FileFields, FileFormFields }

+ 209 - 80
frontend/components/Instrument.js

@@ -1,32 +1,103 @@
 import gql from 'graphql-tag'
 import { Query, Mutation } from 'react-apollo'
 import { adopt } from 'react-adopt'
-import InstrumentSubsystem, { InstrumentSubsystemFormFields, InstrumentSubsystemFields } from './InstrumentSubsystem'
-import File, { FileFormFields } from './File'
+import { InterfaceFormFields } from './Interface'
+import InstrumentCommand, {
+  InstrumentCommandFormFields,
+  InstrumentCommandFields
+} from './InstrumentCommand'
+import File, { FileFormFields, FileFields } from './File'
 import Gallery from './Gallery'
 import { INTERFACES_FULL } from './InterfaceList'
 
 const CREATE_INSTRUMENT = gql`
-  mutation CREATE_INSTRUMENT($name: String!, $description: String!, $interfaces: [String]!) {
-    createInstrument(name: $name, description: $description, interfaces: $interfaces) {
+  mutation CREATE_INSTRUMENT(
+    $id: ID
+    $name: String!
+    $manufacturer: String!
+    $description: String
+    $picture: ID
+    $interfaces: [String]!
+    $documents: [FileUpload]!
+    $commands: [InstrumentCommandInput]!
+  ) {
+    createInstrument(
+      id: $id
+      name: $name
+      manufacturer: $manufacturer
+      description: $description
+      picture: $picture
+      interfaces: $interfaces
+      documents: $documents
+      commands: $commands
+    ) {
       id
     }
   }
 `
 
+const INSTRUMENT_QUERY = gql`
+  query INSTRUMENT_QUERY($id: ID!) {
+    instrument(id: $id) {
+      id
+      name
+      manufacturer
+      description
+      picture
+      documents {
+        id
+        path
+        name
+        description
+        filename
+        mimetype
+        size
+      }
+      interfaces
+      commands {
+        id
+        tag
+        name
+        description
+        readString
+        writeString
+      }
+      parameters {
+        id
+        tag
+        name
+        description
+        type
+        values
+      }
+    }
+  }
+`
+
+const INSTRUMENTS_QUERY = gql`
+  query INSTRUMENTS_QUERY {
+    instruments {
+      id
+      name
+      manufacturer
+      description
+      picture
+    }
+  }
+`
+
 const InstrumentFields = {
   id: null,
   name: '',
+  manufacturer: '',
   description: '',
+  picture: '',
   documents: [],
   interfaces: [],
   commands: [],
-  parameters: [],
-  subsystems: []
+  parameters: []
 }
 
-const MockInterfaces = { interfaces: [{ name: 'serial' }, { name: 'usbtmc' }] }
-
 class InstrumentForm extends React.Component {
   state = {
     ...InstrumentFields,
@@ -37,75 +108,117 @@ class InstrumentForm extends React.Component {
     this.setState({ [event.target.name]: event.target.value })
   }
 
-  handleInterface = event => {
-    let interfaces = this.state.interfaces
-    const ifaceSelected = event.target.checked
-    const index = this.state.interfaces.findIndex(iface => iface === event.target.value)
-    if (ifaceSelected && index < 0) interfaces = [
-      ...this.state.interfaces,
-      event.target.value
-    ]
-    if (!ifaceSelected && index >= 0) interfaces = [
-      ...this.state.interfaces.slice(0, index),
-      ...this.state.interfaces.slice(index + 1)
-    ]
-    this.setState({ interfaces })
+  toSubState = (name, index, subState) => {
+    this.setState({
+      [name]: [
+        ...this.state[name].slice(0, index),
+        subState,
+        ...this.state[name].slice(index + 1)
+      ]
+    })
   }
 
-  render() {
+  render () {
     return (
-      <form>
-        <h1>Create new instrument.</h1>
-        <fieldset>
-          <label htmlFor='name'>Name</label>
-          <input type='text' name='name' id='name' placeholder='Name'
-            value={this.state.name}
-            onChange={this.toState} />
-          <label htmlFor='description'>Description</label>
-          <textarea name='description' id='description' placeholder='Description'
-            value={this.state.description}
-            onChange={this.toState} />
-        </fieldset>
-        {this.state.documents.map(file =>
-          <FileFormFields />
-        )}
-        <Query query={INTERFACES_FULL}>
-          {({ data, error, loading }) => {
-            if (loading) return <p>Loading interfaces.</p>
-            //if (error) return <p>Error loading interfaces: {error.message}</p>
-            //if (!data.length) return <p>No interfaces found.</p>
-            return (
-              <fieldset>
-                {MockInterfaces.interfaces.map((iface, index) =>
-                  <div key={iface.name}>
-                    <label htmlFor={iface.name}>{iface.name}</label>
-                    <input type='checkbox' name='interface' id={iface.name}
-                      value={iface.name}
-                      checked={this.state.interfaces.includes(iface.name)}
-                      onChange={this.handleInterface} />
-                  </div>
-                )}
-              </fieldset>
-            )
-          }}
-        </Query>
-        {this.state.subsystems.map((subsystem, index) =>
-          <InstrumentSubsystemFormFields key={index} state={subsystem} onChange={event => {
-            const updatedSubsystem = { ...this.state.subsystems[index] }
-            updatedSubsystem[event.target.name] = event.target.value
-            this.setState({
-              subsystems: [
-                ...this.state.subsystems.slice(0, index),
-                updatedSubsystem,
-                ...this.state.subsystems.slice(index + 1)
-              ]
-            })
-          }} />
+      <Mutation
+        mutation={CREATE_INSTRUMENT}
+        variables={this.state}
+        refetchQueries={[{ query: INSTRUMENTS_QUERY }]}
+      >
+        {(createInstrument, { data, error, loading }) => (
+          <form
+            onSubmit={event => {
+              event.preventDefault()
+              createInstrument()
+            }}
+          >
+            <h1>Create new instrument.</h1>
+            <h2>Instrument data</h2>
+            <fieldset>
+              <label htmlFor='name'>
+                Name
+                <input
+                  type='text'
+                  name='name'
+                  id='name'
+                  placeholder='Name'
+                  required
+                  value={this.state.name}
+                  onChange={this.toState}
+                />
+              </label>
+              <label htmlFor='manufacturer'>
+                Manufacturer
+                <input
+                  type='text'
+                  name='manufacturer'
+                  id='manufacturer'
+                  placeholder='Manufacturer'
+                  required
+                  value={this.state.manufacturer}
+                  onChange={this.toState}
+                />
+              </label>
+              <label htmlFor='description'>
+                Description
+                <textarea
+                  name='description'
+                  id='description'
+                  placeholder='Description'
+                  value={this.state.description}
+                  onChange={this.toState}
+                />
+              </label>
+            </fieldset>
+            <h2>Documents</h2>
+            {this.state.documents.map((file, index) => (
+              <FileFormFields
+                key={index}
+                state={file}
+                onChange={subState =>
+                  this.toSubState('documents', index, subState)
+                }
+              />
+            ))}
+            <button
+              type='button'
+              onClick={event => {
+                this.setState({
+                  documents: [...this.state.documents, FileFields]
+                })
+              }}
+            >
+              Add document
+            </button>
+            <h2>Interfaces</h2>
+            <InterfaceFormFields
+              state={this.state.interfaces}
+              onChange={interfaces => this.setState({ interfaces })}
+            />
+            <h2>Commands</h2>
+            {this.state.commands.map((command, index) => (
+              <InstrumentCommandFormFields
+                key={index}
+                state={command}
+                onChange={subState =>
+                  this.toSubState('commands', index, subState)
+                }
+              />
+            ))}
+            <button
+              type='button'
+              onClick={event => {
+                this.setState({
+                  commands: [...this.state.commands, InstrumentCommandFields]
+                })
+              }}
+            >
+              Add command
+            </button>
+            <button type='submit'>Save Instrument</button>
+          </form>
         )}
-        <button type='button' onClick={event => {
-          this.setState({ subsystems: [...this.state.subsystems, InstrumentFields] })
-        }}>Add subsystem</button>
-      </form>
+      </Mutation>
     )
   }
 }
@@ -115,17 +228,33 @@ const Instrument = props => {
   return instrument ? (
     <div>
       <h1>{instrument.name}</h1>
+      <h2>{instrument.manufacturer}</h2>
       <p>{instrument.description}</p>
-      <Gallery title='Documents' items={['Hallo']} />
-      <Gallery title='Interfaces' items={['serial', 'usbtmc'].map(item => <div>{item}</div>)} />
-      <Gallery title='Subsystems' items={instrument.subsystems.map(subsystem =>
-        <InstrumentSubsystem subsystem={subsystem} />
-      )} />
+      <p>{instrument.picture}</p>
+      <Gallery
+        title='Documents'
+        items={instrument.documents.map(document => (
+          <File data={document} />
+        ))}
+      />
+      <Gallery
+        title='Interfaces'
+        items={instrument.interfaces.map(iface => (
+          <div>{iface}</div>
+        ))}
+      />
+      <Gallery
+        title='Commands'
+        items={instrument.commands.map(command => (
+          <InstrumentCommand command={command} />
+        ))}
+      />
     </div>
   ) : (
-      <p>Instrument not found.</p>
-    )
+    <p>Instrument not found.</p>
+  )
 }
 
 export default Instrument
 export { InstrumentFields, InstrumentForm }
+export { INSTRUMENT_QUERY, INSTRUMENTS_QUERY }

+ 62 - 16
frontend/components/InstrumentCommand.js

@@ -1,31 +1,77 @@
-import gql from 'graphql-tag'
-import { Query, Mutation } from 'react-apollo'
+const InstrumentCommandFields = {
+  id: null,
+  tag: '',
+  name: '',
+  description: '',
+  readString: '',
+  writeString: '',
+  subsystem: '',
+  parameters: []
+}
 
 const InstrumentCommandFormFields = props => {
   const { state, onChange } = props
 
+  const toState = event => {
+    const newState = {
+      ...state,
+      [event.target.name]: event.target.value
+    }
+    onChange(newState)
+  }
+
   return (
     <fieldset>
+      <label htmlFor='tag'>Tag</label>
+      <input
+        type='text'
+        name='tag'
+        id='tag'
+        placeholder='Tag'
+        value={state.tag}
+        onChange={toState}
+      />
       <label htmlFor='name'>Name</label>
-      <input type='text' name='name' id='name' placeholder='Name' value={state.name} onChange={onChange} />
-      <label htmlFor='description'>Name</label>
-      <textarea name='description' id='description' placeholder='Description' value={state.description} onChange={onChange} />
+      <input
+        type='text'
+        name='name'
+        id='name'
+        placeholder='Name'
+        value={state.name}
+        onChange={toState}
+      />
+      <label htmlFor='description'>Description</label>
+      <textarea
+        name='description'
+        id='description'
+        placeholder='Description'
+        value={state.description}
+        onChange={toState}
+      />
+      <label htmlFor='readString'>Read string</label>
+      <input
+        type='text'
+        name='readString'
+        id='readString'
+        placeholder='Read string'
+        value={state.readString}
+        onChange={toState}
+      />
+      <label htmlFor='writeString'>Write string</label>
+      <input
+        type='text'
+        name='writeString'
+        id='writeString'
+        placeholder='Write string'
+        value={state.writeString}
+        onChange={toState}
+      />
     </fieldset>
   )
 }
 
 const InstrumentCommand = props => {
   const { command } = props
-  const state = {
-    id: null,
-    tag: '',
-    name: '',
-    description: '',
-    instrument: null,
-    readString: '',
-    writeString: '',
-    parameters: []
-  }
 
   return (
     <div>
@@ -40,4 +86,4 @@ const InstrumentCommand = props => {
 }
 
 export default InstrumentCommand
-export { InstrumentCommandFormFields }
+export { InstrumentCommandFields, InstrumentCommandFormFields }

+ 32 - 30
frontend/components/InstrumentList.js

@@ -1,40 +1,42 @@
 import gql from 'graphql-tag'
 import { Query } from 'react-apollo'
 import Link from 'next/link'
-import { InstrumentForm } from './Instrument'
-
-const INSTRUMENTS = gql`
-  query INSTRUMENTS {
-    instruments {
-      id
-      name
-      description
-    }
-  }
-`
+import { InstrumentForm, INSTRUMENTS_QUERY } from './Instrument'
 
 const InstrumentList = props => (
-  <Query query={INSTRUMENTS}>
+  <Query query={INSTRUMENTS_QUERY}>
     {({ data, loading, error }) => {
-      const content = [
-        <h1>Instrument List</h1>
-      ]
-      if (loading) content.push(<p>Loading instruments...</p>)
-      else if (error) content.push(<p>Error loading instruments: {error.message}</p>)
-      else if (data.instruments.length > 0) content.push(
-        <ul>
-          {data.instruments.map(instrument => (
-            <li key={instrument.id} title={instrument.description}>
-              <Link href={{ pathname: 'instruments', query: { id: instrument.id } }}>
-                <a>{instrument.name}</a>
-              </Link>
-            </li>
-          ))}
-        </ul>
+      return (
+        <>
+          <h1>Instrument List</h1>
+          {loading ? (
+            <p>Loading instruments...</p>
+          ) : error ? (
+            <p>Error loading instruments: {error.message}</p>
+          ) : !data.instruments.length ? (
+            <p>No instruments found.</p>
+          ) : (
+            <ul>
+              {data.instruments.map(instrument => (
+                <li key={instrument.id} title={instrument.description}>
+                  <Link
+                    href={{
+                      pathname: 'instruments',
+                      query: { id: instrument.id }
+                    }}
+                  >
+                    <a>{instrument.name}</a>
+                  </Link>
+                </li>
+              ))}
+            </ul>
+          )}
+          <Link href={{ pathname: 'instruments', query: { mode: 'add' } }}>
+            <a>Add an instrument</a>
+          </Link>
+          <InstrumentForm />
+        </>
       )
-      else content.push(<p>No instruments found.</p>)
-      content.push(<InstrumentForm />)
-      return content
     }}
   </Query>
 )

+ 0 - 66
frontend/components/InstrumentSubsystem.js

@@ -1,66 +0,0 @@
-import gql from 'graphql-tag'
-import { Query, Mutation } from 'react-apollo'
-import Gallery from './Gallery'
-import InstrumentCommand from './InstrumentCommand'
-import InstrumentParameter from './InstrumentParameter'
-
-const CREATE_INSTRUMENT_SUBSYSTEM = gql`
-  mutation CREATE_INSTRUMENT_SUBSYSTEM($name: String!, $description: String!) {
-    createInstrumentSubsystem(name: $name, description: $description) {
-      id
-    }
-  }
-`
-
-const UPDATE_INSTRUMENT_SUBSYSTEM = gql`
-  mutation UPDATE_INSTRUMENT_SUBSYSTEM($name: String, $description: String, $commands: [ID]) {
-    updateInstrumentSubsystem(name: $name, description:$description, commands: $commands) {
-      id
-    }
-  }
-`
-
-const InstrumentSubsystemFields = {
-  id: null,
-  name: '',
-  description: '',
-  commands: [],
-  parameters: [],
-  subsystems: []
-}
-
-const InstrumentSubsystemFormFields = props => {
-  const { state, onChange } = props
-
-  return (
-    <fieldset>
-      <label htmlFor='name'>Name</label>
-      <input type='text' name='name' id='name' placeholder='Name' value={state.name} onChange={onChange} />
-      <label htmlFor='description'>Name</label>
-      <textarea name='description' id='description' placeholder='Description' value={state.description} onChange={onChange} />
-    </fieldset>
-  )
-}
-
-const InstrumentSubsystem = props => {
-  const { subsystem } = props
-
-  return subsystem ? (
-    <div>
-      <h1>{subsystem.name}</h1>
-      <p>{subsystem.description}</p>
-      <Gallery title='Commands' items={subsystem.commands.map(command =>
-        <InstrumentCommand command={command} />)} />
-      <Gallery title='Parameters' items={subsystem.parameters.map(parameter =>
-        <InstrumentParameter parameter={parameter} />)} />
-      <Gallery title='Subsystems' items={subsystem.subsystems.map(childSubsystem =>
-        <InstrumentSubsystem subsystem={childSubsystem} />)} />
-    </div>
-  ) : (
-      <p>No data found.</p>
-    )
-}
-
-export default InstrumentSubsystem
-export { InstrumentSubsystemFields, InstrumentSubsystemFormFields }
-export { CREATE_INSTRUMENT_SUBSYSTEM }

+ 83 - 5
frontend/components/Interface.js

@@ -1,19 +1,97 @@
+import gql from 'graphql-tag'
+import { Query } from 'react-apollo'
 import Gallery from './Gallery'
 import Port from './Port'
 import Connection from './Connection'
 import InterfaceOption from './InterfaceOption'
 
+const INTERFACES = gql`
+  query INTERFACES {
+    interfaces {
+      name
+    }
+  }
+`
+
+const InterfaceFormFields = props => {
+  const { state, onChange } = props
+
+  const toState = event => {
+    let interfaces = state
+    const ifaceSelected = event.target.checked
+    const index = state.findIndex(iface => iface === event.target.value)
+    if (ifaceSelected && index < 0) {
+      interfaces = [...state, event.target.value]
+    }
+    if (!ifaceSelected && index >= 0) {
+      interfaces = [...state.slice(0, index), ...state.slice(index + 1)]
+    }
+    onChange(interfaces)
+  }
+
+  return (
+    <Query query={INTERFACES}>
+      {({ data, loading, error }) => {
+        if (loading) return <p>Loading interfaces</p>
+        if (error) return <p>Error loading interfaces: {error.message}</p>
+        const { interfaces } = data
+        if (!interfaces.length) return <p>No interfaces found</p>
+        return (
+          <fieldset>
+            {interfaces.map(iface => (
+              <label key={iface.name} htmlFor='interface'>
+                {iface.name}
+                <input
+                  type='checkbox'
+                  name='interface'
+                  id='interface'
+                  value={iface.name}
+                  checked={state.includes(iface.name)}
+                  onChange={toState}
+                />
+              </label>
+            ))}
+          </fieldset>
+        )
+      }}
+    </Query>
+  )
+}
+
 const Interface = props => {
-  const { workerScript, interfaceName, options, ports, connections } = props.data
+  const {
+    workerScript,
+    interfaceName,
+    options,
+    ports,
+    connections
+  } = props.data
   return (
     <div>
       <h2>{interfaceName}</h2>
-      <p>Script:</p><p>{workerScript}</p>
-      <Gallery title='ports' items={ports.map(port => <Port key={port.device} data={port} />)} />
-      <Gallery title='connections' items={connections.map(connection => <Connection key={connection.id} data={connection} />)} />
-      <Gallery title='options' items={options.map(option => <InterfaceOption key={option.name} data={option} />)} />
+      <p>Script:</p>
+      <p>{workerScript}</p>
+      <Gallery
+        title='ports'
+        items={ports.map(port => (
+          <Port key={port.device} data={port} />
+        ))}
+      />
+      <Gallery
+        title='connections'
+        items={connections.map(connection => (
+          <Connection key={connection.id} data={connection} />
+        ))}
+      />
+      <Gallery
+        title='options'
+        items={options.map(option => (
+          <InterfaceOption key={option.name} data={option} />
+        ))}
+      />
     </div>
   )
 }
 
 export default Interface
+export { InterfaceFormFields }

+ 101 - 33
frontend/components/ProjectForm.js

@@ -1,11 +1,18 @@
 import gql from 'graphql-tag'
 import { Mutation, Query } from 'react-apollo'
-import { FileFields, FileState, uploadFile } from './FileUpload'
 import { ProjectVersionFields, ProjectVersionState } from './ProjectVersionForm'
 
 const CREATE_PROJECT = gql`
-  mutation CREATE_PROJECT($name: String!, $abbreviation: String!, $description: String) {
-    createProject(name: $name, abbreviation: $abbreviation, description: $description) {
+  mutation CREATE_PROJECT(
+    $name: String!
+    $abbreviation: String!
+    $description: String
+  ) {
+    createProject(
+      name: $name
+      abbreviation: $abbreviation
+      description: $description
+    ) {
       id
     }
   }
@@ -40,20 +47,26 @@ const QUERY_PROJECTS = gql`
 const ProjectSelector = props => (
   <Query query={QUERY_PROJECTS}>
     {({ data, loading, error }) => {
-      if (error) return (<p>Error loading project: ${error.message}</p>)
-      if (loading) return (<p>Loading data...</p>)
-      if (!data || !data.projects.length) return (<p>No project found.</p>)
-      if (!props.value) props.onChange({ target: { name: 'project', value: data.projects[0].id } })
+      if (error) return <p>Error loading project: ${error.message}</p>
+      if (loading) return <p>Loading data...</p>
+      if (!data || !data.projects.length) return <p>No project found.</p>
+      if (!props.value) {
+        props.onChange({
+          target: { name: 'project', value: data.projects[0].id }
+        })
+      }
       const selector = (
         <select value={12} {...props}>
-          {data.projects.map(project =>
-            <option key={project.id} value={project.id}>{project.name}</option>
-          )}
+          {data.projects.map(project => (
+            <option key={project.id} value={project.id}>
+              {project.name}
+            </option>
+          ))}
         </select>
       )
       return selector
     }}
-  </Query >
+  </Query>
 )
 
 const ProjectState = {
@@ -77,15 +90,35 @@ const ProjectFields = props => {
   }
 
   return (
-    <fieldset >
+    <fieldset>
       {props.title && <legend>{props.title}</legend>}
-      <label htmlFor='name' > Project name</label>
-      <input type='text' name='name' id='name' placeholder='Project name' value={state.name} onChange={updateState} />
+      <label htmlFor='name'> Project name</label>
+      <input
+        type='text'
+        name='name'
+        id='name'
+        placeholder='Project name'
+        value={state.name}
+        onChange={updateState}
+      />
       <label htmlFor='abbreviation'>Project abbreviation</label>
-      <input type='text' name='abbreviation' id='abbreviation' placeholder='Project abbreviation' value={state.abbreviation} onChange={updateState} />
+      <input
+        type='text'
+        name='abbreviation'
+        id='abbreviation'
+        placeholder='Project abbreviation'
+        value={state.abbreviation}
+        onChange={updateState}
+      />
       <label htmlFor='description'>Project description</label>
-      <textarea name='description' id='description' placeholder='Project description' value={state.description} onChange={updateState} />
-    </fieldset >
+      <textarea
+        name='description'
+        id='description'
+        placeholder='Project description'
+        value={state.description}
+        onChange={updateState}
+      />
+    </fieldset>
   )
 }
 
@@ -112,24 +145,59 @@ const Project = props => {
   }
 
   return (
-    <Mutation mutation={CREATE_PROJECT} variables={state} refetchQueries={[{ query: QUERY_PROJECTS }]}>
+    <Mutation
+      mutation={CREATE_PROJECT}
+      variables={state}
+      refetchQueries={[{ query: QUERY_PROJECTS }]}
+    >
       {(createProject, { data, error, loading }) => (
-        <form onSubmit={async event => {
-          event.preventDefault()
-          console.log('form submitted.')
-          const { data } = await createProject()
-          state.id = data.createProject.id
-        }}>
+        <form
+          onSubmit={async event => {
+            event.preventDefault()
+            console.log('form submitted.')
+            const { data } = await createProject()
+            state.id = data.createProject.id
+          }}
+        >
           <ProjectFields title='Project' state={state} onChange={toState} />
-          {state.versions.map((version, index) =>
-            <ProjectVersionFields title='Project version' key={index} state={version} onChange={(event, state) => updateVersions(event, state, index)} />
-          )}
-          <button type='button' onClick={event => { setState({ ...state, versions: [...state.versions, { ...ProjectVersionState }] }) }}>Add project version</button>
-          {state.files.map((file, index) =>
-            <FileFields title='Files' key={index} state={file} onChange={(event, state) => updateFiles(event, state, index)} />
-          )}
-          <button type='button' onClick={event => { setState({ ...state, files: [...state.files, { ...FileState }] }) }}>Add file</button>
-          <button type='submit'>{state.id && state.id !== '__NEW__' ? 'Save' : 'Add'}</button>
+          {state.versions.map((version, index) => (
+            <ProjectVersionFields
+              title='Project version'
+              key={index}
+              state={version}
+              onChange={(event, state) => updateVersions(event, state, index)}
+            />
+          ))}
+          <button
+            type='button'
+            onClick={event => {
+              setState({
+                ...state,
+                versions: [...state.versions, { ...ProjectVersionState }]
+              })
+            }}
+          >
+            Add project version
+          </button>
+          {state.files.map((file, index) => (
+            <FileFields
+              title='Files'
+              key={index}
+              state={file}
+              onChange={(event, state) => updateFiles(event, state, index)}
+            />
+          ))}
+          <button
+            type='button'
+            onClick={event => {
+              setState({ ...state, files: [...state.files, { ...FileState }] })
+            }}
+          >
+            Add file
+          </button>
+          <button type='submit'>
+            {state.id && state.id !== '__NEW__' ? 'Save' : 'Add'}
+          </button>
         </form>
       )}
     </Mutation>

+ 1 - 0
frontend/lib/withApollo.js

@@ -32,6 +32,7 @@ function createClient ({ ctx, headers, initialState }) {
         credentials: 'include'
       })
     ]),
+    fetchOptions: { mode: 'no-cors' },
     cache: new InMemoryCache(),
     resolvers,
     typeDefs

+ 0 - 4
frontend/pages/index.js

@@ -3,17 +3,13 @@
  * https://www.prisma.io/docs/1.29/get-started/03-build-graphql-servers-with-prisma-JAVASCRIPT-e001/
  */
 
-import DemoForm from '../components/Form'
 import Project from '../components/ProjectForm'
 import ProjectVersionForm from '../components/ProjectVersionForm'
-import { TestForm } from '../components/FileUpload'
 
 const Index = props => (
   <div>
-    <DemoForm />
     <Project />
     <ProjectVersionForm />
-    <TestForm />
   </div>
 )
 

+ 16 - 43
frontend/pages/instruments.js

@@ -1,48 +1,21 @@
-import Instrument from '../components/Instrument'
+import { Query } from 'react-apollo'
+import Instrument, { INSTRUMENT_QUERY } from '../components/Instrument'
 import InstrumentList from '../components/InstrumentList'
 
-const instrument = {
-  name: 'Keithley 2230-3',
-  description: 'A very nice 3 channel power supply.',
-  subsystems: [{
-    name: 'Source',
-    description: 'The commands in the SOURce subsystem are used to control the output of the power supply.',
-    commands: [{
-      id: '1',
-      tag: 'Apply',
-      name: 'Apply voltage and current',
-      description: 'This command sets voltage and current levels on a specified channel with a single command message.',
-      instrument: null,
-      readString: null,
-      writeString: '[SOURce:]APPLy {CH1|CH2|CH3}, <NRf+>, <NRf+>',
-      parameters: ['channel']
-    },
-    {
-      id: '1',
-      tag: 'Output',
-      name: 'Channel output state',
-      description: 'This command sets the output state of the presently selected channel. The query form of this command returns the output state of the presently selected channel.',
-      instrument: null,
-      readString: '[SOURce:]CHANnel:OUTPut[:STATe]?',
-      writeString: '[SOURce:]CHANnel:OUTPut[:STATe] <BOOL>',
-      parameters: [{
-        name: 'channel',
-        values: ['CH1', 'CH2', 'CH3']
-      }]
-    }],
-    parameters: [{
-      name: 'channel',
-      values: ['CH1', 'CH2', 'CH3']
-    }],
-    subsystems: []
-  }]
-}
-
-const InstrumentsPage = props => (
-  props.query && props.query.id
-    ? <Instrument instrument={instrument} />
-    : <InstrumentList />
-)
+const InstrumentsPage = props =>
+  props.query && props.query.id ? (
+    <Query query={INSTRUMENT_QUERY} variables={{ id: props.query.id }}>
+      {({ data, loading, error }) => {
+        console.log(data, loading, error)
+        if (loading) return <p>Loading instrument...</p>
+        if (error) return <p>Error loading instrument: {error.message}</p>
+        const { instrument } = data
+        return <Instrument instrument={instrument} />
+      }}
+    </Query>
+  ) : (
+    <InstrumentList />
+  )
 
 InstrumentsPage.getInitialProps = ({ query }) => {
   return { query }

Some files were not shown because too many files changed in this diff