[pve-devel] [PATCH manager 1/2] add global search field

Dominik Csapak d.csapak at proxmox.com
Mon Jun 20 15:46:36 CEST 2016


this adds a new field, the global search field

its use is to search across 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/filters with 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>
---
 www/manager6/form/GlobalSearchField.js | 261 +++++++++++++++++++++++++++++++++
 1 file changed, 261 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..89a7d18
--- /dev/null
+++ b/www/manager6/form/GlobalSearchField.js
@@ -0,0 +1,261 @@
+/*
+ *  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
+	},
+
+	onFocusEnter: function() {
+	    var me = this;
+	    me.callParent(arguments);
+	    me.hasFocus = true;
+	},
+
+	onFocusLeave: function() {
+	    var me = this;
+	    me.callParent(arguments);
+	    me.hasFocus = false;
+	    if (!me.textfield.hasFocus) {
+		me.hide();
+	    }
+	},
+
+	listeners: {
+	    rowclick: function(grid, record) {
+		var me = this;
+		me.textfield.selectAndHide(record.id);
+	    }
+	},
+
+	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: 'toggleFocus',
+		scope: me
+	    },{
+		key:' ',
+		ctrl: true,
+		fn: '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