/* * 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', userCls: 'proxmox-tags-full', focusOnToFront: false, floating: true, emptyText: Proxmox.Utils.noneText, width: 600, height: 400, scrollable: { xtype: 'scroller', y: true, x: true, }, 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', renderer: function(value, mD, rec) { let overrides = PVE.Utils.tagOverrides; let tags = PVE.Utils.renderTags(rec.data.tags, overrides); return `${value}${tags}`; }, }, { 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 fields = fieldMap[item.data.type] || fieldMap.default; let fieldArr = fields.map(field => item.data[field]?.toString().toLowerCase()); if (item.data.tags) { let tags = item.data.tags.split(/[;, ]/); fieldArr.push(...tags); } 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 fieldValue of fieldArr) { if (fieldValue === undefined || fieldValue === "") { 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', }); }); }, });