Jelajahi Sumber

Added Enter/Delete shortcut

* Added fuzzy search in panel
luis 7 tahun lalu
induk
melakukan
933ebb8c27

+ 17 - 1
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: #57f;
+  --primary: #00b0ff;
   --primary-inverse: #fff;
   --secondary: #666;
   --secondary-inverse: #fff;
@@ -46,6 +46,17 @@ button {
   color: inherit;
 }
 
+button::after,
+.hover::after {
+  background: var(--primary);
+  opacity: 0;
+}
+
+button:hover::after,
+.hover:hover::after {
+  opacity: 0.2;
+}
+
 input {
   padding: 20px;
   outline: none;
@@ -96,6 +107,11 @@ h3 {
 .flow-funcs__src {
   background: #777;
   color: var(--node-label);
+  text-shadow: 0 0 14px #000, 0 0 4px #000;
+}
+
+.flow-funcs__src b {
+  text-shadow: 0 0 14px #000, 0 0 4px #000;
 }
 
 .flow-funcs__container .flow-funcs__header {

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

@@ -54,13 +54,13 @@ button::after,
   bottom: 0;
   left: 0;
   transition: all 0.3s;
-  background: #222;
   opacity: 0;
+  background: #222 !default;
 }
 
 button:hover::after,
 .hover:hover::after {
-  opacity: 0.2;
+  opacity: 0.2 !default;
 }
 
 ul {

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

@@ -4,7 +4,7 @@
     <div class="flow-chat__area">
       <div class="flow-chat__container">
 
-        <input class="handle" type="text" v-model="handle" @blur="sendChatRename" @keyup.enter="sendChatRename">
+        <input tabindex="-1" class="handle" type="text" v-model="handle" @blur="sendChatRename" @keyup.enter="sendChatRename">
         <div ref="messages" class="flow-chat__messages">
           <div v-for="m in events" class="message">
             <div class="handle">
@@ -14,7 +14,7 @@
             <div class="text">{{ m.message }}</div>
           </div>
         </div>
-        <input ref="input" class="message" @keyup.enter="send" type="text" v-model="input">
+        <input tabindex="-1" ref="input" class="message" @keyup.enter="send" type="text" v-model="input">
       </div> <!-- /container -->
 
       <div class="flow-chat__users">

+ 15 - 0
browser/vue-flow/src/components/flow/link.vue

@@ -7,8 +7,17 @@
       class="flow-link__area"
       :d="path"
     />
+
+    <path
+      v-if="color"
+      class="flow-link__status"
+      :stroke="color"
+      :d="path"
+    />
+
     <path
       class="flow-link__visible"
+      stroke="red !important"
       :d="path"
     />
 
@@ -23,6 +32,7 @@ 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}
   },
   computed: {
@@ -56,6 +66,11 @@ export default {
   stroke:#333;
 }
 
+.flow-link__status {
+  opacity:0.3;
+  stroke-width:10;
+}
+
 .flow-link .flow-link__area {
   stroke-width:20;
   stroke: transparent;

+ 41 - 7
browser/vue-flow/src/components/flow/manager.vue

@@ -51,7 +51,6 @@
           class="flow-selector"
           :class="{'flow-selector--selecting':(selector)?true:false}"
           v-bind="selector"/>
-
       </flow-pan-zoom>
     </svg>
     <div class="flow-container__control">
@@ -140,11 +139,24 @@ 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
+          y2: nodeTo.y + toInput.y,
+          color: color
         }
       }
     }
