[pve-devel] [PATCH manager v2 1/2] add globalsearchfield

Dominik Csapak d.csapak at proxmox.com
Tue Jun 21 09:22:54 CEST 2016


this adds a field, the globalsearchfield

its use is to search across our resources (vm,nodes,storage,pools)
quickly and from everywhere

it filters by taking each word you write, and gives
points to resources based on how often the word matches or
if it is an exact match for a field (details in source), and
sorts after this relevance number

with enter or click on an entry, you go directly to the selected
resource

also supports key up/down selecting of the entries and
toggling the search globally with ctrl+space or ctrl+shift+f

Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
---
changes from v1:
 * changed string references of functions in keymap to real functions
   because extjs looks in a different scope for this

 * changed onFocusEnter/Leave to listeners focusenter/leave because
   firefox does not like the extjs implementation of this
   (throws errors on callParent and you cannot immediately open the grid again)
 www/manager6/form/GlobalSearchField.js | 264 +++++++++++++++++++++++++++++++++
 1 file changed, 264 insertions(+)
 create mode 100644 www/manager6/form/GlobalSearchField.js

diff --git a/www/manager6/form/GlobalSearchField.js b/www/manager6/form/GlobalSearchField.js
new file mode 100644
index 0000000..1ebb08d
--- /dev/null
+++ b/www/manager6/form/GlobalSearchField.js
@@ -0,0 +1,264 @@
+/*
+ *  This is a global search field
+ *  it loads the /cluster/resources on focus
+ *  and displays the result in a floating grid
+ *
+ *  it filters and sorts the objects by the algorithm in
+ *  the customFilter function
+ *
+ *  also it does accept key up/down and enter for input
+ *  and it opens to ctrl+shift+f and ctrl+space
+ */
+Ext.define('PVE.form.GlobalSearchField', {
+    extend: 'Ext.form.field.Text',
+    alias: 'widget.pveGlobalSearchField',
+
+    emptyText: gettext('Search'),
+    enableKeyEvents: true,
+    selectOnFocus: true,
+    padding: '0 5 0 5',
+
+    grid: {
+	xtype: 'gridpanel',
+	focusOnToFront: false,
+	floating: true,
+	width: 600,
+	maxHeight: 400,
+	scrollable: {
+	    xtype: 'scroller',
+	    y: true,
+	    x:false
+	},
+	store: {
+	    model: 'PVEResources',
+	    proxy:{
+		type: 'pve',
+		url: '/api2/extjs/cluster/resources'
+	    }
+	},
+	plugins: {
+	    ptype: 'bufferedrenderer',
+	    trailingBufferZone: 20,
+	    leadingBufferZone: 20
+	},
+
+	hideMe: function() {
+	    var me = this;
+	    me.hasFocus = false;
+	    if (!me.textfield.hasFocus) {
+		me.hide();
+	    }
+	},
+
+	setFocus: function() {
+	    var me = this;
+	    me.hasFocus = true;
+	},
+
+	listeners: {
+	    rowclick: function(grid, record) {
+		var me = this;
+		me.textfield.selectAndHide(record.id);
+	    },
+	    /* because of lint */
+	    focusleave: {
+		fn: 'hideMe'
+	    },
+	    focusenter: 'setFocus'
+	},
+
+	columns: [
+	    {
+		text: gettext('Type'),
+		dataIndex: 'type',
+		width: 100,
+		renderer: PVE.Utils.render_resource_type
+	    },
+	    {
+		text: gettext('Description'),
+		flex: 1,
+		dataIndex: 'text'
+	    },
+	    {
+		text: gettext('Node'),
+		dataIndex: 'node'
+	    },
+	    {
+		text: gettext('Pool'),
+		dataIndex: 'pool'
+	    }
+	]
+    },
+
+    customFilter: function(item) {
+	var me = this;
+	var match = 0;
+	var fieldArr = [];
+	var i,j, fields;
+
+	// different types of objects have different fields to search
+	// for example, a node will never have a pool and vice versa
+	switch (item.data.type) {
+	    case 'pool': fieldArr = ['type', 'pool', 'text']; break;
+	    case 'node': fieldArr = ['type', 'node', 'text']; break;
+	    case 'storage': fieldArr = ['type', 'pool', 'node', 'storage']; break;
+	    default: fieldArr = ['name', 'type', 'node', 'pool', 'vmid'];
+	}
+	if (me.filterVal === '') {
+	    item.data.relevance = 0;
+	    return true;
+	}
+
+	// all text is case insensitive and each word is
+	// searched alone
+	// for every partial match, the row gets
+	// 1 match point, for every exact match
+	// it gets 2 points
+	//
+	// results gets sorted by points (descending)
+	fields = me.filterVal.split(/\s+/);
+	for(i = 0; i < fieldArr.length; i++) {
+	    var v = item.data[fieldArr[i]];
+	    if (v !== undefined) {
+		v = v.toString().toLowerCase();
+		for(j = 0; j < fields.length; j++) {
+		    if (v.indexOf(fields[j]) !== -1) {
+			match++;
+			if(v === fields[j]) {
+			    match++;
+			}
+		    }
+		}
+	    }
+	}
+	// give the row the 'relevance' value
+	item.data.relevance = match;
+	return (match > 0);
+    },
+
+    updateFilter: function(field, newValue, oldValue) {
+	var me = this;
+	// parse input and filter store,
+	// show grid
+	me.grid.store.filterVal = newValue.toLowerCase().trim();
+	me.grid.store.clearFilter(true);
+	me.grid.store.filterBy(me.customFilter);
+	me.grid.getSelectionModel().select(0);
+    },
+
+    selectAndHide: function(id) {
+	var me = this;
+	me.tree.selectById(id);
+	me.grid.hide();
+	me.setValue('');
+	me.blur();
+    },
+
+    onKey: function(field, e) {
+	var me = this;
+	var key = e.getKey();
+
+	switch(key) {
+	    case Ext.event.Event.ENTER:
+		// go to first entry if there is one
+		if (me.grid.store.getCount() > 0) {
+		    me.selectAndHide(me.grid.getSelection()[0].data.id);
+		}
+		break;
+	    case Ext.event.Event.UP:
+		me.grid.getSelectionModel().selectPrevious();
+		break;
+	    case Ext.event.Event.DOWN:
+		me.grid.getSelectionModel().selectNext();
+		break;
+	    case Ext.event.Event.ESC:
+		me.grid.hide();
+		me.blur();
+		break;
+	}
+    },
+
+    loadValues: function(field) {
+	var me = this;
+	var records = [];
+
+	me.hasFocus = true;
+	me.grid.textfield = me;
+	me.grid.store.load();
+	me.grid.showBy(me, 'tl-bl');
+    },
+
+    hideGrid: function() {
+	var me = this;
+
+	me.hasFocus = false;
+	if (!me.grid.hasFocus) {
+	    me.grid.hide();
+	}
+    },
+
+    listeners: {
+	change: {
+	    fn: 'updateFilter',
+	    buffer: 250
+	},
+	specialkey: 'onKey',
+	focusenter: 'loadValues',
+	focusleave: {
+	    fn: 'hideGrid',
+	    delay: 100
+	}
+    },
+
+    toggleFocus: function() {
+	var me = this;
+	if (!me.hasFocus) {
+	    me.focus();
+	} else {
+	    me.blur();
+	}
+    },
+
+    initComponent: function() {
+	var me = this;
+
+	if (!me.tree) {
+	    throw "no tree given";
+	}
+
+	me.grid = Ext.create(me.grid);
+
+	me.callParent();
+
+	/*jslint confusion: true*/
+	/*because shift is also a function*/
+	// bind ctrl+shift+f and ctrl+space
+	// to open/close the search
+	me.keymap = new Ext.KeyMap({
+	    target: Ext.get(document),
+	    binding: [{
+		key:'F',
+		ctrl: true,
+		shift: true,
+		fn: me.toggleFocus,
+		scope: me
+	    },{
+		key:' ',
+		ctrl: true,
+		fn: me.toggleFocus,
+		scope: me
+	    }]
+	});
+
+	// always select first item and
+	// sort by relevance after load
+	me.mon(me.grid.store, 'load', function() {
+	    me.grid.getSelectionModel().select(0);
+	    me.grid.store.sort({
+		property: 'relevance',
+		direction: 'DESC'
+	    });
+	});
+    }
+
+});
-- 
2.1.4




More information about the pve-devel mailing list