Browse Source

Moved modal, to a panel tab within funcs

* Better to hve control over run
luis 7 years ago
parent
commit
ab8c07d2f6

+ 3 - 2
browser/vue-flow/package.json

@@ -27,7 +27,8 @@
     "babel-preset-stage-3": "^6.24.1",
     "cross-env": "^5.1.3",
     "css-loader": "^0.28.7",
-    "eslint": "^4.14.0",
+    "eslint": "^4.16.0",
+    "eslint-config-esnext": "^2.0.0",
     "eslint-config-node": "^2.0.0",
     "eslint-config-standard": "^11.0.0-beta.0",
     "eslint-plugin-babel": "^4.1.2",
@@ -35,7 +36,7 @@
     "eslint-plugin-node": "^5.2.1",
     "eslint-plugin-promise": "^3.6.0",
     "eslint-plugin-standard": "^3.0.1",
-    "eslint-plugin-vue": "^4.0.0-beta.4",
+    "eslint-plugin-vue": "^4.2.0",
     "file-loader": "^1.1.4",
     "flow": "^0.2.3",
     "flow-bin": "^0.62.0",

+ 6 - 0
browser/vue-flow/src/assets/dark-theme.css

@@ -7,6 +7,8 @@
   --normal: #eee !important;
   --normal-secondary: #777 !important;
   --primary: #f57c00 !important;
+  --primary-darker: #b54c00 !important;
+  --primary-lighter: #ff8c20 !important;
   --node-label: var(--normal) !important;
   --node-socket: var(--primary) !important;
   --link-hover: var(--primary) !important;
@@ -14,6 +16,10 @@
   --selector-color: var(--primary);
 }
 
+.dark .primary-inverse {
+  background: var(--primary-darker) !important;
+}
+
 .dark .flow-node__body {
   fill: transparent;
 }

+ 36 - 5
browser/vue-flow/src/assets/default-theme.css

@@ -15,6 +15,7 @@
   --link-hover: #f00;
   --selector-background: rgba(0, 0, 200, 0.1);
   --selector-color: var(--primary);
+  --transition-speed: 0.2s;
 }
 
 .vertical_sep {
@@ -41,7 +42,7 @@
   right: 0;
   bottom: 0;
   left: 0;
-  transition: all 0.3s;
+  transition: all var(--transition-speed);
   opacity: 0.4;
   background: #000;
 }