@@ -206,9 +218,26 @@ export default {
   },
   methods: {
     keyDown (ev) {
+      if (document.activeElement && document.activeElement.matches('input,textarea')) return
       if (ev.shiftKey) {
         this.linking = true
       }
+
+      let single = null
+      const selectionIds = Object.keys(this.nodeSelection)
+      if (selectionIds.length === 1) { single = this.nodeSelection[selectionIds[0]] }
+      switch (ev.key) {
+        case 'Enter':
+          if (!single) { return }
+          this.$emit('nodeInspect', single)
+          break
+        case 'Delete':
+          if (!this.nodeSelection) { return }
+          for (let k in this.nodeSelection) {
+            this.nodeRemove(this.nodeSelection[k])
+          }
+          break
+      }
     },
     keyUp (ev) {
       if (!ev.shiftKey) {
@@ -309,11 +338,8 @@ export default {
       document.activeElement && document.activeElement.blur()
       const tnode = this.nodeData.nodes[i]
       if (ev.button === 1) {
+        this.nodeRemove(tnode)
         // remove related links
-        this.nodeData.links = this.nodeData.links.filter(l => l.from !== tnode.id && l.to !== tnode.id)
-        this.nodeData.nodes.splice(i, 1)
-        this.sendFlowEvent('nodeRemove', tnode)
-        this.sendDocumentUpdate()
         return
       }
       if (ev.button !== 0) return // first button
@@ -354,6 +380,14 @@ export default {
         }
       })
     },
+    nodeRemove (node) {
+      const i = this.nodeData.nodes.indexOf(node)
+      if (i === -1) return
+      this.nodeData.links = this.nodeData.links.filter(l => l.from !== node.id && l.to !== node.id)
+      this.nodeData.nodes.splice(i, 1)
+      this.sendFlowEvent('nodeRemove', node)
+      this.sendDocumentUpdate()
+    },
     nodeAdd (src, x = 100, y = 100) {
       const newNode = {
         id: utils.guid(),
@@ -376,7 +410,7 @@ export default {
       this.sendDocumentUpdate()
     },
     linkPointerClick (ev, link) {
-      if (ev.button !== 1) return
+      console.log('Link click')
       ev.preventDefault()
       this.linkRemove(link)
     },

+ 0 - 1
browser/vue-flow/src/components/flow/node.vue

@@ -150,7 +150,6 @@ export default {
   },
   computed: {
     style () {
-      console.log('Testing parent:', this.$parent)
       return this.nodeStyle || {}
     },
     status () {

+ 17 - 8
browser/vue-flow/src/components/main.vue

@@ -72,7 +72,12 @@
       <!-- Node inspector -->
       <!-- Move this to a different place -->
       <!-- And rename it to inspector -->
-      <hx-modal class="flow-modal" v-if="nodeInspect" @close="nodeInspect=null">
+      <hx-modal
+        class="flow-modal"
+        v-if="nodeInspect"
+        @close="nodeInspect=null"
+        @keydown.esc="nodeInspect=null"
+      >
         <div slot="header">Node inspector:</div>
         <div slot="body" class="flow-modal__body">
           <div class="flow-modal__info">
@@ -92,11 +97,16 @@
                 />
               </flow-panzoom>
             </svg>
-            <div class="flow-modal__params" v-if="nodeInspect.prop" @keydown.enter="nodeInspect=null">
+            <div class="flow-modal__params" v-if="nodeInspect.prop" >
               <h3>Node Parameters</h3>
               <div class="flow-modal__param" v-for="(v,k) in nodeInspect.prop">
                 <label>{{ k }}</label>
-                <input ref="nodeInspectProp" type="text" v-model="nodeInspect.prop[k]">
+                <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-modal__properties">
@@ -113,10 +123,9 @@
           <input
             ref="modalInput"
             type="text"
-            v-model="nodeInspect.label"
+            @keydown.enter="nodeInspect=null;"
             @keydown.esc="nodeInspect=null"
-            @keydown.enter="nodeInspect=null"
-          >
+            v-model="nodeInspect.label" >
         </div>
         <div class="flow-modal__footer" slot="footer">
           <button class="secondary-inverse" @click="nodeProcess(nodeInspect);nodeInspect = null">Run</button>
@@ -262,11 +271,11 @@ export default {
       this.$nextTick(() => {
         if (!this.$refs.modalInput) { return }
         let targetInput = this.$refs.modalInput
-        if (this.$refs.nodeInspectProp.length > 0) {
+        if (this.$refs.nodeInspectProp && this.$refs.nodeInspectProp.length > 0) {
           targetInput = this.$refs.nodeInspectProp[0]
         }
 
-        targetInput.setSelectionRange(0, this.$refs.modalInput.value.length)
+        targetInput.setSelectionRange(0, targetInput.value.length)
         targetInput.focus()
       })
     },

+ 44 - 10
browser/vue-flow/src/components/panel.vue

@@ -15,6 +15,9 @@
         Resize
       </button>
     </div>
+    <div class="flow-funcs__search">
+      <input type="text" placeholder="search" v-model="search">
+    </div>
 
     <div class="flow-funcs__inner">
       <hx-collapsible
@@ -28,16 +31,15 @@
           :class="{blocks:funcsViewBlocks}">
           <div
             ref="src"
-            :key="k"
+            v-for="k in funcsGroupItems(g)"
+            :key="k.src"
             class="flow-funcs__src hover"
             draggable="true"
-            v-for="k in funcsGroup(g)"
-            @dragstart="fnDrag($event,k)"
-            :style="{ 'background': registry[k].style && registry[k].style.color, }"
-            :title="k"
-          >
-            {{ k }}
-          </div>
+            @dragstart="fnDrag($event,k.src)"
+            :style="{ 'background': registry[k.src].style && registry[k.src].style.color, }"
+            :title="k.src"
+            v-html="k.label"
+          />
         </div>
       </hx-collapsible>
     </div>
@@ -46,6 +48,8 @@
 </template>
 <script>
 import HxCollapsible from '@/components/shared/hx-collapsible'
+import utils from '@/utils/utils'
+
 export default {
   name: 'FlowPanel',
   components: {HxCollapsible},
@@ -55,6 +59,7 @@ export default {
   },
   data () {
     return {
+      search: null,
       funcsViewBlocks: true
     }
   },
@@ -69,10 +74,24 @@ export default {
       }
       return [...group]
     },
-    funcsGroup () {
+    funcsGroupItems () {
       return (g) => {
         const ret = Object.keys(this.registry).filter(v => this.registry[v].categories.includes(g))
-        return ret
+          .map(v => {
+            return { src: v, label: v }
+          })
+        if (!this.search) {
+          return ret
+        }
+
+        const filtered = []
+        ret.forEach(e => {
+          let r = utils.fuzzysearch(this.search, e.label)
+          if (r !== false) {
+            filtered.push({src: e.src, label: r})
+          }
+        })
+        return filtered
       }
     }
   },
@@ -116,6 +135,16 @@ export default {
   overflow:hidden;
 }
 
+.flow-funcs__search input {
+  background: transparent;
+  padding:13px;
+  min-width:50px;
+  height:50px;
+  color: var(--normal);
+  box-shadow:none;
+  border-bottom: solid 1px var(--secondary);
+}
+
 .flow-funcs__inner {
   overflow:hidden;
   margin-top:20px;
@@ -149,6 +178,11 @@ export default {
   cursor: -webkit-grab;
 }
 
+.flow-funcs__src b{
+  font-weight:bold;
+  color: var(--primary);
+}
+
 .flow-funcs__group.blocks {
   flex-flow: row;
   flex-wrap: wrap;

+ 35 - 0
browser/vue-flow/src/utils/utils.js

@@ -57,5 +57,40 @@ module.exports = {
       }
     }
     return ret
+  },
+  fuzzysearch (needle, haystack) {
+    var hlen = haystack.length
+    var nlen = needle.length
+    if (nlen > hlen) {
+      return false
+    }
+    if (nlen === hlen) {
+      if (needle === haystack) {
+        return `<b>${needle}</b>`
+      }
+    }
+    let ret = ''
+    let j = 0
+    let toggleHL = false
+    outer: for (let i = 0; i < nlen; i++) {
+      // 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}`
+          continue outer
+        }
+        if (toggleHL) { toggleHL = false; ret += '</b>' }
+        ret += ch
+      }
+      return false
+    }
+    if (toggleHL) { toggleHL = false; ret += '</b>' }
+    for (;j < hlen; j++) {
+      ret += haystack[j]
+    }
+    return ret
   }
 }

+ 17 - 8
go/src/flowserver/cmd/flowserver/main.go

@@ -30,11 +30,15 @@ func main() {
 			"color": "#a55",
 		})
 
+	registry.Batch{
+		flow.Register("longduration", duration(10)),
+		flow.Register("mediumduration", duration(5)),
+		flow.Register("randomduration", randduration(10)),
+	}.Categories("time simulation")
+
 	registry.Batch{
 		flow.Register("multiplex4", multiplex4),
 		flow.Register("multiplex6", multiplex6),
-		flow.Register("longduration", longduration),
-		flow.Register("longduration", longduration),
 		flow.Register("error", func(s string) (string, error) {
 			time.Sleep(4 * time.Second)
 			return "", errors.New("Some error")
@@ -110,11 +114,16 @@ func longduration() string {
 	return " took: " + fmt.Sprint(now.Sub(mark))
 
 }
-func duration(s string) string {
-
-	mark := time.Now()
-	time.Sleep(7 * time.Second) // Simulate
-	now := time.Now()
 
-	return s + " took: " + fmt.Sprint(now.Sub(mark))
+func duration(n time.Duration) func() string {
+	return func() string {
+		time.Sleep(n)
+		return fmt.Sprint("I waited:", n)
+	}
+}
+func randduration(n time.Duration) func() string {
+	return func() string {
+		time.Sleep(time.Duration(2+rand.Intn(int(n))) * time.Second) // Simulate
+		return fmt.Sprint("I waited:", n)
+	}
 }