luis před 7 roky
rodič
revize
4735885eeb

+ 3 - 4
browser/vue-flow/src/assets/dark-theme.css

@@ -34,10 +34,9 @@
 }
 
 .dark .flow-node__activity[status="running"] .flow-node__activity-icon > * {
-  stroke: #9af;
+  stroke: yellow;
 }
 
-.dark .flow-funcs__search input {
-  color: var(--normal);
-  border-bottom: solid 1px rgba(48, 48, 48, 0.8);
+.dark .flow-node__activity {
+  fill: var(--background-secondary);
 }

+ 22 - 3
browser/vue-flow/src/assets/default-theme.css

@@ -6,7 +6,7 @@
   --background-tertiary: rgba(188, 188, 188, 1);
   --normal: #333;
   --normal-secondary: #999;
-  --primary: #00b0ff;
+  --primary: #5060a0;
   --primary-inverse: #fff;
   --secondary: #666;
   --secondary-inverse: #fff;
@@ -29,10 +29,23 @@
 }
 
 .primary-inverse {
+  position: relative;
   background: var(--primary);
   color: var(--primary-inverse);
 }
 
+.primary-inverse:hover::after {
+  content: " ";
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  transition: all 0.3s;
+  opacity: 0.4;
+  background: #000;
+}
+
 .secondary {
   color: var(--secondary);
 }
