]> git.proxmox.com Git - pve-manager.git/blame - www/manager6/form/GlobalSearchField.js
ui: refactor ui option related methods into UIOptions
[pve-manager.git] / www / manager6 / form / GlobalSearchField.js
CommitLineData
18532680 1/*
ae8f688d
TL
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
18532680 4 *
ae8f688d 5 * Accepts key up/down and enter for input, and it opens to CTRL+SHIFT+F and CTRL+SPACE
18532680
DC
6 */
7Ext.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',
ad4a19f6 18 userCls: 'proxmox-tags-full',
18532680
DC
19 focusOnToFront: false,
20 floating: true,
e7ade592 21 emptyText: Proxmox.Utils.noneText,
18532680 22 width: 600,
b35f19a3 23 height: 400,
18532680
DC
24 scrollable: {
25 xtype: 'scroller',
26 y: true,
ad4a19f6 27 x: true,
18532680
DC
28 },
29 store: {
30 model: 'PVEResources',
8058410f 31 proxy: {
56a353b9 32 type: 'proxmox',
f6710aac
TL
33 url: '/api2/extjs/cluster/resources',
34 },
18532680
DC
35 },
36 plugins: {
37 ptype: 'bufferedrenderer',
38 trailingBufferZone: 20,
f6710aac 39 leadingBufferZone: 20,
18532680
DC
40 },
41
42 hideMe: function() {
43 var me = this;
9f0b4e04
CE
44 if (typeof me.ctxMenu !== 'undefined' && me.ctxMenu.isVisible()) {
45 return;
46 }
18532680
DC
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 },
9f0b4e04
CE
63 itemcontextmenu: function(v, record, item, index, event) {
64 var me = this;
65 me.ctxMenu = PVE.Utils.createCmdMenu(v, record, item, index, event);
66 },
ae8f688d 67 focusleave: 'hideMe',
f6710aac 68 focusenter: 'setFocus',
18532680
DC
69 },
70
71 columns: [
72 {
73 text: gettext('Type'),
74 dataIndex: 'type',
75 width: 100,
f6710aac 76 renderer: PVE.Utils.render_resource_type,
18532680
DC
77 },
78 {
79 text: gettext('Description'),
80 flex: 1,
f6710aac 81 dataIndex: 'text',
ad4a19f6 82 renderer: function(value, mD, rec) {
731436ee 83 let overrides = PVE.UIOptions.tagOverrides;
ad4a19f6
DC
84 let tags = PVE.Utils.renderTags(rec.data.tags, overrides);
85 return `${value}${tags}`;
86 },
18532680
DC
87 },
88 {
89 text: gettext('Node'),
f6710aac 90 dataIndex: 'node',
18532680
DC
91 },
92 {
93 text: gettext('Pool'),
f6710aac
TL
94 dataIndex: 'pool',
95 },
96 ],
18532680
DC
97 },
98
99 customFilter: function(item) {
86b60bd5 100 let me = this;
18532680 101
18532680
DC
102 if (me.filterVal === '') {
103 item.data.relevance = 0;
104 return true;
105 }
ae8f688d
TL
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 };
ad4a19f6
DC
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 }
18532680 119
ae8f688d
TL
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;
ad4a19f6
DC
125 for (let fieldValue of fieldArr) {
126 if (fieldValue === undefined || fieldValue === "") {
86b60bd5
TL
127 continue;
128 }
ae8f688d
TL
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
18532680
DC
134 }
135 }
136 }
137 }
ae8f688d 138 item.data.relevance = match; // set the row's virtual 'relevance' value for ordering
53e3ea84 139 return match > 0;
18532680
DC
140 },
141
142 updateFilter: function(field, newValue, oldValue) {
86b60bd5
TL
143 let me = this;
144 // parse input and filter store, show grid
18532680
DC
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
8058410f 163 switch (key) {
18532680
DC
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) {
86b60bd5 184 let me = this;
18532680
DC
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() {
86b60bd5 192 let me = this;
18532680
DC
193 me.hasFocus = false;
194 if (!me.grid.hasFocus) {
195 me.grid.hide();
196 }
197 },
198
199 listeners: {
200 change: {
201 fn: 'updateFilter',
f6710aac 202 buffer: 250,
18532680
DC
203 },
204 specialkey: 'onKey',
205 focusenter: 'loadValues',
206 focusleave: {
207 fn: 'hideGrid',
f6710aac
TL
208 delay: 100,
209 },
18532680
DC
210 },
211
212 toggleFocus: function() {
86b60bd5 213 let me = this;
18532680
DC
214 if (!me.hasFocus) {
215 me.focus();
216 } else {
217 me.blur();
218 }
219 },
220
221 initComponent: function() {
86b60bd5 222 let me = this;
18532680
DC
223
224 if (!me.tree) {
225 throw "no tree given";
226 }
227
228 me.grid = Ext.create(me.grid);
229
230 me.callParent();
231
86b60bd5 232 // bind CTRL + SHIFT + F and CTRL + SPACE to open/close the search
18532680
DC
233 me.keymap = new Ext.KeyMap({
234 target: Ext.get(document),
235 binding: [{
8058410f 236 key: 'F',
18532680
DC
237 ctrl: true,
238 shift: true,
239 fn: me.toggleFocus,
f6710aac
TL
240 scope: me,
241 }, {
8058410f 242 key: ' ',
18532680
DC
243 ctrl: true,
244 fn: me.toggleFocus,
f6710aac
TL
245 scope: me,
246 }],
18532680
DC
247 });
248
86b60bd5 249 // always select first item and sort by relevance after load
18532680
DC
250 me.mon(me.grid.store, 'load', function() {
251 me.grid.getSelectionModel().select(0);
252 me.grid.store.sort({
253 property: 'relevance',
f6710aac 254 direction: 'DESC',
18532680
DC
255 });
256 });
f6710aac 257 },
18532680 258});