]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/form/GlobalSearchField.js
ui: refactor ui option related methods into UIOptions
[pve-manager.git] / www / manager6 / form / GlobalSearchField.js
1 /*
2 * This is a global search field it loads the /cluster/resources on focus and displays the
3 * result in a floating grid. Filtering and sorting is done in the customFilter function
4 *
5 * Accepts key up/down and enter for input, and it opens to CTRL+SHIFT+F and CTRL+SPACE
6 */
7 Ext.define('PVE.form.GlobalSearchField', {
8 extend: 'Ext.form.field.Text',
9 alias: 'widget.pveGlobalSearchField',
10
11 emptyText: gettext('Search'),
12 enableKeyEvents: true,
13 selectOnFocus: true,
14 padding: '0 5 0 5',
15
16 grid: {
17 xtype: 'gridpanel',
18 userCls: 'proxmox-tags-full',
19 focusOnToFront: false,
20 floating: true,
21 emptyText: Proxmox.Utils.noneText,
22 width: 600,
23 height: 400,
24 scrollable: {
25 xtype: 'scroller',
26 y: true,
27 x: true,
28 },
29 store: {
30 model: 'PVEResources',
31 proxy: {
32 type: 'proxmox',
33 url: '/api2/extjs/cluster/resources',
34 },
35 },
36 plugins: {
37 ptype: 'bufferedrenderer',
38 trailingBufferZone: 20,
39 leadingBufferZone: 20,
40 },
41
42 hideMe: function() {
43 var me = this;
44 if (typeof me.ctxMenu !== 'undefined' && me.ctxMenu.isVisible()) {
45 return;
46 }
47 me.hasFocus = false;
48 if (!me.textfield.hasFocus) {
49 me.hide();
50 }
51 },
52
53 setFocus: function() {
54 var me = this;
55 me.hasFocus = true;
56 },
57
58 listeners: {
59 rowclick: function(grid, record) {
60 var me = this;
61 me.textfield.selectAndHide(record.id);
62 },
63 itemcontextmenu: function(v, record, item, index, event) {
64 var me = this;
65 me.ctxMenu = PVE.Utils.createCmdMenu(v, record, item, index, event);
66 },
67 focusleave: 'hideMe',
68 focusenter: 'setFocus',
69 },
70
71 columns: [
72 {
73 text: gettext('Type'),
74 dataIndex: 'type',
75 width: 100,
76 renderer: PVE.Utils.render_resource_type,
77 },
78 {
79 text: gettext('Description'),
80 flex: 1,
81 dataIndex: 'text',
82 renderer: function(value, mD, rec) {
83 let overrides = PVE.UIOptions.tagOverrides;
84 let tags = PVE.Utils.renderTags(rec.data.tags, overrides);
85 return `${value}${tags}`;
86 },
87 },
88 {
89 text: gettext('Node'),
90 dataIndex: 'node',
91 },
92 {
93 text: gettext('Pool'),
94 dataIndex: 'pool',
95 },
96 ],
97 },
98
99 customFilter: function(item) {
100 let me = this;
101
102 if (me.filterVal === '') {
103 item.data.relevance = 0;
104 return true;
105 }
106 // different types have different fields to search, e.g., a node will never have a pool
107 const fieldMap = {
108 'pool': ['type', 'pool', 'text'],
109 'node': ['type', 'node', 'text'],
110 'storage': ['type', 'pool', 'node', 'storage'],
111 'default': ['name', 'type', 'node', 'pool', 'vmid'],
112 };
113 let fields = fieldMap[item.data.type] || fieldMap.default;
114 let fieldArr = fields.map(field => item.data[field]?.toString().toLowerCase());
115 if (item.data.tags) {
116 let tags = item.data.tags.split(/[;, ]/);
117 fieldArr.push(...tags);
118 }
119
120 let filterWords = me.filterVal.split(/\s+/);
121
122 // all text is case insensitive and each split-out word is searched for separately.
123 // a row gets 1 point for every partial match, and and additional point for every exact match
124 let match = 0;
125 for (let fieldValue of fieldArr) {
126 if (fieldValue === undefined || fieldValue === "") {
127 continue;
128 }
129 for (let filterWord of filterWords) {
130 if (fieldValue.indexOf(filterWord) !== -1) {
131 match++; // partial match
132 if (fieldValue === filterWord) {
133 match++; // exact match is worth more
134 }
135 }
136 }
137 }
138 item.data.relevance = match; // set the row's virtual 'relevance' value for ordering
139 return match > 0;
140 },
141
142 updateFilter: function(field, newValue, oldValue) {
143 let me = this;
144 // parse input and filter store, show grid
145 me.grid.store.filterVal = newValue.toLowerCase().trim();
146 me.grid.store.clearFilter(true);
147 me.grid.store.filterBy(me.customFilter);
148 me.grid.getSelectionModel().select(0);
149 },
150
151 selectAndHide: function(id) {
152 var me = this;
153 me.tree.selectById(id);
154 me.grid.hide();
155 me.setValue('');
156 me.blur();
157 },
158
159 onKey: function(field, e) {
160 var me = this;
161 var key = e.getKey();
162
163 switch (key) {
164 case Ext.event.Event.ENTER:
165 // go to first entry if there is one
166 if (me.grid.store.getCount() > 0) {
167 me.selectAndHide(me.grid.getSelection()[0].data.id);
168 }
169 break;
170 case Ext.event.Event.UP:
171 me.grid.getSelectionModel().selectPrevious();
172 break;
173 case Ext.event.Event.DOWN:
174 me.grid.getSelectionModel().selectNext();
175 break;
176 case Ext.event.Event.ESC:
177 me.grid.hide();
178 me.blur();
179 break;
180 }
181 },
182
183 loadValues: function(field) {
184 let me = this;
185 me.hasFocus = true;
186 me.grid.textfield = me;
187 me.grid.store.load();
188 me.grid.showBy(me, 'tl-bl');
189 },
190
191 hideGrid: function() {
192 let me = this;
193 me.hasFocus = false;
194 if (!me.grid.hasFocus) {
195 me.grid.hide();
196 }
197 },
198
199 listeners: {
200 change: {
201 fn: 'updateFilter',
202 buffer: 250,
203 },
204 specialkey: 'onKey',
205 focusenter: 'loadValues',
206 focusleave: {
207 fn: 'hideGrid',
208 delay: 100,
209 },
210 },
211
212 toggleFocus: function() {
213 let me = this;
214 if (!me.hasFocus) {
215 me.focus();
216 } else {
217 me.blur();
218 }
219 },
220
221 initComponent: function() {
222 let me = this;
223
224 if (!me.tree) {
225 throw "no tree given";
226 }
227
228 me.grid = Ext.create(me.grid);
229
230 me.callParent();
231
232 // bind CTRL + SHIFT + F and CTRL + SPACE to open/close the search
233 me.keymap = new Ext.KeyMap({
234 target: Ext.get(document),
235 binding: [{
236 key: 'F',
237 ctrl: true,
238 shift: true,
239 fn: me.toggleFocus,
240 scope: me,
241 }, {
242 key: ' ',
243 ctrl: true,
244 fn: me.toggleFocus,
245 scope: me,
246 }],
247 });
248
249 // always select first item and sort by relevance after load
250 me.mon(me.grid.store, 'load', function() {
251 me.grid.getSelectionModel().select(0);
252 me.grid.store.sort({
253 property: 'relevance',
254 direction: 'DESC',
255 });
256 });
257 },
258 });