@@ -106,7 +119,7 @@ h3 {
 
 .flow-funcs__search input {
   color: var(--normal);
-  border-bottom: solid 1px rgba(198, 188, 188, 0.8);
+  border-bottom: solid 1px rgba(150, 150, 150, 0.2);
 }
 
 .flow-funcs__src {
@@ -238,13 +251,19 @@ h3 {
  * NODE ACTIVITY
  */
 .flow-node__activity {
-  fill: var(--background-secondary);
+  fill: white;
+
+  /* fill: var(--background-secondary); */
 }
 
 .flow-node__activity-icon > * {
   stroke: var(--normal);
 }
 
+.flow-node__activity-time {
+  fill: var(--normal);
+}
+
 /*
  * CHAT
  */

+ 36 - 13
browser/vue-flow/src/components/flow/link.vue

@@ -2,6 +2,7 @@
   <g
     class="flow-link"
     :class="{'flow-link--pointer':pointer}"
+    :status="status"
     @click="$emit('click',$event)">
     <path
       class="flow-link__area"
@@ -9,15 +10,12 @@
     />
 
     <path
-      v-if="color"
-      class="flow-link__status"
-      :stroke="color"
+      class="flow-link__visible"
       :d="path"
     />
-
     <path
-      class="flow-link__visible"
-      stroke="red !important"
+      v-if="status"
+      class="flow-link__status"
       :d="path"
     />
 
@@ -32,8 +30,8 @@ export default {
     y1: {type: Number, default: 0},
     x2: {type: Number, default: 0},
     y2: {type: Number, default: 0},
-    color: {type: String, default: null},
-    pointer: {type: Boolean, default: false}
+    pointer: {type: Boolean, default: false},
+    status: {type: String, default: null}
   },
   computed: {
     path () {
@@ -66,11 +64,6 @@ export default {
   stroke:#333;
 }
 
-.flow-link__status {
-  opacity:0.3;
-  stroke-width:10;
-}
-
 .flow-link .flow-link__area {
   stroke-width:20;
   stroke: transparent;
@@ -86,4 +79,34 @@ export default {
 .flow-link--pointer {
   pointer-events:none;
 }
+
+.flow-link__status {
+  opacity:1;
+  stroke-width:4;
+}
+
+.flow-link[status=waiting] .flow-link__status {
+  stroke-dasharray:8;
+  stroke: grey;
+  animation: dash 10s linear infinite;
+}
+
+.flow-link[status=running] .flow-link__status {
+  stroke-dasharray:4,10;
+  stroke: #aa2;
+  animation: dash 1s linear infinite;
+}
+
+.flow-link[status=finish] .flow-link__status {
+  stroke: green;
+}
+
+.flow-link[status=error] .flow-link__status {
+  stroke: red;
+}
+
+@keyframes dash {
+  from { stroke-dashoffset:100;}
+  to { stroke-dashoffset: 0; }
+}
 </style>

+ 22 - 18
browser/vue-flow/src/components/flow/manager.vue

@@ -8,7 +8,8 @@
       :class="{
         'flow-linking':linking || stickySockets,
         'activity':dragging || pointerLink.active ,
-        'flow-detail': detailed,
+        'flow-node--detail': detailed,
+        'flow-node--activity':nodeActivity,
         'selecting': !!selector
       }"
       @dragover.prevent
@@ -57,6 +58,7 @@
       <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">
         Reset view
@@ -94,11 +96,13 @@ export default {
 
       dragging: null,
       linking: false,
-      stickySockets: false,
-      detailed: false,
       pointerLink: {active: false, props: {}, src: {}},
       selector: null,
-      nodeSelection: {}
+      nodeSelection: {},
+
+      stickySockets: false,
+      detailed: false,
+      nodeActivity: true
     }
   },
   computed: {
@@ -139,24 +143,12 @@ export default {
         const fromOutput = refFrom.outputPos(0) // only 1 output
         const toInput = refTo.inputPos(link.in)
 
-        let color = null
-        if (this.activity[nodeFrom.id]) {
-          if (this.activity[nodeFrom.id].status === 'running') {
-            color = 'blue'
-          }
-          if (this.activity[nodeFrom.id].status === 'finish') {
-            color = 'green'
-          }
-          if (this.activity[nodeFrom.id].status === 'error') {
-            color = 'red'
-          }
-        }
         return {
           x1: nodeFrom.x + fromOutput.x,
           y1: nodeFrom.y + fromOutput.y,
           x2: nodeTo.x + toInput.x,
           y2: nodeTo.y + toInput.y,
-          color: color
+          status: this.activity[nodeFrom.id] && this.activity[nodeFrom.id].status
         }
       }
     }
@@ -218,7 +210,7 @@ export default {
   },
   methods: {
     keyDown (ev) {
-      if (document.activeElement && document.activeElement.matches('input,textarea')) return
+      if (document.activeElement && document.activeElement.matches('input,textarea')) { return }
       if (ev.shiftKey) {
         this.linking = true
       }
@@ -236,6 +228,18 @@ export default {
           for (let k in this.nodeSelection) {
             this.nodeRemove(this.nodeSelection[k])
           }
+          break
+        case 'a':
+          if (ev.ctrlKey) {
+            ev.preventDefault()
+            ev.stopPropagation()
+
+            this.nodeSelection = {}
+            for (let n of this.nodeData.nodes) {
+              this.nodeSelection[n.id] = n
+            }
+          }
+
           break
       }
     },

+ 101 - 14
browser/vue-flow/src/components/flow/node-activity.vue

@@ -1,50 +1,137 @@
 <template>
-  <g class="flow-node__activity"
-     :status="status"
+  <g
+    class="flow-node__activity"
+    :status="activity.status"
   >
-    <circle
+    <!--<circle
       class="flow-node__activity-background"
       rx="0"
       ry="0"
-      r="17"/>
-    <icon-refresh v-if="status=='running'" v-bind="iconProps" class="flow-node__activity-icon" />
-    <icon-wait v-else-if="status=='waiting'" v-bind="iconProps" class="flow-node__activity-icon"/>
-    <icon-ok v-else-if="status=='finish'" v-bind="iconProps"class="flow-node__activity-icon"/>
-    <icon-fail v-else-if="status=='error'" v-bind="iconProps"class="flow-node__activity-icon"/>
+      r="17"/>-->
+    <rect
+      class="flow-node__activity-background"
+      x="-12"
+      y="-12"
+      :width="ellapsed?65:24"
+      height="24"
+      rx="12"
+    />
+
+    <icon-refresh v-if="activity.status=='running'" v-bind="iconProps" class="flow-node__activity-icon" />
+    <icon-wait v-else-if="activity.status=='waiting'" v-bind="iconProps" class="flow-node__activity-icon"/>
+    <icon-ok v-else-if="activity.status=='finish'" v-bind="iconProps"class="flow-node__activity-icon"/>
+    <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">
+      {{ ellapsed }}
+    </text>
   </g>
 </template>
 <script>
+
 import IconWait from '@/assets/icons/wait.svg'
 import IconFail from '@/assets/icons/fail.svg'
 import IconOk from '@/assets/icons/ok.svg'
 import IconQuestion from '@/assets/icons/question.svg'
 import IconRefresh from '@/assets/icons/refresh.svg'
+import utils from '@/utils/utils'
 
 export default {
   name: 'FlowNodeStatus',
   components: {IconWait, IconFail, IconOk, IconQuestion, IconRefresh},
-  props: { status: {type: String, default: ''} },
+  props: { activity: {type: Object, default: () => {}} },
+  data () {
+    return {
+      finishTime: null
+    }
+  },
   computed: {
     iconProps () {
       return {
-        x: -14,
-        y: -14,
+        x: -9,
+        y: -9,
         viewBox: '-4 -4 72 72',
-        width: 28,
-        height: 28
+        width: 18,
+        height: 18
+      }
+    },
+    ellapsed () {
+      if (!this.finishTime) return null
+      const s = new Date(Date.parse(this.activity.startTime))
+      if (!dateIsValid(s)) { return null }
+      const intervalms = this.finishTime - s
+      const sec = intervalms / 1000
+      const min = intervalms / 1000 / 60
+      return utils.padStart(min.toFixed(0), 2, '0') + ':' + utils.padStart(sec.toFixed(0), 2, '0')
+    }
+  },
+  watch: {
+    activity: {
+      handler (val) {
+        console.log('Activity changed', val)
+        const e = new Date(Date.parse(val.endTime))
+        if (dateIsValid(e)) {
+          console.log('End time valid', e.getTime())
+        } else {
+          console.log('End time invalid')
+        }
+      },
+      deep: true
+    }
+  },
+
+  mounted () {
+    console.log('Activity', this.activity)
+    this._timeOut = setTimeout(this.updateTime, 999)
+  },
+  beforeDestroy () {
+    clearTimeout(this._timeOut)
+  },
+  methods: {
+    updateTime () {
+      const finish = new Date(Date.parse(this.activity.endTime))
+      if (dateIsValid(finish)) {
+        console.log('Date is valid')
+        this.finishTime = finish
+        return
+      }
+      this.finishTime = new Date()
+      this._timeOut = setTimeout(this.updateTime, 999)
+    }
+
+  }
+}
+
+const invalidDate = -62135596800000
+function dateIsValid (d) {
+  if (Object.prototype.toString.call(d) === '[object Date]') {
+    if (!isNaN(d.getTime())) { // d.valueOf() could also work
+      if (d.getTime() === invalidDate) {
+        return false
       }
+      return true
     }
   }
+  return false
 }
 </script>
 
 <style>
 .flow-node__activity {
+  font-size:12px;
+  opacity:0;
+  user-select: none;
+  pointer-event:none;
+  transition: all 0.3s;
+}
+
+.flow-view.flow-node--activity .flow-node__activity {
   opacity:0.8;
 }
 
 .flow-node__activity-background {
+  stroke: rgba(0,0,0,0.2);
   fill: inherits;
   transition: all 0.3s;
 }
@@ -64,7 +151,7 @@ export default {
   -webkit-animation: spin 1s infinite linear;
   -moz-animation: spin 1s infinite linear;
   animation: spin 1s infinite linear;
-  stroke: #00c;
+  stroke: #aa0;
 }
 
 .flow-node__activity[status=error] .flow-node__activity-icon  > * {

+ 3 - 3
browser/vue-flow/src/components/flow/node.vue

@@ -108,8 +108,8 @@
     </g>
 
     <flow-node-activity
-      v-if="status"
-      :status="status"
+      v-if="activity"
+      :activity="activity"
       :transform="'translate('+bodyProps.width/2 +','+ -bodyProps.height/2 +')'"/>
   </g>
 </template>
@@ -317,7 +317,7 @@ for hidden
   fill: #fff !default;
 }
 
-.flow-view.flow-detail .flow-node__socket-detail {
+.flow-view.flow-node--detail .flow-node__socket-detail {
   opacity:1;
 }
 

+ 5 - 5
browser/vue-flow/src/components/main.vue

@@ -81,7 +81,7 @@
         <div slot="header">Node inspector:</div>
         <div slot="body" class="flow-modal__body">
           <div class="flow-modal__info">
-            <svg class="flow-view preview activity flow-detail flow-linking" width="100%" height="100%" viewBox="0 0 300 200">
+            <svg class="flow-view preview activity flow-node--detail flow-node--activity flow-linking" width="100%" height="100%" viewBox="0 0 300 200">
               <flow-panzoom>
                 <flow-node
                   style="pointer-events:none"
@@ -117,6 +117,8 @@
               <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>
           </div>
           <label>label</label>
@@ -128,8 +130,8 @@
             v-model="nodeInspect.label" >
         </div>
         <div class="flow-modal__footer" slot="footer">
-          <button class="secondary-inverse" @click="nodeProcess(nodeInspect);nodeInspect = null">Run</button>
-          <button @click="nodeInspect=false">OK</button>
+          <button @click="nodeInspect=false">Dismiss</button>
+          <button class="primary-inverse" @click="nodeProcess(nodeInspect);nodeInspect = null">Run</button>
         </div>
       </hx-modal>
 
@@ -407,8 +409,6 @@ export default {
 }
 
 .flow-modal__footer {
-  display:flex;
-  justify-content: space-between;
 }
 
 </style>

+ 0 - 2
browser/vue-flow/src/components/panel.vue

@@ -140,9 +140,7 @@ export default {
   padding:13px;
   min-width:50px;
   height:50px;
-  color: var(--normal);
   box-shadow:none;
-  border-bottom: solid 1px rgba(198,188,188,0.8)
 }
 
 .flow-funcs__inner {

+ 20 - 5
browser/vue-flow/src/utils/utils.js

@@ -71,26 +71,41 @@ module.exports = {
     }
     let ret = ''
     let j = 0
-    let toggleHL = false
+    let match = ''
     outer: for (let i = 0; i < nlen; i++) {
+      if (match.length > 0) {
+        ret += `<b>${match}</b>`
+        match = ''
+      }
       // let nch = needle.charCodeAt(i)
       let nch = needle[i]
       while (j < hlen) {
         let ch = haystack[j++]
         if (ch.toUpperCase() === nch.toUpperCase()) {
-          if (!toggleHL) { toggleHL = true; ret += '<b>' }
-          ret += `${ch}`
+          match += `${ch}`
           continue outer
         }
-        if (toggleHL) { toggleHL = false; ret += '</b>' }
         ret += ch
       }
       return false
     }
-    if (toggleHL) { toggleHL = false; ret += '</b>' }
+    if (match.length > 0) { ret += `<b>${match}</b>` }
     for (;j < hlen; j++) {
       ret += haystack[j]
     }
     return ret
+  },
+  padStart (str, targetLength, padString) {
+    targetLength = targetLength >> 0 // truncate if number or convert non-number to 0;
+    padString = String((typeof padString !== 'undefined' ? padString : ' '))
+    if (this.length > targetLength) {
+      return String(str)
+    } else {
+      targetLength = targetLength - str.length
+      if (targetLength > padString.length) {
+        padString += padString.repeat(targetLength / padString.length) // append to original to ensure we are longer than needed
+      }
+      return padString.slice(0, targetLength) + String(str)
+    }
   }
 }

+ 12 - 10
go/src/flow/hook.go

@@ -1,38 +1,40 @@
 package flow
 
+import "time"
+
 type Hooks []Hook
 
 // Hook funcs to handle certain events on the flow
 type Hook struct {
-	Wait   func(ID string)
-	Start  func(ID string)
-	Finish func(ID string, res interface{})
-	Error  func(ID string, err error)
-	Any    func(name string, ID string, extra ...interface{})
+	Wait   func(ID string, triggerTime time.Time)
+	Start  func(ID string, triggerTime time.Time)
+	Finish func(ID string, triggerTime time.Time, res interface{})
+	Error  func(ID string, triggerTime time.Time, err error)
+	Any    func(name string, ID string, triggerTime time.Time, extra ...interface{})
 }
 
 // Trigger a hook
 func (hooks Hooks) Trigger(name string, ID string, extra ...Data) {
 	for _, h := range hooks {
 		if h.Any != nil {
-			h.Any(name, ID, extra...)
+			h.Any(name, ID, time.Now(), extra...)
 		}
 		switch name {
 		case "Wait":
 			if h.Wait != nil {
-				h.Wait(ID)
+				h.Wait(ID, time.Now())
 			}
 		case "Start":
 			if h.Start != nil {
-				h.Start(ID)
+				h.Start(ID, time.Now())
 			}
 		case "Finish":
 			if h.Finish != nil {
-				h.Finish(ID, extra[0])
+				h.Finish(ID, time.Now(), extra[0])
 			}
 		case "Error":
 			if h.Error != nil {
-				h.Error(ID, extra[0].(error))
+				h.Error(ID, time.Now(), extra[0].(error))
 			}
 		}
 	}

+ 30 - 3
go/src/flow/operation.go

@@ -8,8 +8,11 @@ package flow
 import (
 	"errors"
 	"fmt"
+	"log"
 	"reflect"
 	"sync"
+
+	"github.com/gohxs/prettylog"
 )
 
 // OpCtx operation Context
@@ -34,9 +37,11 @@ type Operation interface { // Id perhaps?
 
 //local operation information
 type operation struct {
+	sync.Mutex
 	flow    *Flow
 	id      interface{} // Interface key
 	kind    string
+	src     string
 	set     func(params ...Data)
 	process func(ctx OpCtx, params ...Data) Data
 }
@@ -53,16 +58,31 @@ func (o *operation) Process(params ...Data) Data {
 
 // Every single one is run with this internally
 func (o *operation) processWithCtx(ctx OpCtx, params ...Data) Data {
+	entry, _ := o.flow.getOp(fmt.Sprint(o.id))
+	log := prettylog.New(o.ID() + ":" + entry.name)
+
+	log.Printf("Operation waiting")
+	o.Lock()
+	defer o.Unlock()
+	log.Printf("Executing")
+
+	log.Println("Context", ctx)
+
 	if o.flow.err != nil {
 		return nil
 	}
 	if ctx == nil { // No cache/Context
+		log.Printf("Processing")
 		return o.process(ctx, params...)
 	}
 	if v, ok := ctx.Load(o); ok {
+		log.Printf("Cached")
 		return v
 	}
+
+	log.Printf("Processing")
 	res := o.process(ctx, params...)
+	log.Println("Storing", res)
 	ctx.Store(o, res)
 
 	return res
@@ -114,13 +134,16 @@ func opFunc(f *Flow, id string) *operation {
 			op, ok := f.getOp(id)
 			if !ok {
 				f.err = fmt.Errorf("invalid operation '%s'", id)
+				log.Println("Operation not ok", f.err)
 				f.hooks.error(id, f.err)
 				return nil
 			}
 
 			fnval := reflect.ValueOf(op.executor)
 			if fnval.Type().NumIn() != len(op.inputs) {
-				f.hooks.error(id, fmt.Errorf("expect %d inputs got %d", fnval.Type().NumIn(), len(op.inputs)))
+				f.err = fmt.Errorf("expect %d inputs got %d", fnval.Type().NumIn(), len(op.inputs))
+				f.hooks.error(id, f.err)
+				log.Println("Operation not ok", f.err)
 				return nil
 			}
 			/////////////////////////////
@@ -143,7 +166,9 @@ func opFunc(f *Flow, id string) *operation {
 			// Check params
 			for _, p := range callParam {
 				if !p.IsValid() {
-					f.hooks.error(id, errors.New("Input failed"))
+					f.err = fmt.Errorf("Input failed", p)
+					log.Println("Flow err:", f.err)
+					f.hooks.error(id, f.err)
 					return nil
 				}
 			}
@@ -151,7 +176,9 @@ func opFunc(f *Flow, id string) *operation {
 			fnret := fnval.Call(callParam)
 			if len(fnret) > 1 {
 				if err := fnret[1].Interface().(error); err != nil {
-					f.hooks.error(id, err)
+					f.err = err
+					log.Println("Flow err:", f.err)
+					f.hooks.error(id, f.err)
 					return nil
 				}
 			}

+ 5 - 5
go/src/flowserver/cmd/flowserver/main.go

@@ -31,9 +31,9 @@ func main() {
 		})
 
 	registry.Batch{
-		flow.Register("longduration", duration(10)),
-		flow.Register("mediumduration", duration(5)),
-		flow.Register("randomduration", randduration(10)),
+		flow.Register("longduration", duration(10*time.Second)),
+		flow.Register("mediumduration", duration(5*time.Second)),
+		flow.Register("randomduration", randduration(10*time.Second)),
 	}.Categories("time simulation")
 
 	registry.Batch{
@@ -74,7 +74,7 @@ func multiplex6(a1, a2, a3, a4, a5, a6 string) string {
 
 /* Sample funcs */
 func strCat(a1, a2 string) string {
-	return a1 + a2
+	return a1 + " " + a2
 }
 
 func strReverse(s string) string {
@@ -123,7 +123,7 @@ func duration(n time.Duration) func() string {
 }
 func randduration(n time.Duration) func() string {
 	return func() string {
-		time.Sleep(time.Duration(2+rand.Intn(int(n))) * time.Second) // Simulate
+		time.Sleep(time.Duration(2 + rand.Intn(int(n)))) // Simulate
 		return fmt.Sprint("I waited:", n)
 	}
 }

+ 11 - 4
go/src/flowserver/session.go

@@ -10,6 +10,7 @@ import (
 	"os"
 	"path/filepath"
 	"sync"
+	"time"
 
 	"github.com/gorilla/websocket"
 )
@@ -20,8 +21,11 @@ const (
 
 // NodeActivity when nodes are processing
 type NodeActivity struct {
-	Status string    `json:"status"` // nodeStatus, Running, error, result
-	Data   flow.Data `json:"data"`
+	Status    string    `json:"status"` // nodeStatus, Running, error, result
+	StartTime time.Time `json:"startTime"`
+	EndTime   time.Time `json:"endTime"`
+	Data      flow.Data `json:"data"`
+	Error     error     `json:"error"`
 }
 
 // FlowSession Create a session and link clients
@@ -187,7 +191,7 @@ func (s *FlowSession) NodeRun(c *websocket.Conn, data []byte) error {
 			s.flow = nil
 		}()
 		s.flow.Hook(flow.Hook{
-			Any: func(name string, ID string, extra ...flow.Data) {
+			Any: func(name string, ID string, triggerTime time.Time, extra ...flow.Data) {
 				s.Lock()
 				defer s.Unlock()
 
@@ -202,12 +206,15 @@ func (s *FlowSession) NodeRun(c *websocket.Conn, data []byte) error {
 					status = "waiting"
 				case "Start":
 					status = "running"
+					act.StartTime = triggerTime
 				case "Finish":
 					status = "finish"
+					act.EndTime = triggerTime
 					act.Data = extra[0]
 				case "Error":
 					status = "error"
-					act.Data = extra[0]
+					act.EndTime = triggerTime
+					act.Error = extra[0].(error)
 				}
 				if act.Status == status {
 					return