pve-manager/www/manager6/form/GlobalSearchField.js
Christian Ebner d979f93ed5 Add context menu to global search entries
This add a context menu to the entries of the global search field instead of
showing the browsers default one.
This allows to search and easily manage nodes, VMs and CTs similar to the
behaviour in the resource tree.

Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
2019-01-29 14:04:10 +01:00

273 lines
5.5 KiB
JavaScript

/*
* 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,
emptyText: Proxmox.Utils.noneText,
width: 600,
height: 400,
scrollable: {
xtype: 'scroller',
y: true,
x:false
},
store: {
model: 'PVEResources',
proxy:{
type: 'proxmox',
url: '/api2/extjs/cluster/resources'
}
},
plugins: {
ptype: 'bufferedrenderer',
trailingBufferZone: 20,
leadingBufferZone: 20
},
hideMe: function() {
var me = this;
if (typeof me.ctxMenu !== 'undefined' && me.ctxMenu.isVisible()) {
return;
}
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);
},
itemcontextmenu: function(v, record, item, index, event) {
var me = this;
me.ctxMenu = PVE.Utils.createCmdMenu(v, record, item, index, event);
},
/* 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'
});
});
}
});