@@ -74,6 +75,7 @@ input {
   padding: 20px;
   outline: none;
   box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
+  transition: all var(--transition-speed);
 }
 
 h3 {
@@ -89,6 +91,7 @@ h3 {
   box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
   overflow: visible;
   z-index: 10;
+  transition: all var(--transition-speed);
 }
 
 .app-info {
@@ -102,6 +105,11 @@ h3 {
 
 .app-flow-container {
   background: var(--background);
+  transition: all var(--transition-speed);
+}
+
+.flow-panel__container {
+  background: var(--background-secondary) !important;
 }
 
 /* Flow Funcs PANEL */
@@ -110,7 +118,6 @@ h3 {
 }
 
 .flow-funcs__container {
-  background: var(--background-secondary) !important;
 }
 
 .flow-funcs__group {
@@ -208,7 +215,7 @@ h3 {
 }
 
 .flow-node__selection {
-  transition: all 1s;
+  transition: all var(--transition-speed);
 }
 
 .flow-node--selected .flow-node__selection {
@@ -242,7 +249,7 @@ h3 {
 .flow-node__socket-detail {
   font-size: 12px;
   font-weight: 100;
-  transition: all 0.3s;
+  transition: all var(--transition-speed);
   fill: var(--normal);
   filter: url(#solid-white);
 }
@@ -310,6 +317,30 @@ h3 {
   color: var(--normal);
 }
 
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.5s;
+}
+
+.fade-enter,
+.fade-leave-to {
+  opacity: 0;
+}
+
+.slide-leave-active,
+.slide-enter-active {
+  transition: 1s;
+}
+
+.slide-enter {
+  transform: translate(100%, 0);
+}
+
+.slide-leave-to {
+  transform: translate(-100%, 0);
+}
+
+/*
 .flow-modal .flow-view {
   background: var(--background);
 }
@@ -322,4 +353,4 @@ h3 {
 .flow-modal .hx-modal__body input {
   background: var(--background) !important;
   color: var(--normal) !important;
-}
+}*/

+ 2 - 2
browser/vue-flow/src/assets/style.css

@@ -38,7 +38,7 @@ button {
   cursor: pointer;
   border: none;
   padding: 10px;
-  transition: all 0.3s;
+  transition: all var(--transition-speed);
 }
 
 button::-moz-focus-inner {
@@ -53,7 +53,7 @@ button::after,
   right: 0;
   bottom: 0;
   left: 0;
-  transition: all 0.3s;
+  transition: all var(--transition-speed);
   opacity: 0;
   background: #222 !default;
 }

+ 1 - 1
browser/vue-flow/src/components/chat.vue

@@ -134,7 +134,7 @@ function pad (n, width, z) {
   box-sizing:border-box;
   position:relative;
   width:0;
-  transition: all 0.3s;
+  transition: all var(--transition-speed);
 }
 
 .flow-chat.active {

+ 50 - 0
browser/vue-flow/src/components/defregistry.js

@@ -0,0 +1,50 @@
+export default{
+  'Input': {
+    categories: ['core'],
+    output: 'interface {}',
+    style: { color: '#686', shape: 'circle' },
+    props: {} // should be sent in the node
+  },
+  'Variable': {
+    categories: ['core'],
+    output: 'interface {}',
+    style: { color: '#88a', shape: 'circle' },
+    props: {init: ''}
+  },
+  'Const': {
+    categories: ['core'],
+    output: 'interface {}',
+    style: { color: '#555' }
+    // , props: {value: ''}
+  },
+  'Log': {
+    categories: ['core'],
+    inputs: ['interface {}'],
+    output: 'interface {}',
+    style: { color: '#665', shape: 'circle' }
+  }
+
+}
+
+/* {
+        // Fixed default stuff
+        'Test': {
+          group: 'Generic',
+          output: 'any',
+          style: {
+            shape: 'thing'
+          }
+        },
+
+        'MatMul': { group: 'Machine learning', inputs: [ '[]float32', '[]float32' ], output: '[]float32', style: { color: '#a44', textColor: 'white' } },
+        'Activator': { group: 'Machine learning', inputs: [ '[]float32' ], output: '[]float32', style: { color: '#a44', textColor: 'white', shape: 'circle' } },
+
+        'test': { group: 'Text', inputs: [ '[]float32', 'string' ], output: 'string', style: {'color': '#a93'} },
+        'reverse': { group: 'Text', inputs: [ 'string' ], output: 'string', style: {'color': '#a93'} },
+
+        'fetch': { group: 'json', output: 'json', style: {'color': '#99a'} },
+        'jsonExtract': { group: 'json', inputs: ['json'], output: 'string', style: {'color': '#99a'} },
+
+        'string': { group: 'Visualization', inputs: ['string'], style: {'color': '#9a9'} },
+        'lineGraph': { group: 'Visualization', inputs: ['[]float32', '[]float32'], style: {'color': '#9a9'} }
+      }, */

+ 16 - 9
browser/vue-flow/src/components/flow/manager.vue

@@ -8,7 +8,6 @@
       :class="{
         'flow-linking':linking || stickySockets,
         'activity':dragging || pointerLink.active ,
-        'flow-node--detail': detailed,
         'flow-node--activity':nodeActivity,
         'selecting': !!selector
       }"
@@ -57,7 +56,6 @@
     <div class="flow-container__control">
       <button @click="$emit('funcsPanelToggle')">Panel</button>
       <button @click="stickySockets=!stickySockets"> {{ stickySockets? 'Hide':'Show' }} sockets </button>
-      <button @click="detailed=!detailed"> {{ detailed? 'Hide':'Show' }} detail </button>
       <button @click="nodeActivity=!nodeActivity"> {{ nodeActivity? 'Hide':'Show' }} activity </button>
       <button @click="$emit('documentSave')"> Save </button> <!-- should disable until confirmation -->
       <button v-if="panzoom.x!=0 || panzoom.y!=0 || panzoom.zoom!=1" @click="panzoomReset">
@@ -101,7 +99,6 @@ export default {
       nodeSelection: {},
 
       stickySockets: false,
-      detailed: false,
       nodeActivity: true
     }
   },
@@ -143,7 +140,7 @@ export default {
 
         const refFrom = this.$refs.nodes.find(n => n.id === link.from)
         const refTo = this.$refs.nodes.find(n => n.id === link.to)
-        if (!refFrom) { // delete link
+        if (!refFrom || !refTo) { // delete link
           return {}
         }
 
@@ -296,15 +293,23 @@ export default {
         drop: (ev) => {
           this.pointerLink.active = false
 
-          const targetNodeId = ev.target.getAttribute('data-nodeid')
-          const targetIn = ev.target.getAttribute('data-in')
-          const targetOut = ev.target.getAttribute('data-out')
+          // find Parent
 
-          // Not a node or same node
-          if (targetNodeId === undefined || targetNodeId === nodeId) {
+          var curTarget = ev.target
+          for (;curTarget = curTarget.parentNode; curTarget != document.body) {
+            if (curTarget.hasAttribute('data-nodeid')) {
+              break
+            }
+          }
+          if (curTarget === document.body) {
             console.error('LINK: target is not a socket')
             return
           }
+
+          const targetNodeId = curTarget.getAttribute('data-nodeid')
+          const targetIn = curTarget.getAttribute('data-in')
+          const targetOut = curTarget.getAttribute('data-out')
+
           let link
           // target is input
           if (targetIn && !isInput) {
@@ -358,6 +363,8 @@ export default {
         this.socketPointerDown(tnode.id, ev, {out: 0})
         return
       }
+      this.$emit('nodeInspect', tnode)
+
       // Switch selection
       if (!this.nodeSelection[tnode.id] && !ev.ctrlKey) this.nodeSelection = {}
       this.nodeSelection[tnode.id] = tnode

+ 13 - 4
browser/vue-flow/src/components/flow/node-activity.vue

@@ -23,7 +23,7 @@
     <icon-fail v-else-if="activity.status=='error'" v-bind="iconProps"class="flow-node__activity-icon"/>
     <icon-question v-else v-bind="iconProps" class="flow-node__activity-icon" />
 
-    <text v-if="ellapsed" class="flow-node__activity-time" x="13" y="4" fill="black">
+    <text :class="{active:ellapsed}" class="flow-node__activity-time" x="13" y="4" fill="black">
       {{ ellapsed }}
     </text>
   </g>
@@ -106,7 +106,7 @@ function dateIsValid (d) {
   opacity:0;
   user-select: none;
   pointer-event:none;
-  transition: all 0.3s;
+  transition: all var(--transition-speed);
 }
 
 .flow-view.flow-node--activity .flow-node__activity {
@@ -115,8 +115,7 @@ function dateIsValid (d) {
 
 .flow-node__activity-background {
   stroke: rgba(0,0,0,0.2);
-  fill: inherits;
-  transition: all 0.3s;
+  transition: all var(--transition-speed);
 }
 
 .flow-node__activity-icon{
@@ -124,6 +123,16 @@ function dateIsValid (d) {
   height:20px;
 }
 
+.flow-node__activity-time{
+  opacity:0;
+  width:0;
+  transition: all var(--transition-speed);
+}
+
+.flow-node__activity-time.active{
+  opacity:1;
+}
+
 .flow-node__activity-icon  >* {
   transform-origin: 32px 32px;
   stroke-width: 6px;

+ 36 - 41
browser/vue-flow/src/components/flow/node.vue

@@ -66,47 +66,47 @@
 
     <!-- Sockets -->
     <!-- input -->
-    <circle
-      class="flow-node__socket flow-node__socket--inputs"
+    <g
       v-for="(inp,i) in inputs"
-      v-bind="inputProps(i)"
+      :key="'in'+i"
       :data-nodeid="id"
       :data-in="i"
-      :class="{'flow-node__socket--match': match.in && (inp == match.in || match.in == 'interface {}' || inp=='interface {}')}"
-      :key="'in'+i"
+      v-bind="inputProps(i)"
       @mousedown.stop.prevent="socketPointerDown($event, {in:i})"
-    />
+      class="flow-node__socket flow-node__socket--inputs"
+      :class="{'flow-node__socket--match': match.in && (inp == match.in || match.in == 'interface {}' || inp=='interface {}')}"
+    >
+      <circle r="5" />
+      <text
+        text-anchor="end"
+        :x="-18"
+        :y="4"
+        class="flow-node__socket-detail"
+      >{{ inp }}</text>
+
+    </g>
     <!-- output -->
-    <circle
+    <g
       v-if="output"
-      class="flow-node__socket flow-node__socket--outputs"
+      v-bind="outputProps(0)"
       :data-nodeid="id"
       :data-out="0"
       :key="'out'+0"
-      :class="{ 'flow-node__socket--match': match.out && (output == match.out || match.out == 'interface {}' || output == 'interface {}'), }"
-      v-bind="outputProps(0)"
       @mousedown.stop.prevent="socketPointerDown($event, {out:0})"
-    />
-
-    <!-- socket labels -->
-
-    <g>
-      <text
-        :key="'inp' +i"
-        class="flow-node__socket-detail"
-        v-for="(inp,i) in inputs"
-        text-anchor="end"
-        :x="inputPos(i).x - 13"
-        :y="inputPos(i).y+5"
-      >{{ inp }}</text>
+      class="flow-node__socket flow-node__socket--outputs"
+      :class="{ 'flow-node__socket--match': match.out && (output == match.out || match.out == 'interface {}' || output == 'interface {}'), }"
+    >
+      <circle r="5" />
       <text
         class="flow-node__socket-detail"
-        :x="outputPos(0).x + 13"
-        :y="outputPos(0).y+5">
+        :x="18"
+        :y="4">
         {{ output }}
       </text>
     </g>
 
+    <!-- socket labels -->
+
     <flow-node-activity
       v-if="activity"
       :activity="activity"
@@ -193,8 +193,7 @@ export default {
       return (i) => {
         const {x, y} = this.inputPos(i)
         return {
-          transform: `translate(${x} ${y})`,
-          r: 5
+          transform: `translate(${x} ${y})`
         }
       }
     },
@@ -259,11 +258,11 @@ export default {
   pointer-events: none;
   stroke-width:1;
   opacity:0;
-  transition: all 0.3s;
+  transition: all var(--transition-speed);
 }
 
 .flow-node__body {
-  transition: all 0.3s;
+  transition: all var(--transition-speed);
 }
 
 .flow-node[status=running] .flow-node__body{
@@ -283,6 +282,13 @@ export default {
   cursor:pointer;
 }
 
+.flow-node__socket-detail {
+  opacity:1;
+  stroke-width:0 !important;
+  fill: #fff !default;
+  transition: all var(--transition-speed);
+}
+
 .flow-node__socket--match {
   cursor:pointer;
   stroke-width:10;
@@ -310,22 +316,11 @@ for hidden
   fill:#333 !default;
 }
 
-.flow-node__socket-detail {
-  pointer-events:none;
-  user-select:none;
-  opacity:0;
-  fill: #fff !default;
-}
-
-.flow-view.flow-node--detail .flow-node__socket-detail {
-  opacity:1;
-}
-
 .flow-node .flow-node__selection {
   opacity:0;
   stroke-width:1;
   pointer-events:none;
-  transition: 0.3s;
+  transition: all var(--transition-speed);
 }
 
 .flow-node--selected .flow-node__selection {

+ 82 - 68
browser/vue-flow/src/components/main.vue

@@ -43,17 +43,32 @@
         @input="documentUpdate"-->
         <hx-split
           dir="horizontal"
-          :resizeable="funcsActive && funcsResizeable"
+          :resizeable="funcsActive"
           :split="funcsActive?funcsSize:'0px'"
           @onSplitResize="funcsSizeUpdate"
 
         >
-          <flow-panel
-            :registry="registry"
-            @toggleResizeable="funcsResizeable=!funcsResizeable"
-            @toggleStickySockets="managerStickySockets=!managerStickySockets"
-          />
-
+          <div class="flow-panel__container">
+            <div class="flow-panel__selector">
+              <button @click="panel='palette'">Funcs</button>
+              <button @click="panel='inspector'">Inspector</button>
+            </div>
+            <transition name="fade">
+              <flow-funcs
+                v-show="panel=='palette'"
+                :registry="registry"
+                @toggleResizeable="funcsResizeable=!funcsResizeable"
+                @toggleStickySockets="managerStickySockets=!managerStickySockets"
+            /></transition>
+            <transition name="fade">
+              <flow-inspector
+                v-show="panel=='inspector'"
+                :registry="registry"
+                :activity="activity"
+                :node-inspect="nodeInspect"
+                @nodeProcess="nodeProcess($event)"
+            /></transition>
+          </div>
           <flow-manager
             ref="flowManager"
             :activity="activity"
@@ -72,7 +87,7 @@
       <!-- Node inspector -->
       <!-- Move this to a different place -->
       <!-- And rename it to inspector -->
-      <hx-modal
+      <!--<hx-modal
         class="flow-modal"
         v-if="nodeInspect"
         @close="nodeInspect=null"
@@ -110,15 +125,22 @@
               </div>
             </div>
             <div class="flow-modal__properties">
+
               <h3>Node Properties</h3>
               <label>Description</label>
               <div class="property">Bogus description</div>
               <label>Help</label>
               <div class="property">Connect to input a thing and goes to output another thing</div>
-              <label>Result</label>
-              <div class="property">{{ activity && activity[nodeInspect.id] && activity[nodeInspect.id].data }}</div>
-              <label>Error</label>
-              <div class="property">{{ activity && activity[nodeInspect.id] && activity[nodeInspect.id].error }}</div>
+              <div v-if="activity && activity[nodeInspect.id] && activity[nodeInspect.id].data" class="flow-modal__properties-result">
+                <label>Result</label>
+                <div class="property">{{ activity && activity[nodeInspect.id] && activity[nodeInspect.id].data }}</div>
+              </div>
+              <div v-if="activity && activity[nodeInspect.id] && activity[nodeInspect.id].error" class="flow-modal__properties-error">
+                <label>Error</label>
+                <div class="property">{{ activity && activity[nodeInspect.id] && activity[nodeInspect.id].error }}</div>
+              </div>
+              <div style="flex: 1 0 100%;">&nbsp;</div>
+              <button class="flow-modal__button-run primary-inverse" @click="nodeProcess(nodeInspect)">Run</button>
             </div>
           </div>
           <label>label</label>
@@ -130,10 +152,9 @@
             v-model="nodeInspect.label" >
         </div>
         <div class="flow-modal__footer" slot="footer">
-          <button @click="nodeInspect=false">Dismiss</button>
-          <button class="primary-inverse" @click="nodeProcess(nodeInspect);nodeInspect = null">Run</button>
+          <button class="primary" @click="nodeInspect=false">OK</button>
         </div>
-      </hx-modal>
+      </hx-modal>-->
 
     </div>
   </div>
@@ -143,70 +164,27 @@ import AppChat from '@/components/chat'
 import FlowManager from '@/components/flow/manager'
 import FlowNode from '@/components/flow/node'
 import FlowPanzoom from '@/components/flow/panzoom'
-import FlowPanel from './panel'
+import FlowFuncs from './panel-funcs'
+import FlowInspector from './panel-inspector'
 import HxSplit from '@/components/shared/hx-split'
 import HxModal from '@/components/shared/hx-modal'
+import defRegistry from './defregistry'
 import 'reset-css/reset.css'
 
 import '@/assets/dark-theme.css'
 import '@/assets/style.css'
 // import nodeData from './nodedata'
 
-const defRegistry = {
-  'Input': {
-    categories: ['core'],
-    output: 'interface {}',
-    style: { color: '#686', textColor: '#fff', shape: 'circle' },
-    props: {} // should be sent in the node
-  },
-  'Variable': {
-    categories: ['core'],
-    output: 'interface {}',
-    style: { color: '#88a', textColor: '#000' },
-    props: {init: ''}
-  },
-  'Const': {
-    categories: ['core'],
-    output: 'interface {}',
-    style: {
-      color: '#555',
-      textColor: '#333'
-    },
-    props: {value: ''}
-  }
-}
-
 export default {
-  components: {FlowManager, FlowPanel, FlowNode, FlowPanzoom, HxSplit, HxModal, AppChat},
+  components: {FlowManager, FlowNode, FlowPanzoom, FlowInspector, FlowFuncs, HxSplit, HxModal, AppChat},
   data () {
     return {
       registry: JSON.parse(JSON.stringify(defRegistry)),
       activity: {},
 
-      /* {
-        // Fixed default stuff
-        'Test': {
-          group: 'Generic',
-          output: 'any',
-          style: {
-            shape: 'thing'
-          }
-        },
-
-        'MatMul': { group: 'Machine learning', inputs: [ '[]float32', '[]float32' ], output: '[]float32', style: { color: '#a44', textColor: 'white' } },
-        'Activator': { group: 'Machine learning', inputs: [ '[]float32' ], output: '[]float32', style: { color: '#a44', textColor: 'white', shape: 'circle' } },
+      panel: 'palette',
 
-        'test': { group: 'Text', inputs: [ '[]float32', 'string' ], output: 'string', style: {'color': '#a93'} },
-        'reverse': { group: 'Text', inputs: [ 'string' ], output: 'string', style: {'color': '#a93'} },
-
-        'fetch': { group: 'json', output: 'json', style: {'color': '#99a'} },
-        'jsonExtract': { group: 'json', inputs: ['json'], output: 'string', style: {'color': '#99a'} },
-
-        'string': { group: 'Visualization', inputs: ['string'], style: {'color': '#9a9'} },
-        'lineGraph': { group: 'Visualization', inputs: ['[]float32', '[]float32'], style: {'color': '#9a9'} }
-      }, */
-
-      nodeInspect: false,
+      nodeInspect: null,
 
       funcsSize: '250px',
       funcsActive: true,
@@ -255,6 +233,10 @@ export default {
     this.$flowService.on('nodeActivity', (v) => {
       this.activity = v.data || {}
     })
+    this.$flowService.on('sessionLog', (v) => {
+      // Make this elsewhere
+      console.log(v.data)
+    })
 
     // Connected
     this.$flowService.connected(() => {
@@ -269,7 +251,9 @@ export default {
   methods: {
     nodeInspectStart (node) { // node
       this.nodeInspect = node
+      this.panel = 'inspector'
       this.$nextTick(() => {
+        // panel input
         if (!this.$refs.modalInput) { return }
         let targetInput = this.$refs.modalInput
         if (this.$refs.nodeInspectProp && this.$refs.nodeInspectProp.length > 0) {
@@ -348,10 +332,11 @@ export default {
 }
 
 .split:not(.resizeable) .content:first-child {
-  transition: all 0.3s;
+  transition: all var(--transition-speed);
 }
 
 .split.resizeable.horizontal .splitter::after {
+  opacity:0.4;
   display:flex;
   justify-content: center;
   align-items: center;
@@ -363,7 +348,7 @@ export default {
   left:0;
   width:10px;
   background: rgba(0,0,0,0.4);
-  transition: all 0.3s;
+  transition: var(--transition-speed);
 }
 
 .app-horizontal {
@@ -382,13 +367,33 @@ export default {
   height:100%;
 }
 
+.flow-panel__container {
+  display:flex;
+  flex-flow:column;
+  flex:1;
+  overflow:hidden;
+}
+
+.flow-panel__selector {
+  color: var(--normal);
+  display:flex;
+  align-content: stretch;
+  flex-shrink: 0;
+  height:50px;
+  border-bottom:solid 1px var(--primary);
+}
+
+.flow-panel__selector button{
+  flex:1;
+}
+
+/*
 .flow-modal__info {
   padding-bottom:20px;
   display:flex;
   flex-flow:row;
 }
 
-/* Columns */
 .flow-modal__info > * {
   margin-right:10px;
   flex:1;
@@ -407,7 +412,16 @@ export default {
   font-size:12px;
 }
 
-.flow-modal__footer {
+.flow-modal__properties-error .property{
+  color: red;
+}
+
+.flow-modal__button-run {
+  align-self: flex-end;
+  width:100%;
 }
 
+.flow-modal__footer {
+}*/
+
 </style>

+ 29 - 19
browser/vue-flow/src/components/panel.vue

@@ -2,19 +2,18 @@
   <div
     class="flow-funcs__container"
     :class="{active:active}">
-    <div class="flow-funcs__control">
-      <!-- make this toggle able -->
-      <button
-        class="item"
-        @click="funcsViewBlocks=!funcsViewBlocks">
-        {{ funcsViewBlocks ? 'List':'Blocks' }} view
-      </button>
-      <button
-        class="item"
-        @click="$emit('toggleResizeable')">
-        Resize
-      </button>
-    </div>
+    <!--<div class="flow-funcs__control">
+    <button
+      class="item"
+      @click="funcsViewBlocks=!funcsViewBlocks">
+      {{ funcsViewBlocks ? 'List':'Blocks' }} view
+    </button>
+    <button
+      class="item"
+      @click="$emit('toggleResizeable')">
+      Resize
+    </button>
+  </div>-->
     <div class="flow-funcs__search">
       <input type="text" placeholder="search" v-model="search">
     </div>
@@ -44,7 +43,6 @@
       </hx-collapsible>
     </div>
   </div>
-
 </template>
 <script>
 import HxCollapsible from '@/components/shared/hx-collapsible'
@@ -69,6 +67,9 @@ export default {
       let group = new Set()
       for (let r in this.registry) {
         for (let c of this.registry[r].categories) {
+          if (this.funcsGroupItems(c).length === 0) {
+            continue
+          }
           group.add(c)
         }
       }
@@ -103,17 +104,21 @@ export default {
 }
 </script>
 <style>
+.flow-funcs {
+  opacity:1;
+}
+
 .flow-funcs__container {
   white-space: nowrap;
   width:0;
-  transition: all 0.3s;
+  transition: all var(--transition-speed);
   height:100%;
   overflow-x:hidden;
   overflow-y:auto;
 }
 
 .flow-funcs__container.active {
-  width:300px;
+  width:100%;
 }
 
 .flow-funcs__control {
@@ -148,16 +153,21 @@ export default {
   margin-top:20px;
 }
 
+.flow-funcs__header {
+  transition: all var(--transition-speed);
+}
+
 .flow-funcs__inner .hx-collapsible__header {
   font-size:14px;
   padding:5px 10px;
-  transition: all 0.3s;
+  transition: all var(--transition-speed);
 }
 
 .flow-funcs__group{
   display:flex;
   flex-flow:column;
   padding:10px;
+  transition: all var(--transition-speed);
 }
 
 .flow-funcs__src {
@@ -165,7 +175,7 @@ export default {
   padding:12px 4px;
   margin-top:1px;
   text-align:center;
-  transition: all 0.3s;
+  transition: all var(--transition-speed);
   position:relative;
   display:flex;
   justify-content: center;
@@ -190,7 +200,7 @@ export default {
 .flow-funcs__group.blocks .flow-funcs__src {
   display:block;
   font-size:10px;
-  width: calc(25% - 2px);
+  width: calc(50% - 2px);
   padding:18px 4px;
   text-overflow: ellipsis;
   margin:1px;

+ 159 - 0
browser/vue-flow/src/components/panel-inspector.vue

@@ -0,0 +1,159 @@
+<template>
+  <div class="flow-inspector">
+    <template v-if="nodeInspect">
+      <!-- VIEWER -->
+      <div class="flow-inspector__content">
+        <svg
+          class="flow-view preview activity flow-node--detail flow-node--activity flow-linking flow-inspector__area flow-inspector--view "
+          width="100%"
+          height="100%"
+          viewBox="0 0 300 200">
+          <flow-panzoom>
+            <flow-node
+              style="pointer-events:none"
+              ref="modalPreviewNode"
+              :id="nodeInspect.id"
+              transform="translate(150,100)"
+              :match="{}"
+              :label="nodeInspect.label"
+              :inputs= "registry[nodeInspect.src].inputs"
+              :output= "registry[nodeInspect.src].output"
+              :activity= "activity[nodeInspect.id]"
+              :nodeStyle= "registry[nodeInspect.src].style"
+            />
+          </flow-panzoom>
+        </svg>
+
+        <!-- DESCRIPTIONS -->
+        <div class="flow-inspector__area flow-inspector--properties ">
+
+          <label>Description</label>
+          <div class="property">Bogus description</div>
+          <label>Help</label>
+          <div class="property">Connect to input a thing and goes to output another thing</div>
+        </div>
+        <div class="flow-inspector__activity flow-inspector__area">
+          <div
+            v-if="activity && activity[nodeInspect.id] && activity[nodeInspect.id].data"
+            class="flow-inspector--properties-result">
+            <label>Result</label>
+            <div class="property">{{ activity && activity[nodeInspect.id] && activity[nodeInspect.id].data }}</div>
+          </div>
+          <div
+            v-if="activity && activity[nodeInspect.id] && activity[nodeInspect.id].error"
+            class="flow-inspector--properties-error">
+            <label>Error</label>
+            <div class="property">{{ activity && activity[nodeInspect.id] && activity[nodeInspect.id].error }}</div>
+          </div>
+        </div>
+        <!-- PARAMETERS -->
+        <div class="flow-inspector--params flow-inspector__area" v-if="nodeInspect.prop" >
+          <div class="flow-inspector__param" v-for="(v,k) in nodeInspect.prop">
+            <label>{{ k }}</label>
+            <input
+              ref="nodeInspectProp"
+              type="text"
+              @keydown.prevent.stop.enter="nodeInspect=null;"
+              @keydown.esc="nodeInspect=null"
+              v-model="nodeInspect.prop[k]">
+          </div>
+        </div>
+
+        <div class="flow-inspector__area  flow-inspector--static">
+          <label>label</label>
+          <input type="text" v-model="nodeInspect.label">
+        </div>
+      <!-- STATIC PARAM -->
+      </div>
+      <div class="flow-inspector__area flow-inspector--control">
+        <button
+          class="primary-inverse"
+          @click="$emit('nodeProcess',nodeInspect)">Run</button>
+      </div>
+    </template>
+    <template v-else>
+      Select a node
+    </template>
+  </div>
+</template>
+<script>
+import FlowPanzoom from '@/components/flow/panzoom'
+import FlowNode from '@/components/flow/node'
+import HxCollapsible from '@/components/shared/hx-collapsible'
+
+export default {
+  name: 'FlowInspector',
+  components: {FlowPanzoom, FlowNode, HxCollapsible},
+  props: {
+    nodeInspect: {type: Object, default: null},
+    activity: {type: Object, default: null},
+    registry: {type: Object, default: null}
+  }
+}
+</script>
+<style>
+.flow-inspector {
+  font-size:12px;
+  flex:1;
+  overflow:hidden;
+  height: available;
+  display:flex;
+  flex-flow:column;
+  color: var(--normal);
+  padding:10px;
+}
+
+.flow-inspector input {
+  background: var(--background);
+  color: var(--normal);
+}
+
+.flow-inspector__area {
+  flex:1;
+}
+
+.flow-inspector__area label{
+  display:block;
+  padding:5px 0;
+  color: var(--primary);
+}
+
+.flow-inspector__area h3{
+  display:none;
+  background: var(--background);
+  font-size:14px;
+  color: var(--primary-darker);
+  padding:4px 4px;
+  border-bottom: solid 1px rgba(150,150,150,0.2);
+}
+
+.flow-inspector__content {
+  display:flex;
+  flex-flow:column;
+  overflow-y:auto;
+  overflow-x: hidden;
+  flex:1;
+}
+
+.flow-inspector--view {
+  flex-basis:150px;
+  flex-shrink: 0;
+}
+
+.flow-inspector--properties-error {
+  color: red;
+}
+
+.flow-inspector--parameters {
+  padding:20px;
+}
+
+.flow-inspector--control {
+  flex:0;
+  padding-top:10px;
+}
+
+.flow-inspector--control button {
+  width: 100%;
+}
+</style>

+ 6 - 4
browser/vue-flow/src/components/shared/hx-collapsible.vue

@@ -50,6 +50,7 @@ export default {
   display:flex;
   flex-direction: column;
 }
+
 /* header */
 .hx-collapsible__header {
   display:flex;
@@ -64,17 +65,18 @@ export default {
 .hx-collapsible__content{
   overflow:hidden;
   opacity:0;
-  max-height:0px;
-  flex-basis: 0px;
+  max-height:0;
+  flex-basis: 0;
   flex-grow:0;
-  transition: all 300ms;
+  transition: all var(--transition-speed);
 }
+
 .hx-collapsible.active .hx-collapsible__content{
   flex-basis:100%;
   flex-grow:1;
   opacity:1;
   max-height:900px;
-  transition: all 300ms;
+  transition: all var(--transition-speed);
 }
 
 </style>

+ 8 - 8
browser/vue-flow/src/components/shared/hx-modal.vue

@@ -34,9 +34,9 @@
   left: 0;
   width: 100%;
   height: 100%;
-  background-color: rgba(0, 0, 0, .5);
+  background-color: rgba(0, 0, 0, 0.5);
   display: table;
-  transition: opacity .3s ease;
+  transition: opacity var(--transition-speed) ease;
 }
 
 .hx-modal__wrapper {
@@ -46,10 +46,10 @@
 
 .hx-modal__container {
   width: 50%;
-  margin: 0px auto;
+  margin: 0 auto;
   padding: 10px 15px;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, .33);
-  transition: all .3s ease;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
+  transition: all var(--transition-speed) ease;
   font-family: Helvetica, Arial, sans-serif;
 }
 
@@ -61,12 +61,12 @@
 }
 
 .hx-modal__header h3 {
-  margin:0px;
+  margin:0;
 }
 
 .hx-modal__body {
-  margin: 0px 0;
-  padding: 20px 0px;
+  margin: 0 0;
+  padding: 20px 0;
 }
 
 .hx-modal__footer {

+ 3 - 2
browser/vue-flow/src/components/shared/hx-toggle-arrow.vue

@@ -18,10 +18,11 @@ export default {
 <style>
 .hx-toggle-arrow {
   transform: rotate(90deg);
-  transition: all 300ms;
+  transition: all var(--transition-speed);
   padding:8px;
-  text-shadow: inset 1px 1px 2px #FFF;
+  text-shadow: inset 1px 1px 2px #fff;
 }
+
 .hx-toggle-arrow.active {
   content:"=";
   transform: rotate(0deg);

+ 56 - 13
browser/vue-flow/yarn.lock

@@ -1745,6 +1745,12 @@ doctrine@^2.0.2:
   dependencies:
     esutils "^2.0.2"
 
+doctrine@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
+  dependencies:
+    esutils "^2.0.2"
+
 dom-converter@~0.1:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.1.4.tgz#a45ef5727b890c9bffe6d7c876e7b19cb0e17f3b"
@@ -2011,12 +2017,11 @@ eslint-plugin-standard@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-3.0.1.tgz#34d0c915b45edc6f010393c7eef3823b08565cf2"
 
-eslint-plugin-vue@^4.0.0-beta.4:
-  version "4.0.0-beta.4"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-4.0.0-beta.4.tgz#d47d8b9dcf0bcc5e417a1ec2ef405bb111822d56"
+eslint-plugin-vue@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-4.2.0.tgz#25fade387bf9a97377cf0e5cd17ef0d60ac9da57"
   dependencies:
-    require-all "^2.2.0"
-    vue-eslint-parser "^2.0.1-beta.1"
+    vue-eslint-parser "^2.0.1"
 
 eslint-scope@^3.7.1, eslint-scope@~3.7.1:
   version "3.7.1"
@@ -2029,7 +2034,7 @@ eslint-visitor-keys@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
 
-eslint@^4.0.0, eslint@^4.14.0:
+eslint@^4.0.0:
   version "4.14.0"
   resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.14.0.tgz#96609768d1dd23304faba2d94b7fefe5a5447a82"
   dependencies:
@@ -2071,6 +2076,48 @@ eslint@^4.0.0, eslint@^4.14.0:
     table "^4.0.1"
     text-table "~0.2.0"
 
+eslint@^4.16.0:
+  version "4.16.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.16.0.tgz#934ada9e98715e1d7bbfd6f6f0519ed2fab35cc1"
+  dependencies:
+    ajv "^5.3.0"
+    babel-code-frame "^6.22.0"
+    chalk "^2.1.0"
+    concat-stream "^1.6.0"
+    cross-spawn "^5.1.0"
+    debug "^3.1.0"
+    doctrine "^2.1.0"
+    eslint-scope "^3.7.1"
+    eslint-visitor-keys "^1.0.0"
+    espree "^3.5.2"
+    esquery "^1.0.0"
+    esutils "^2.0.2"
+    file-entry-cache "^2.0.0"
+    functional-red-black-tree "^1.0.1"
+    glob "^7.1.2"
+    globals "^11.0.1"
+    ignore "^3.3.3"
+    imurmurhash "^0.1.4"
+    inquirer "^3.0.6"
+    is-resolvable "^1.0.0"
+    js-yaml "^3.9.1"
+    json-stable-stringify-without-jsonify "^1.0.1"
+    levn "^0.3.0"
+    lodash "^4.17.4"
+    minimatch "^3.0.2"
+    mkdirp "^0.5.1"
+    natural-compare "^1.4.0"
+    optionator "^0.8.2"
+    path-is-inside "^1.0.2"
+    pluralize "^7.0.0"
+    progress "^2.0.0"
+    require-uncached "^1.0.3"
+    semver "^5.3.0"
+    strip-ansi "^4.0.0"
+    strip-json-comments "~2.0.1"
+    table "^4.0.1"
+    text-table "~0.2.0"
+
 espree@^3.5.2:
   version "3.5.2"
   resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca"
@@ -4646,10 +4693,6 @@ request@~2.79.0:
     tunnel-agent "~0.4.1"
     uuid "^3.0.0"
 
-require-all@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/require-all/-/require-all-2.2.0.tgz#b4420c233ac0282d0ff49b277fb880a8b5de0894"
-
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -5408,9 +5451,9 @@ vm-browserify@0.0.4:
   dependencies:
     indexof "0.0.1"
 
-vue-eslint-parser@^2.0.1-beta.1:
-  version "2.0.1-beta.3"
-  resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.1-beta.3.tgz#cf1391d9c277dd72ee7faa2a2171d71aa1b85f70"
+vue-eslint-parser@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.2.tgz#8d603545e9d7c134699075bd1772af1ffd86b744"
   dependencies:
     debug "^3.1.0"
     eslint-scope "^3.7.1"

+ 17 - 5
go/src/flow/operation.go

@@ -121,6 +121,14 @@ func opFunc(f *Flow, id string) *operation {
 		kind: "func",
 		set:  dumbSet,
 		process: func(ctx OpCtx, params ...Data) Data {
+
+			defer func() {
+				if r := recover(); r != nil {
+					f.Err(fmt.Errorf("Panic: %v", r))
+					f.hooks.error(id, f.Err())
+				}
+			}()
+
 			op, ok := f.getOp(id)
 			if !ok {
 				f.err = fmt.Errorf("invalid operation '%s'", id)
@@ -166,11 +174,11 @@ func opFunc(f *Flow, id string) *operation {
 
 					// Error checking
 					if !p.IsValid() {
-						f.Err(fmt.Errorf("Input %d is not valid %v", i, p))
+						f.Err(fmt.Errorf("Input %d is not valid", i))
 						return
 					}
 					if !p.Type().AssignableTo(fnval.Type().In(i)) {
-						f.Err(fmt.Errorf("Input %d not assignable to %v", i, p))
+						f.Err(fmt.Errorf("Input %d not assignable to %v", i, p.Type()))
 					}
 
 					callParam[i] = reflect.ValueOf(fr)
@@ -186,9 +194,13 @@ func opFunc(f *Flow, id string) *operation {
 			}
 
 			fnret := fnval.Call(callParam)
-			if len(fnret) > 1 && (fnret[1].Interface().(error) != nil) {
-				f.Err(fnret[1].Interface().(error))
-				f.hooks.error(id, f.err)
+			if len(fnret) > 1 && (fnret[1].Interface() != nil) {
+				err, ok := fnret[1].Interface().(error)
+				if !ok {
+					err = errors.New("unknown error")
+				}
+				f.Err(err)
+				f.hooks.error(id, err)
 				return nil
 			}
 

+ 13 - 13
go/src/flow/registry/entry.go

@@ -7,8 +7,8 @@ import (
 
 // Description of an entry
 type Description struct {
-	Name       string   `json:"name"`
-	Categories []string `json:"categories"`
+	Name string   `json:"name"`
+	Tags []string `json:"categories"`
 
 	Inputs     []string       `json:"inputs"`
 	InputDesc  map[int]string `json:"inputsDesc"`
@@ -53,11 +53,11 @@ func NewEntry(r *R, fn interface{}) *Entry {
 	e.Output = fnTyp.Out(0)
 
 	e.Description = &Description{
-		Categories: []string{"generic"},
-		Inputs:     Inputs,
-		Output:     Output,
-		InputDesc:  map[int]string{},
-		Extra:      map[string]interface{}{},
+		Tags:      []string{"generic"},
+		Inputs:    Inputs,
+		Output:    Output,
+		InputDesc: map[int]string{},
+		Extra:     map[string]interface{}{},
 	}
 	return e
 }
@@ -67,12 +67,12 @@ func (e *Entry) Err() error {
 	return e.err
 }
 
-//Categories of the entry
-func (e *Entry) Categories(cat ...string) *Entry {
+//Tags of the entry
+func (e *Entry) Tags(cat ...string) *Entry {
 	if e.err != nil {
 		return e
 	}
-	e.Description.Categories = cat
+	e.Description.Tags = cat
 	return e
 }
 
@@ -106,10 +106,10 @@ func (e *Entry) Extra(name string, extra interface{}) *Entry {
 //Batch helper to batch set properties
 type Batch []*Entry
 
-//Categories set categories of the group
-func (eg Batch) Categories(cat ...string) Batch {
+//Tags set categories of the group
+func (eg Batch) Tags(cat ...string) Batch {
 	for _, e := range eg {
-		e.Categories(cat...)
+		e.Tags(cat...)
 	}
 	return eg
 }

+ 17 - 1
go/src/flow/registry/registry.go

@@ -15,6 +15,9 @@ var (
 	Add          = Global.Add
 )
 
+// M Alias for map[string]interface{}
+type M = map[string]interface{}
+
 // R the function registry
 type R struct {
 	data map[string]*Entry
@@ -25,13 +28,26 @@ func New() *R {
 	return &R{map[string]*Entry{}}
 }
 
+// Clone an existing registry
+func (r *R) Clone() *R {
+	newR := &R{map[string]*Entry{}}
+	for k, v := range r.data {
+		newR.data[k] = v
+	}
+	return newR
+}
+
 // Add unnamed function
 func (r *R) Add(fns ...interface{}) Batch {
 
 	b := Batch{}
 	for _, fn := range fns {
+		if e, ok := fn.(*Entry); ok { // Just add to batch
+			b = append(b, e)
+			continue
+		}
 		name := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()
-		name = path.Base(name)
+		name = path.Ext(name)[1:]
 		b = append(b, r.Register(name, fn))
 	}
 

+ 0 - 29
go/src/flowserver/cmd/flowserver/floatops/floatops.go

@@ -1,29 +0,0 @@
-package floatops
-
-import (
-	"errors"
-	"flow/registry"
-	"math"
-)
-
-// Register into registry
-func Register(r *registry.R) {
-
-	r.Add(sum, sub, div, mul).Categories("floatops")
-}
-func sum(a, b float32) float32 {
-	return a + b
-}
-func sub(a, b float32) float32 {
-	return a - b
-}
-func div(a, b float32) (float32, error) {
-	if b == 0 {
-		return float32(math.NaN()), errors.New("Division by 0")
-	}
-	return a / b, nil
-}
-
-func mul(a, b float32) float32 {
-	return a * b
-}

+ 47 - 13
go/src/flowserver/cmd/flowserver/main.go

@@ -4,10 +4,11 @@ import (
 	"flow"
 	"flow/registry"
 	"flowserver"
-	"flowserver/cmd/flowserver/floatops"
 	"fmt"
 	"io"
 	"log"
+	"math"
+	"strings"
 	"time"
 
 	"github.com/gohxs/prettylog"
@@ -17,18 +18,29 @@ func main() {
 	prettylog.Global()
 	log.Println("Running version:", flowserver.Version)
 
-	floatops.Register(registry.Global)
+	// String functions
+	registry.Add(
+		strings.Split, strings.Join,
+		strings.Compare, strings.Contains,
+		registry.Register("Cat", func(a, b string) string { return a + " " + b }),
+		registry.Register("ToString", func(a interface{}) string { return fmt.Sprint(a) }),
+	).Tags("string").Extra("style", registry.M{"color": "#839"})
 
-	registry.Add(customStruct, setName, setURL).Categories("test-struct")
+	// Math functions
+	registry.Add(
+		math.Abs, math.Cos, math.Sin, math.Exp, math.Exp2, math.Tanh, math.Max, math.Min,
+	).Tags("math").Extra("style", registry.M{"color": "#386"})
 
-	registry.Batch{registry.Register("stream", stream)}.Categories("streamer")
+	// Test functions
+	registry.Add(customNew, customEmpty, customSetName, customSetURL).
+		Tags("testing")
+
+	registry.Add(testErrorPanic, testErrorDelayed).
+		Tags("testing-errors")
 
 	registry.Batch{
 		registry.Register("wait", wait),
-	}.Categories("time-test").
-		Extra("style", map[string]string{
-			"color": "#8a5",
-		})
+	}.Tags("testing-time").Extra("style", map[string]string{"color": "#8a5"})
 
 	addr := ":2015"
 	log.Println("Starting server  at:", ":2015")
@@ -47,20 +59,42 @@ func stream(w io.Writer, val interface{}) interface{} {
 	return val
 }
 
+////////////////////
+// Testing custom struct operations
+/////
+//
 // CustomStruct testing custom struct passing
 type CustomStruct struct {
 	Name string
-	Url  string
+	URL  string
 }
 
-func customStruct() CustomStruct {
+func customNew(name string, url string) CustomStruct {
+	return CustomStruct{name, url}
+}
+func customEmpty() CustomStruct {
 	return CustomStruct{}
 }
-func setName(a CustomStruct, name string) CustomStruct {
+func customSetName(a CustomStruct, name string) CustomStruct {
 	a.Name = name
 	return a
 }
-func setURL(a CustomStruct, url string) CustomStruct {
-	a.Url = url
+func customSetURL(a CustomStruct, url string) CustomStruct {
+	a.URL = url
 	return a
 }
+
+////////////////////////
+// testOps
+////////////////
+func testErrorPanic(n int) flow.Data {
+	dur := time.Duration(n)
+	time.Sleep(dur * time.Second)
+	panic("I panicked")
+}
+
+func testErrorDelayed(n int) (flow.Data, error) {
+	dur := time.Duration(n)
+	time.Sleep(dur * time.Second)
+	return nil, fmt.Errorf("I got an error %v", dur)
+}

+ 16 - 10
go/src/flowserver/flowbuilder.go

@@ -10,9 +10,10 @@ import (
 
 // Node that will contain registry src
 type Node struct {
-	ID   string            `json:"id"`
-	Src  string            `json:"src"`
-	Prop map[string]string `json:"prop"`
+	ID    string            `json:"id"`
+	Src   string            `json:"src"`
+	Label string            `json:"label"`
+	Prop  map[string]string `json:"prop"`
 }
 
 // Link that joins two nodes
@@ -29,7 +30,7 @@ type FlowDocument struct {
 }
 
 // FlowBuild build a flowGraph
-func FlowBuild(rawData []byte) (*flow.Flow, error) {
+func FlowBuild(rawData []byte, r *registry.R) (*flow.Flow, error) {
 	doc := FlowDocument{[]Node{}, []Link{}}
 	err := json.Unmarshal(rawData, &doc)
 	if err != nil {
@@ -37,6 +38,7 @@ func FlowBuild(rawData []byte) (*flow.Flow, error) {
 	}
 
 	f := flow.New()
+	f.SetRegistry(r)
 	nodeMap := map[string]Node{}
 	for _, n := range doc.Nodes {
 		nodeMap[n.ID] = n
@@ -50,7 +52,7 @@ func FlowBuild(rawData []byte) (*flow.Flow, error) {
 		}
 
 		// Find link refered as To
-		entry, err := registry.GetEntry(n.Src)
+		entry, err := r.Entry(n.Src)
 		if err != nil {
 			return nil, err
 		}
@@ -77,15 +79,19 @@ func FlowBuild(rawData []byte) (*flow.Flow, error) {
 			case "Const":
 				// XXX: Automate this in a func
 				newVal := reflect.New(entry.Inputs[l.In])
-				raw := from.Prop["value"]
+				raw := from.Label
+				//raw := from.Prop["value"]
 				if _, ok := newVal.Interface().(*string); ok {
-					raw = "\"" + from.Prop["value"] + "\""
+					log.Println("Trying to unmarshal a string")
+					raw = "\"" + raw + "\""
 				}
+				log.Println("Will unmarshal raw:", raw)
 				err := json.Unmarshal([]byte(raw), newVal.Interface())
 				if err != nil {
-					log.Println("Error", err)
-					param[l.In] = nil
-					continue
+					// ignore error
+					log.Println("unmarshalling Error", err)
+					//param[l.In] = nil
+					//continue
 				}
 				param[l.In] = f.Const(newVal.Elem().Interface())
 			default:

+ 16 - 2
go/src/flowserver/session.go

@@ -186,7 +186,13 @@ func (s *FlowSession) NodeRun(c *websocket.Conn, data []byte) error {
 
 	go func() {
 		log.Printf("Building flow from '%s'\n", string(s.RawDoc))
-		s.flow, err = FlowBuild(s.RawDoc)
+
+		localr := registry.Global.Clone()
+		//Add our log func that is not in global registry
+		localr.Register("Log", s.Log)
+
+		s.flow, err = FlowBuild(s.RawDoc, localr)
+
 		if err != nil {
 			log.Println("Flow error:", err)
 			return
@@ -238,10 +244,18 @@ func (s *FlowSession) NodeRun(c *websocket.Conn, data []byte) error {
 			log.Println("error processing node", s.flow.Err())
 		}
 	}()
-
 	return nil
 }
 
+// Log broadcast a log event
+func (s *FlowSession) Log(v interface{}) (interface{}, error) {
+	err := s.Broadcast(nil, flowmsg.SendMessage{OP: "sessionLog", Data: v})
+	if err != nil {
+		return nil, err
+	}
+	return v, nil
+}
+
 // Broadcast broadcast a message in session besides C
 func (s *FlowSession) Broadcast(c *websocket.Conn, v interface{}) error {
 	s.Lock()