]> git.proxmox.com Git - proxmox-backup.git/blame - www/DataStoreContent.js
ui: DataStoreContent: add 'Verify All' button
[proxmox-backup.git] / www / DataStoreContent.js
CommitLineData
33839735 1Ext.define('pbs-data-store-snapshots', {
ca23a97f 2 extend: 'Ext.data.Model',
d9c38ddc 3 fields: [
d9c38ddc 4 'backup-type',
507c39c5
DM
5 'backup-id',
6 {
e8f0ad19 7 name: 'backup-time',
507c39c5 8 type: 'date',
c6e07769 9 dateFormat: 'timestamp',
507c39c5 10 },
d9c38ddc 11 'files',
04b0ca8b 12 'owner',
7b212c1f 13 'verification',
c6e07769 14 { name: 'size', type: 'int', allowNull: true },
e005f953 15 {
2774566b 16 name: 'crypt-mode',
676b0fde 17 type: 'boolean',
e005f953 18 calculate: function(data) {
2774566b
DC
19 let crypt = {
20 none: 0,
21 mixed: 0,
22 'sign-only': 0,
23 encrypt: 0,
106603c5 24 count: 0,
2774566b 25 };
e005f953
DC
26 data.files.forEach(file => {
27 if (file.filename === 'index.json.blob') return; // is never encrypted
2774566b
DC
28 let mode = PBS.Utils.cryptmap.indexOf(file['crypt-mode']);
29 if (mode !== -1) {
30 crypt[file['crypt-mode']]++;
1bfdae79 31 crypt.count++;
e005f953 32 }
e005f953
DC
33 });
34
106603c5 35 return PBS.Utils.calculateCryptMode(crypt);
c6e07769 36 },
6d55603d
DC
37 },
38 {
39 name: 'matchesFilter',
40 type: 'boolean',
41 defaultValue: true,
42 },
c6e07769 43 ],
ca23a97f
DM
44});
45
46Ext.define('PBS.DataStoreContent', {
e8f0ad19 47 extend: 'Ext.tree.Panel',
ca23a97f
DM
48 alias: 'widget.pbsDataStoreContent',
49
e8f0ad19 50 rootVisible: false,
507c39c5 51
c0ac2074
DC
52 title: gettext('Content'),
53
f1baa7f4
TL
54 controller: {
55 xclass: 'Ext.app.ViewController',
56
57 init: function(view) {
58 if (!view.datastore) {
59 throw "no datastore specified";
60 }
61
3f98b347 62 this.store = Ext.create('Ext.data.Store', {
33839735 63 model: 'pbs-data-store-snapshots',
e8f0ad19
DM
64 groupField: 'backup-group',
65 });
3f98b347 66 this.store.on('load', this.onLoad, this);
e8f0ad19 67
7b1e2669
DC
68 view.getStore().setSorters([
69 'backup-group',
70 'text',
c6e07769 71 'backup-time',
7b1e2669 72 ]);
90779237 73 Proxmox.Utils.monStoreErrors(view, this.store);
f1baa7f4
TL
74 this.reload(); // initial load
75 },
76
77 reload: function() {
3f98b347
TL
78 let view = this.getView();
79
80 if (!view.store || !this.store) {
81 console.warn('cannot reload, no store(s)');
82 return;
83 }
f1baa7f4 84
e8f0ad19 85 let url = `/api2/json/admin/datastore/${view.datastore}/snapshots`;
3f98b347 86 this.store.setProxy({
f1baa7f4 87 type: 'proxmox',
26f499b1 88 timeout: 300*1000, // 5 minutes, we should make that api call faster
c6e07769 89 url: url,
f1baa7f4 90 });
e8f0ad19 91
3f98b347
TL
92 this.store.load();
93 },
e8f0ad19 94
3f98b347
TL
95 getRecordGroups: function(records) {
96 let groups = {};
97
98 for (const item of records) {
99 var btype = item.data["backup-type"];
100 let group = btype + "/" + item.data["backup-id"];
101
102 if (groups[group] !== undefined) {
103 continue;
104 }
105
106 var cls = '';
107 if (btype === 'vm') {
108 cls = 'fa-desktop';
109 } else if (btype === 'ct') {
110 cls = 'fa-cube';
111 } else if (btype === 'host') {
112 cls = 'fa-building';
113 } else {
add5861e 114 console.warn(`got unknown backup-type '${btype}'`);
3f98b347
TL
115 continue; // FIXME: auto render? what do?
116 }
117
118 groups[group] = {
119 text: group,
120 leaf: false,
121 iconCls: "fa " + cls,
122 expanded: false,
123 backup_type: item.data["backup-type"],
124 backup_id: item.data["backup-id"],
c6e07769 125 children: [],
aeee4329 126 };
3f98b347 127 }
aeee4329 128
3f98b347
TL
129 return groups;
130 },
e8f0ad19 131
90779237 132 onLoad: function(store, records, success, operation) {
6d55603d 133 let me = this;
3f98b347
TL
134 let view = this.getView();
135
136 if (!success) {
90779237 137 Proxmox.Utils.setErrorMask(view, Proxmox.Utils.getResponseErrorMessage(operation.getError()));
3f98b347
TL
138 return;
139 }
140
141 let groups = this.getRecordGroups(records);
e8f0ad19 142
69d970a6
DC
143 let selected;
144 let expanded = {};
145
146 view.getSelection().some(function(item) {
147 let id = item.data.text;
148 if (item.data.leaf) {
149 id = item.parentNode.data.text + id;
150 }
151 selected = id;
152 return true;
153 });
154
155 view.getRootNode().cascadeBy({
156 before: item => {
157 if (item.isExpanded() && !item.data.leaf) {
158 let id = item.data.text;
159 expanded[id] = true;
160 return true;
161 }
162 return false;
163 },
c6e07769 164 after: Ext.emptyFn,
69d970a6
DC
165 });
166
3f98b347
TL
167 for (const item of records) {
168 let group = item.data["backup-type"] + "/" + item.data["backup-id"];
169 let children = groups[group].children;
170
171 let data = item.data;
172
f68ae22c 173 data.text = group + '/' + PBS.Utils.render_datetime_utc(data["backup-time"]);
3e395378 174 data.leaf = false;
3f98b347 175 data.cls = 'no-leaf-icons';
6d55603d 176 data.matchesFilter = true;
3f98b347 177
69d970a6
DC
178 data.expanded = !!expanded[data.text];
179
3e395378
DC
180 data.children = [];
181 for (const file of data.files) {
b1149ebb 182 file.text = file.filename;
3e395378
DC
183 file['crypt-mode'] = PBS.Utils.cryptmap.indexOf(file['crypt-mode']);
184 file.leaf = true;
6d55603d 185 file.matchesFilter = true;
3e395378
DC
186
187 data.children.push(file);
188 }
189
3f98b347
TL
190 children.push(data);
191 }
192
423df9b1 193 let nowSeconds = Date.now() / 1000;
3f98b347 194 let children = [];
69d970a6 195 for (const [name, group] of Object.entries(groups)) {
3f98b347 196 let last_backup = 0;
2774566b
DC
197 let crypt = {
198 none: 0,
199 mixed: 0,
200 'sign-only': 0,
106603c5 201 encrypt: 0,
2774566b 202 };
423df9b1
TL
203 let verify = {
204 outdated: 0,
205 none: 0,
206 failed: 0,
207 ok: 0,
208 };
209 for (let item of group.children) {
2774566b 210 crypt[PBS.Utils.cryptmap[item['crypt-mode']]]++;
48e22a89 211 if (item["backup-time"] > last_backup && item.size !== null) {
3f98b347
TL
212 last_backup = item["backup-time"];
213 group["backup-time"] = last_backup;
214 group.files = item.files;
215 group.size = item.size;
04b0ca8b 216 group.owner = item.owner;
c879e5af 217 verify.lastFailed = item.verification && item.verification.state !== 'ok';
3f98b347 218 }
423df9b1
TL
219 if (!item.verification) {
220 verify.none++;
221 } else {
222 if (item.verification.state === 'ok') {
223 verify.ok++;
224 } else {
225 verify.failed++;
226 }
227 let task = Proxmox.Utils.parse_task_upid(item.verification.upid);
228 item.verification.lastTime = task.starttime;
229 if (nowSeconds - task.starttime > 30 * 24 * 60 * 60) {
230 verify.outdated++;
231 }
7b212c1f 232 }
3f98b347 233 }
423df9b1 234 group.verification = verify;
3f98b347 235 group.count = group.children.length;
6d55603d 236 group.matchesFilter = true;
106603c5
DC
237 crypt.count = group.count;
238 group['crypt-mode'] = PBS.Utils.calculateCryptMode(crypt);
69d970a6 239 group.expanded = !!expanded[name];
3f98b347
TL
240 children.push(group);
241 }
242
243 view.setRootNode({
244 expanded: true,
c6e07769 245 children: children,
3f98b347 246 });
69d970a6
DC
247
248 if (selected !== undefined) {
249 let selection = view.getRootNode().findChildBy(function(item) {
250 let id = item.data.text;
251 if (item.data.leaf) {
252 id = item.parentNode.data.text + id;
253 }
254 return selected === id;
255 }, undefined, true);
80db161e
SR
256 if (selection) {
257 view.setSelection(selection);
258 view.getView().focusRow(selection);
259 }
69d970a6
DC
260 }
261
90779237 262 Proxmox.Utils.setErrorMask(view, false);
6d55603d
DC
263 if (view.getStore().getFilters().length > 0) {
264 let searchBox = me.lookup("searchbox");
c6e07769 265 let searchvalue = searchBox.getValue();
6d55603d
DC
266 me.search(searchBox, searchvalue);
267 }
f1baa7f4 268 },
5f448992 269
3e395378 270 onPrune: function(view, rI, cI, item, e, rec) {
c6e07769 271 view = this.getView();
5f448992 272
5f448992
DM
273 if (!(rec && rec.data)) return;
274 let data = rec.data;
3e395378 275 if (rec.parentNode.id !== 'root') return;
5f448992
DM
276
277 if (!view.datastore) return;
278
279 let win = Ext.create('PBS.DataStorePrune', {
280 datastore: view.datastore,
281 backup_type: data.backup_type,
282 backup_id: data.backup_id,
283 });
284 win.on('destroy', this.reload, this);
285 win.show();
98425309
DC
286 },
287
cd92fd73
DC
288 verifyAll: function() {
289 var view = this.getView();
290
291 Proxmox.Utils.API2Request({
292 url: `/admin/datastore/${view.datastore}/verify`,
293 method: 'POST',
294 failure: function(response) {
295 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
296 },
297 success: function(response, options) {
298 Ext.create('Proxmox.window.TaskViewer', {
299 upid: response.result.data,
300 }).show();
301 },
302 });
303 },
304
3e395378 305 onVerify: function(view, rI, cI, item, e, rec) {
484d439a
TL
306 let me = this;
307 view = me.getView();
8f6088c1
DM
308
309 if (!view.datastore) return;
310
8f6088c1
DM
311 if (!(rec && rec.data)) return;
312 let data = rec.data;
313
314 let params;
315
3e395378 316 if (rec.parentNode.id !== 'root') {
8f6088c1
DM
317 params = {
318 "backup-type": data["backup-type"],
319 "backup-id": data["backup-id"],
320 "backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
321 };
322 } else {
323 params = {
324 "backup-type": data.backup_type,
325 "backup-id": data.backup_id,
326 };
327 }
328
329 Proxmox.Utils.API2Request({
330 params: params,
331 url: `/admin/datastore/${view.datastore}/verify`,
332 method: 'POST',
333 failure: function(response) {
334 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
335 },
336 success: function(response, options) {
337 Ext.create('Proxmox.window.TaskViewer', {
338 upid: response.result.data,
484d439a 339 taskDone: () => me.reload(),
8f6088c1
DM
340 }).show();
341 },
342 });
343 },
344
3e395378
DC
345 onForget: function(view, rI, cI, item, e, rec) {
346 let me = this;
c6e07769 347 view = this.getView();
4ff2c9b8 348
4ff2c9b8
DM
349 if (!(rec && rec.data)) return;
350 let data = rec.data;
4ff2c9b8
DM
351 if (!view.datastore) return;
352
3e395378
DC
353 Ext.Msg.show({
354 title: gettext('Confirm'),
355 icon: Ext.Msg.WARNING,
356 message: Ext.String.format(gettext('Are you sure you want to remove snapshot {0}'), `'${data.text}'`),
357 buttons: Ext.Msg.YESNO,
358 defaultFocus: 'no',
359 callback: function(btn) {
360 if (btn !== 'yes') {
361 return;
362 }
4ff2c9b8 363
3e395378
DC
364 Proxmox.Utils.API2Request({
365 params: {
366 "backup-type": data["backup-type"],
367 "backup-id": data["backup-id"],
368 "backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
369 },
370 url: `/admin/datastore/${view.datastore}/snapshots`,
371 method: 'DELETE',
372 waitMsgTarget: view,
373 failure: function(response, opts) {
374 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
375 },
376 callback: me.reload.bind(me),
377 });
4ff2c9b8 378 },
4ff2c9b8
DM
379 });
380 },
381
3e395378 382 downloadFile: function(tV, rI, cI, item, e, rec) {
98425309
DC
383 let me = this;
384 let view = me.getView();
385
98425309 386 if (!(rec && rec.data)) return;
3e395378
DC
387 let data = rec.parentNode.data;
388
389 let file = rec.data.filename;
390 let params = {
391 'backup-id': data['backup-id'],
392 'backup-type': data['backup-type'],
393 'backup-time': (data['backup-time'].getTime()/1000).toFixed(0),
394 'file-name': file,
395 };
396
397 let idx = file.lastIndexOf('.');
398 let filename = file.slice(0, idx);
399 let atag = document.createElement('a');
400 params['file-name'] = file;
401 atag.download = filename;
c6e07769
TL
402 let url = new URL(`/api2/json/admin/datastore/${view.datastore}/download-decoded`,
403 window.location.origin);
3e395378
DC
404 for (const [key, value] of Object.entries(params)) {
405 url.searchParams.append(key, value);
406 }
407 atag.href = url.href;
408 atag.click();
8567c0d2
DC
409 },
410
3e395378 411 openPxarBrowser: function(tv, rI, Ci, item, e, rec) {
8567c0d2
DC
412 let me = this;
413 let view = me.getView();
414
8567c0d2 415 if (!(rec && rec.data)) return;
3e395378 416 let data = rec.parentNode.data;
8567c0d2
DC
417
418 let id = data['backup-id'];
419 let time = data['backup-time'];
420 let type = data['backup-type'];
421 let timetext = PBS.Utils.render_datetime_utc(data["backup-time"]);
422
423 Ext.create('PBS.window.FileBrowser', {
424 title: `${type}/${id}/${timetext}`,
425 datastore: view.datastore,
426 'backup-id': id,
427 'backup-time': (time.getTime()/1000).toFixed(0),
428 'backup-type': type,
3e395378 429 archive: rec.data.filename,
8567c0d2 430 }).show();
6d55603d
DC
431 },
432
433 filter: function(item, value) {
434 if (item.data.text.indexOf(value) !== -1) {
435 return true;
436 }
437
438 if (item.data.owner && item.data.owner.indexOf(value) !== -1) {
439 return true;
440 }
441
442 return false;
443 },
444
445 search: function(tf, value) {
446 let me = this;
447 let view = me.getView();
448 let store = view.getStore();
449 if (!value && value !== 0) {
450 store.clearFilter();
451 store.getRoot().collapseChildren(true);
452 tf.triggers.clear.setVisible(false);
453 return;
454 }
455 tf.triggers.clear.setVisible(true);
456 if (value.length < 2) return;
457 Proxmox.Utils.setErrorMask(view, true);
458 // we do it a little bit later for the error mask to work
459 setTimeout(function() {
460 store.clearFilter();
461 store.getRoot().collapseChildren(true);
462
463 store.beginUpdate();
464 store.getRoot().cascadeBy({
465 before: function(item) {
c6e07769 466 if (me.filter(item, value)) {
6d55603d
DC
467 item.set('matchesFilter', true);
468 if (item.parentNode && item.parentNode.id !== 'root') {
469 item.parentNode.childmatches = true;
470 }
471 return false;
472 }
473 return true;
474 },
475 after: function(item) {
476 if (me.filter(item, value) || item.id === 'root' || item.childmatches) {
477 item.set('matchesFilter', true);
478 if (item.parentNode && item.parentNode.id !== 'root') {
479 item.parentNode.childmatches = true;
480 }
481 if (item.childmatches) {
482 item.expand();
483 }
484 } else {
485 item.set('matchesFilter', false);
486 }
487 delete item.childmatches;
488 },
489 });
490 store.endUpdate();
491
492 store.filter((item) => !!item.get('matchesFilter'));
493 Proxmox.Utils.setErrorMask(view, false);
494 }, 10);
495 },
f1baa7f4
TL
496 },
497
c879e5af
TL
498 viewConfig: {
499 getRowClass: function(record, index) {
500 let verify = record.get('verification');
501 if (verify && verify.lastFailed) {
502 return 'proxmox-invalid-row';
503 }
c6e07769 504 return null;
c879e5af
TL
505 },
506 },
507
04b0ca8b
DC
508 columns: [
509 {
510 xtype: 'treecolumn',
511 header: gettext("Backup Group"),
512 dataIndex: 'text',
c6e07769 513 flex: 1,
04b0ca8b 514 },
3e395378
DC
515 {
516 header: gettext('Actions'),
517 xtype: 'actioncolumn',
518 dataIndex: 'text',
519 items: [
520 {
521 handler: 'onVerify',
522 tooltip: gettext('Verify'),
523 getClass: (v, m, rec) => rec.data.leaf ? 'pmx-hidden' : 'fa fa-search',
524 isDisabled: (v, r, c, i, rec) => !!rec.data.leaf,
525 },
526 {
527 handler: 'onPrune',
528 tooltip: gettext('Prune'),
529 getClass: (v, m, rec) => rec.parentNode.id ==='root' ? 'fa fa-scissors' : 'pmx-hidden',
530 isDisabled: (v, r, c, i, rec) => rec.parentNode.id !=='root',
531 },
532 {
533 handler: 'onForget',
534 tooltip: gettext('Forget Snapshot'),
535 getClass: (v, m, rec) => !rec.data.leaf && rec.parentNode.id !== 'root' ? 'fa critical fa-trash-o' : 'pmx-hidden',
536 isDisabled: (v, r, c, i, rec) => rec.data.leaf || rec.parentNode.id === 'root',
537 },
538 {
539 handler: 'downloadFile',
540 tooltip: gettext('Download'),
541 getClass: (v, m, rec) => rec.data.leaf && rec.data.filename ? 'fa fa-download' : 'pmx-hidden',
542 isDisabled: (v, r, c, i, rec) => !rec.data.leaf || !rec.data.filename || rec.data['crypt-mode'] > 2,
543 },
544 {
545 handler: 'openPxarBrowser',
546 tooltip: gettext('Browse'),
547 getClass: (v, m, rec) => {
548 let data = rec.data;
549 if (data.leaf && data.filename && data.filename.endsWith('pxar.didx')) {
550 return 'fa fa-folder-open-o';
551 }
552 return 'pmx-hidden';
553 },
554 isDisabled: (v, r, c, i, rec) => {
555 let data = rec.data;
556 return !(data.leaf &&
557 data.filename &&
558 data.filename.endsWith('pxar.didx') &&
c96b0de4 559 data['crypt-mode'] < 3);
c6e07769 560 },
3e395378 561 },
c6e07769 562 ],
3e395378 563 },
04b0ca8b
DC
564 {
565 xtype: 'datecolumn',
566 header: gettext('Backup Time'),
567 sortable: true,
568 dataIndex: 'backup-time',
569 format: 'Y-m-d H:i:s',
c6e07769 570 width: 150,
04b0ca8b
DC
571 },
572 {
573 header: gettext("Size"),
574 sortable: true,
575 dataIndex: 'size',
a7a5f56d 576 renderer: (v, meta, record) => {
3e395378
DC
577 if (record.data.text === 'client.log.blob' && v === undefined) {
578 return '';
579 }
a7a5f56d
TL
580 if (v === undefined || v === null) {
581 meta.tdCls = "x-grid-row-loading";
582 return '';
583 }
584 return Proxmox.Utils.format_size(v);
585 },
04b0ca8b
DC
586 },
587 {
588 xtype: 'numbercolumn',
589 format: '0',
590 header: gettext("Count"),
591 sortable: true,
46388e6a
TL
592 width: 75,
593 align: 'right',
04b0ca8b
DC
594 dataIndex: 'count',
595 },
596 {
597 header: gettext("Owner"),
598 sortable: true,
599 dataIndex: 'owner',
600 },
e005f953
DC
601 {
602 header: gettext('Encrypted'),
2774566b 603 dataIndex: 'crypt-mode',
3e395378 604 renderer: (v, meta, record) => {
71282dd9
SR
605 if (record.data.size === undefined || record.data.size === null) {
606 return '';
607 }
3e395378
DC
608 if (v === -1) {
609 return '';
610 }
611 let iconCls = PBS.Utils.cryptIconCls[v] || '';
612 let iconTxt = "";
613 if (iconCls) {
614 iconTxt = `<i class="fa fa-fw fa-${iconCls}"></i> `;
615 }
c6e07769
TL
616 return (iconTxt + PBS.Utils.cryptText[v]) || Proxmox.Utils.unknownText;
617 },
04b0ca8b 618 },
7b212c1f
TL
619 {
620 header: gettext('Verify State'),
621 sortable: true,
622 dataIndex: 'verification',
423df9b1 623 width: 120,
7b212c1f 624 renderer: (v, meta, record) => {
423df9b1
TL
625 let i = (cls, txt) => `<i class="fa fa-fw fa-${cls}"></i> ${txt}`;
626 if (v === undefined || v === null) {
627 return record.data.leaf ? '' : i('question-circle-o warning', gettext('None'));
7b212c1f 628 }
423df9b1 629 let tip, iconCls, txt;
7b212c1f 630 if (record.parentNode.id === 'root') {
423df9b1
TL
631 if (v.failed === 0) {
632 if (v.none === 0) {
633 if (v.outdated > 0) {
634 tip = 'All OK, but some snapshots were not verified in last 30 days';
635 iconCls = 'check warning';
636 txt = gettext('All OK (old)');
637 } else {
638 tip = 'All snapshots verified at least once in last 30 days';
639 iconCls = 'check good';
640 txt = gettext('All OK');
641 }
642 } else if (v.ok === 0) {
643 tip = `${v.none} not verified yet`;
644 iconCls = 'question-circle-o warning';
645 txt = gettext('None');
646 } else {
647 tip = `${v.ok} OK, ${v.none} not verified yet`;
648 iconCls = 'check faded';
649 txt = `${v.ok} OK`;
650 }
651 } else {
652 tip = `${v.ok} OK, ${v.failed} failed, ${v.none} not verified yet`;
653 iconCls = 'times critical';
654 txt = v.ok === 0 && v.none === 0
655 ? gettext('All failed')
656 : `${v.failed} failed`;
657 }
658 } else if (!v.state) {
659 return record.data.leaf ? '' : gettext('None');
660 } else {
661 let verify_time = Proxmox.Utils.render_timestamp(v.lastTime);
662 tip = `Last verify task started on ${verify_time}`;
663 txt = v.state;
664 iconCls = 'times critical';
665 if (v.state === 'ok') {
666 iconCls = 'check good';
667 let now = Date.now() / 1000;
668 if (now - v.lastTime > 30 * 24 * 60 * 60) {
669 tip = `Last verify task over 30 days ago: ${verify_time}`;
670 iconCls = 'check warning';
671 }
672 }
7b212c1f
TL
673 }
674 return `<span data-qtip="${tip}">
423df9b1 675 <i class="fa fa-fw fa-${iconCls}"></i> ${txt}
7b212c1f
TL
676 </span>`;
677 },
678 listeners: {
679 dblclick: function(view, el, row, col, ev, rec) {
680 let data = rec.data || {};
681 let verify = data.verification;
682 if (verify && verify.upid && rec.parentNode.id !== 'root') {
683 let win = Ext.create('Proxmox.window.TaskViewer', {
684 upid: verify.upid,
685 });
686 win.show();
687 }
688 },
689 },
690 },
04b0ca8b 691 ],
b1127fd0 692
04b0ca8b
DC
693 tbar: [
694 {
695 text: gettext('Reload'),
696 iconCls: 'fa fa-refresh',
697 handler: 'reload',
698 },
cd92fd73
DC
699 '-',
700 {
701 xtype: 'proxmoxButton',
702 text: gettext('Verify All'),
703 handler: 'verifyAll',
704 },
6d55603d
DC
705 '->',
706 {
707 xtype: 'tbtext',
708 html: gettext('Search'),
709 },
710 {
711 xtype: 'textfield',
712 reference: 'searchbox',
c3b1da9e 713 emptyText: gettext('group, date or owner'),
6d55603d
DC
714 triggers: {
715 clear: {
716 cls: 'pmx-clear-trigger',
717 weight: -1,
718 hidden: true,
719 handler: function() {
720 this.triggers.clear.setVisible(false);
721 this.setValue('');
722 },
c6e07769 723 },
6d55603d
DC
724 },
725 listeners: {
726 change: {
727 fn: 'search',
728 buffer: 500,
729 },
730 },
c6e07769 731 },
04b0ca8b 732 ],
ca23a97f 733});