mirror of
https://git.proxmox.com/git/pve-manager
synced 2025-07-15 12:00:57 +00:00

Single letter variable names really do not help understanding what's going on, as do overly general names like fields for the split up words we actually search+filter for. Using a switch block as map is often also not ideal (way more syntax noise and style hacks like break on the same line to keep it compact), rather just use an actual object map. Some of the improvements where not possible when this was implemented, as then we ensured < es5 compat for IE 10 support. With for-of and nullish-chaining a few things to get nicer to express. While at it also fix comment text width making them less look like haikus and improve match calculation comment to avoid implying that match is either 0, 1 or 2, it can be higher too (if multiple columns match). Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
249 lines
5.3 KiB
JavaScript
249 lines
5.3 KiB
JavaScript
/*
|
|
* This is a global search field it loads the /cluster/resources on focus and displays the
|
|
* result in a floating grid. Filtering and sorting is done in the customFilter function
|
|
*
|
|
* Accepts 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);
|
|
},
|
|
focusleave: '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) {
|
|
let me = this;
|
|
|
|
if (me.filterVal === '') {
|
|
item.data.relevance = 0;
|
|
return true;
|
|
}
|
|
// different types have different fields to search, e.g., a node will never have a pool
|
|
const fieldMap = {
|
|
'pool': ['type', 'pool', 'text'],
|
|
'node': ['type', 'node', 'text'],
|
|
'storage': ['type', 'pool', 'node', 'storage'],
|
|
'default': ['name', 'type', 'node', 'pool', 'vmid'],
|
|
};
|
|
let fieldArr = fieldMap[item.data.type] || fieldMap.default;
|
|
|
|
let filterWords = me.filterVal.split(/\s+/);
|
|
|
|
// all text is case insensitive and each split-out word is searched for separately.
|
|
// a row gets 1 point for every partial match, and and additional point for every exact match
|
|
let match = 0;
|
|
for (let field of fieldArr) {
|
|
let fieldValue = item.data[field]?.toString().toLowerCase();
|
|
if (fieldValue === undefined) {
|
|
continue;
|
|
}
|
|
for (let filterWord of filterWords) {
|
|
if (fieldValue.indexOf(filterWord) !== -1) {
|
|
match++; // partial match
|
|
if (fieldValue === filterWord) {
|
|
match++; // exact match is worth more
|
|
}
|
|
}
|
|
}
|
|
}
|
|
item.data.relevance = match; // set the row's virtual 'relevance' value for ordering
|
|
return match > 0;
|
|
},
|
|
|
|
updateFilter: function(field, newValue, oldValue) {
|
|
let 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) {
|
|
let me = this;
|
|
me.hasFocus = true;
|
|
me.grid.textfield = me;
|
|
me.grid.store.load();
|
|
me.grid.showBy(me, 'tl-bl');
|
|
},
|
|
|
|
hideGrid: function() {
|
|
let 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() {
|
|
let me = this;
|
|
if (!me.hasFocus) {
|
|
me.focus();
|
|
} else {
|
|
me.blur();
|
|
}
|
|
},
|
|
|
|
initComponent: function() {
|
|
let me = this;
|
|
|
|
if (!me.tree) {
|
|
throw "no tree given";
|
|
}
|
|
|
|
me.grid = Ext.create(me.grid);
|
|
|
|
me.callParent();
|
|
|
|
// 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',
|
|
});
|
|
});
|
|
},
|
|
});
|