]>
Commit | Line | Data |
---|---|---|
4a580e60 DM |
1 | Ext.define('PVE.storage.Upload', { |
2 | extend: 'Ext.window.Window', | |
3f90858a | 3 | alias: 'widget.pveStorageUpload', |
4a580e60 DM |
4 | |
5 | resizable: false, | |
6 | ||
7 | modal: true, | |
8 | ||
1d8306e4 | 9 | initComponent: function() { |
4a580e60 DM |
10 | var me = this; |
11 | ||
4a580e60 DM |
12 | if (!me.nodename) { |
13 | throw "no node name specified"; | |
14 | } | |
177de3de | 15 | if (!me.storage) { |
4a580e60 DM |
16 | throw "no storage ID specified"; |
17 | } | |
18 | ||
1d8306e4 | 19 | let baseurl = `/nodes/${me.nodename}/storage/${me.storage}/upload`; |
4a580e60 | 20 | |
1d8306e4 | 21 | let pbar = Ext.create('Ext.ProgressBar', { |
4a580e60 | 22 | text: 'Ready', |
1d8306e4 | 23 | hidden: true, |
4a580e60 DM |
24 | }); |
25 | ||
c128543f FE |
26 | let acceptedExtensions = { |
27 | iso: ".img, .iso", | |
28 | vztmpl: ".tar.gz, .tar.xz", | |
29 | }; | |
30 | ||
31 | let defaultContent = me.contents[0] || ''; | |
32 | ||
33 | let fileField = Ext.create('Ext.form.field.File', { | |
34 | name: 'filename', | |
35 | buttonText: gettext('Select File...'), | |
36 | allowBlank: false, | |
37 | setAccept: function(content) { | |
38 | let acceptString = acceptedExtensions[content] || ''; | |
39 | this.fileInputEl.set({ | |
40 | accept: acceptString, | |
41 | }); | |
42 | }, | |
43 | listeners: { | |
44 | afterrender: function(cmp) { | |
45 | cmp.setAccept(defaultContent); | |
46 | }, | |
47 | }, | |
48 | }); | |
49 | ||
4a580e60 DM |
50 | me.formPanel = Ext.create('Ext.form.Panel', { |
51 | method: 'POST', | |
52 | waitMsgTarget: true, | |
53 | bodyPadding: 10, | |
54 | border: false, | |
55 | width: 300, | |
56 | fieldDefaults: { | |
57 | labelWidth: 100, | |
1d8306e4 | 58 | anchor: '100%', |
4a580e60 DM |
59 | }, |
60 | items: [ | |
61 | { | |
62 | xtype: 'pveContentTypeSelector', | |
6c1a2b86 | 63 | cts: me.contents, |
4a580e60 DM |
64 | fieldLabel: gettext('Content'), |
65 | name: 'content', | |
c128543f | 66 | value: defaultContent, |
518a3974 SR |
67 | allowBlank: false, |
68 | listeners: { | |
c128543f FE |
69 | change: function(cmp, newValue, oldValue) { |
70 | fileField.setAccept(newValue); | |
71 | }, | |
72 | }, | |
4a580e60 | 73 | }, |
c128543f | 74 | fileField, |
1d8306e4 TL |
75 | pbar, |
76 | ], | |
4a580e60 DM |
77 | }); |
78 | ||
1d8306e4 | 79 | let form = me.formPanel.getForm(); |
4a580e60 | 80 | |
1d8306e4 | 81 | let doStandardSubmit = function() { |
4a580e60 DM |
82 | form.submit({ |
83 | url: "/api2/htmljs" + baseurl, | |
84 | waitMsg: gettext('Uploading file...'), | |
85 | success: function(f, action) { | |
86 | me.close(); | |
87 | }, | |
88 | failure: function(f, action) { | |
89 | var msg = PVE.Utils.extractFormActionError(action); | |
90 | Ext.Msg.alert(gettext('Error'), msg); | |
1d8306e4 | 91 | }, |
4a580e60 DM |
92 | }); |
93 | }; | |
94 | ||
1d8306e4 | 95 | let updateProgress = function(per, bytes) { |
4a580e60 DM |
96 | var text = (per * 100).toFixed(2) + '%'; |
97 | if (bytes) { | |
e7ade592 | 98 | text += " (" + Proxmox.Utils.format_size(bytes) + ')'; |
4a580e60 DM |
99 | } |
100 | pbar.updateProgress(per, text); | |
101 | }; | |
177de3de | 102 | |
1d8306e4 | 103 | let abortBtn = Ext.create('Ext.Button', { |
4a580e60 DM |
104 | text: gettext('Abort'), |
105 | disabled: true, | |
106 | handler: function() { | |
107 | me.close(); | |
1d8306e4 | 108 | }, |
4a580e60 DM |
109 | }); |
110 | ||
1d8306e4 | 111 | let submitBtn = Ext.create('Ext.Button', { |
4a580e60 DM |
112 | text: gettext('Upload'), |
113 | disabled: true, | |
114 | handler: function(button) { | |
115 | var fd; | |
116 | try { | |
117 | fd = new FormData(); | |
118 | } catch (err) { | |
119 | doStandardSubmit(); | |
120 | return; | |
121 | } | |
122 | ||
123 | button.setDisabled(true); | |
124 | abortBtn.setDisabled(false); | |
125 | ||
126 | var field = form.findField('content'); | |
127 | fd.append("content", field.getValue()); | |
128 | field.setDisabled(true); | |
129 | ||
130 | field = form.findField('filename'); | |
131 | var file = field.fileInputEl.dom; | |
132 | fd.append("filename", file.files[0]); | |
133 | field.setDisabled(true); | |
134 | ||
135 | pbar.setVisible(true); | |
136 | updateProgress(0); | |
137 | ||
1d8306e4 TL |
138 | let xhr = new XMLHttpRequest(); |
139 | me.xhr = xhr; | |
4a580e60 | 140 | |
177de3de | 141 | xhr.addEventListener("load", function(e) { |
1d8306e4 | 142 | if (xhr.status === 200) { |
4a580e60 | 143 | me.close(); |
1d8306e4 TL |
144 | return; |
145 | } | |
146 | let err = Ext.htmlEncode(xhr.statusText); | |
147 | let msg = `${gettext('Error')} ${xhr.status.toString()}: ${err}`; | |
148 | if (xhr.responseText !== "") { | |
149 | let result = Ext.decode(xhr.responseText); | |
150 | result.message = msg; | |
151 | msg = Proxmox.Utils.extractRequestError(result, true); | |
177de3de | 152 | } |
1d8306e4 | 153 | Ext.Msg.alert(gettext('Error'), msg, btn => me.close()); |
4a580e60 DM |
154 | }, false); |
155 | ||
156 | xhr.addEventListener("error", function(e) { | |
1d8306e4 TL |
157 | let err = e.target.status.toString(); |
158 | let msg = `Error '${err}' occurred while receiving the document.`; | |
159 | Ext.Msg.alert(gettext('Error'), msg, btn => me.close()); | |
4a580e60 | 160 | }); |
177de3de | 161 | |
4a580e60 | 162 | xhr.upload.addEventListener("progress", function(evt) { |
177de3de | 163 | if (evt.lengthComputable) { |
1d8306e4 | 164 | let percentComplete = evt.loaded / evt.total; |
4a580e60 | 165 | updateProgress(percentComplete, evt.loaded); |
177de3de | 166 | } |
4a580e60 DM |
167 | }, false); |
168 | ||
1d8306e4 | 169 | xhr.open("POST", `/api2/json${baseurl}`, true); |
177de3de | 170 | xhr.send(fd); |
1d8306e4 | 171 | }, |
4a580e60 DM |
172 | }); |
173 | ||
1d8306e4 | 174 | form.on('validitychange', (f, valid) => submitBtn.setDisabled(!valid)); |
4a580e60 | 175 | |
1d8306e4 TL |
176 | Ext.apply(me, { |
177 | title: gettext('Upload'), | |
4a580e60 | 178 | items: me.formPanel, |
1d8306e4 | 179 | buttons: [abortBtn, submitBtn], |
4a580e60 DM |
180 | listeners: { |
181 | close: function() { | |
1d8306e4 TL |
182 | if (me.xhr) { |
183 | me.xhr.abort(); | |
184 | delete me.xhr; | |
4a580e60 | 185 | } |
1d8306e4 TL |
186 | }, |
187 | }, | |
4a580e60 DM |
188 | }); |
189 | ||
190 | me.callParent(); | |
1d8306e4 | 191 | }, |
4a580e60 DM |
192 | }); |
193 | ||
af3c0a92 LS |
194 | Ext.define('PVE.storage.DownloadUrl', { |
195 | extend: 'Proxmox.window.Edit', | |
196 | alias: 'widget.pveStorageDownloadUrl', | |
197 | mixins: ['Proxmox.Mixin.CBind'], | |
198 | ||
199 | isCreate: true, | |
200 | ||
201 | method: 'POST', | |
202 | ||
203 | showTaskViewer: true, | |
204 | ||
205 | title: gettext('Download from URL'), | |
206 | submitText: gettext('Download'), | |
207 | ||
208 | cbindData: function(initialConfig) { | |
209 | var me = this; | |
210 | return { | |
211 | nodename: me.nodename, | |
212 | storage: me.storage, | |
213 | content: me.content, | |
214 | }; | |
215 | }, | |
216 | ||
217 | cbind: { | |
218 | url: '/nodes/{nodename}/storage/{storage}/download-url', | |
219 | }, | |
220 | ||
221 | controller: { | |
222 | xclass: 'Ext.app.ViewController', | |
223 | ||
224 | urlChange: function(field) { | |
225 | let me = this; | |
226 | let view = me.getView(); | |
227 | field = view.down('[name=url]'); | |
228 | field.setValidation(gettext("Please check URL")); | |
229 | field.validate(); | |
230 | view.setValues({ | |
231 | size: gettext("unknown"), | |
232 | mimetype: gettext("unknown"), | |
233 | }); | |
234 | }, | |
235 | ||
236 | urlCheck: function(field) { | |
237 | let me = this; | |
238 | let view = me.getView(); | |
239 | field = view.down('[name=url]'); | |
240 | view.setValues({ | |
241 | size: gettext("unknown"), | |
242 | mimetype: gettext("unknown"), | |
243 | }); | |
244 | Proxmox.Utils.API2Request({ | |
245 | url: `/nodes/${view.nodename}/query-url-metadata`, | |
246 | method: 'GET', | |
247 | params: { | |
248 | url: field.getValue(), | |
249 | 'verify-certificates': view.getValues()['verify-certificates'], | |
250 | }, | |
251 | waitMsgTarget: view, | |
252 | failure: function(res, opt) { | |
253 | field.setValidation(res.result.message); | |
254 | field.validate(); | |
255 | }, | |
256 | success: function(res, opt) { | |
257 | field.setValidation(); | |
258 | field.validate(); | |
259 | ||
260 | let data = res.result.data; | |
261 | view.setValues({ | |
262 | filename: data.filename || "", | |
263 | size: (data.size && Proxmox.Utils.format_size(data.size)) || gettext("unknown"), | |
264 | mimetype: data.mimetype || gettext("unknown"), | |
265 | }); | |
266 | }, | |
267 | }); | |
268 | }, | |
269 | ||
270 | hashChange: function(field) { | |
271 | let checksum = Ext.getCmp('downloadUrlChecksum'); | |
272 | if (field.getValue() === '__default__') { | |
273 | checksum.setDisabled(true); | |
274 | checksum.setValue(""); | |
275 | checksum.allowBlank = true; | |
276 | } else { | |
277 | checksum.setDisabled(false); | |
278 | checksum.allowBlank = false; | |
279 | } | |
280 | }, | |
281 | }, | |
282 | ||
283 | items: [ | |
284 | { | |
285 | xtype: 'inputpanel', | |
286 | border: false, | |
287 | columnT: [ | |
288 | { | |
289 | xtype: 'fieldcontainer', | |
290 | layout: 'hbox', | |
291 | fieldLabel: gettext('URL'), | |
292 | items: [ | |
293 | { | |
294 | xtype: 'textfield', | |
295 | name: 'url', | |
296 | allowBlank: false, | |
297 | flex: 1, | |
298 | listeners: { | |
299 | change: 'urlChange', | |
300 | }, | |
301 | }, | |
302 | { | |
303 | xtype: 'button', | |
304 | name: 'check', | |
305 | text: gettext('Check'), | |
306 | margin: '0 0 0 5', | |
307 | listeners: { | |
308 | click: 'urlCheck', | |
309 | }, | |
310 | }, | |
311 | ], | |
312 | }, | |
313 | { | |
314 | xtype: 'textfield', | |
315 | name: 'filename', | |
316 | allowBlank: false, | |
317 | fieldLabel: gettext('File name'), | |
318 | }, | |
319 | ], | |
320 | column1: [ | |
321 | { | |
322 | xtype: 'displayfield', | |
323 | name: 'size', | |
324 | fieldLabel: gettext('File size'), | |
325 | value: gettext('unknown'), | |
326 | }, | |
327 | ], | |
328 | column2: [ | |
329 | { | |
330 | xtype: 'displayfield', | |
331 | name: 'mimetype', | |
332 | fieldLabel: gettext('MIME type'), | |
333 | value: gettext('unknown'), | |
334 | }, | |
335 | ], | |
336 | advancedColumn1: [ | |
337 | { | |
338 | xtype: 'pveHashAlgorithmSelector', | |
339 | name: 'checksum-algorithm', | |
340 | fieldLabel: gettext('Hash algorithm'), | |
341 | allowBlank: true, | |
342 | hasNoneOption: true, | |
343 | value: '__default__', | |
344 | listeners: { | |
345 | change: 'hashChange', | |
346 | }, | |
347 | }, | |
348 | { | |
349 | xtype: 'textfield', | |
350 | name: 'checksum', | |
351 | fieldLabel: gettext('Checksum'), | |
352 | allowBlank: true, | |
353 | disabled: true, | |
354 | emptyText: gettext('none'), | |
355 | id: 'downloadUrlChecksum', | |
356 | }, | |
357 | ], | |
358 | advancedColumn2: [ | |
359 | { | |
360 | xtype: 'proxmoxcheckbox', | |
361 | name: 'verify-certificates', | |
362 | fieldLabel: gettext('Verify certificates'), | |
363 | uncheckedValue: 0, | |
364 | checked: true, | |
365 | listeners: { | |
366 | change: 'urlChange', | |
367 | }, | |
368 | }, | |
369 | ], | |
370 | }, | |
371 | { | |
372 | xtype: 'hiddenfield', | |
373 | name: 'content', | |
374 | cbind: { | |
375 | value: '{content}', | |
376 | }, | |
377 | }, | |
378 | ], | |
379 | ||
380 | initComponent: function() { | |
381 | var me = this; | |
382 | ||
383 | if (!me.nodename) { | |
384 | throw "no node name specified"; | |
385 | } | |
386 | if (!me.storage) { | |
387 | throw "no storage ID specified"; | |
388 | } | |
389 | ||
390 | me.callParent(); | |
391 | }, | |
392 | }); | |
393 | ||
4a580e60 DM |
394 | Ext.define('PVE.storage.ContentView', { |
395 | extend: 'Ext.grid.GridPanel', | |
396 | ||
3f90858a | 397 | alias: 'widget.pveStorageContentView', |
4a580e60 | 398 | |
c90539de DC |
399 | viewConfig: { |
400 | trackOver: false, | |
1d8306e4 | 401 | loadMask: false, |
c90539de | 402 | }, |
1d8306e4 | 403 | initComponent: function() { |
4a580e60 DM |
404 | var me = this; |
405 | ||
dbeddeb3 FE |
406 | if (!me.nodename) { |
407 | me.nodename = me.pveSelNode.data.node; | |
408 | if (!me.nodename) { | |
409 | throw "no node name specified"; | |
410 | } | |
4a580e60 | 411 | } |
1d8306e4 | 412 | const nodename = me.nodename; |
4a580e60 | 413 | |
dbeddeb3 FE |
414 | if (!me.storage) { |
415 | me.storage = me.pveSelNode.data.storage; | |
416 | if (!me.storage) { | |
417 | throw "no storage ID specified"; | |
418 | } | |
4a580e60 | 419 | } |
1d8306e4 | 420 | const storage = me.storage; |
4a580e60 | 421 | |
e8b422bc FE |
422 | var content = me.content; |
423 | if (!content) { | |
424 | throw "no content type specified"; | |
425 | } | |
426 | ||
1d8306e4 TL |
427 | const baseurl = `/nodes/${nodename}/storage/${storage}/content`; |
428 | let store = me.store = Ext.create('Ext.data.Store', { | |
4a580e60 | 429 | model: 'pve-storage-content', |
4a580e60 | 430 | proxy: { |
56a353b9 | 431 | type: 'proxmox', |
e8b422bc FE |
432 | url: '/api2/json' + baseurl, |
433 | extraParams: { | |
434 | content: content, | |
435 | }, | |
4a580e60 | 436 | }, |
177de3de DC |
437 | sorters: { |
438 | property: 'volid', | |
1d8306e4 TL |
439 | order: 'DESC', |
440 | }, | |
4a580e60 DM |
441 | }); |
442 | ||
dbeddeb3 FE |
443 | if (!me.sm) { |
444 | me.sm = Ext.create('Ext.selection.RowModel', {}); | |
445 | } | |
1d8306e4 | 446 | let sm = me.sm; |
4a580e60 | 447 | |
1d8306e4 | 448 | let reload = () => store.load(); |
4a580e60 | 449 | |
e7ade592 | 450 | Proxmox.Utils.monStoreErrors(me, store); |
4a580e60 | 451 | |
9ce0c258 FE |
452 | if (!me.tbar) { |
453 | me.tbar = []; | |
454 | } | |
8798c35b | 455 | if (me.useUploadButton) { |
af3c0a92 LS |
456 | me.tbar.unshift( |
457 | { | |
458 | xtype: 'button', | |
459 | text: gettext('Upload'), | |
460 | disabled: !me.enableUploadButton, | |
461 | handler: function() { | |
462 | Ext.create('PVE.storage.Upload', { | |
463 | nodename: nodename, | |
464 | storage: storage, | |
465 | contents: [content], | |
466 | autoShow: true, | |
467 | taskDone: () => reload(), | |
468 | }); | |
469 | }, | |
470 | }, | |
471 | { | |
472 | xtype: 'button', | |
473 | text: gettext('Download from URL'), | |
474 | disabled: !me.enableDownloadUrlButton, | |
475 | handler: function() { | |
476 | Ext.create('PVE.storage.DownloadUrl', { | |
477 | nodename: nodename, | |
478 | storage: storage, | |
479 | content: content, | |
480 | autoShow: true, | |
481 | taskDone: () => reload(), | |
482 | }); | |
483 | }, | |
484 | }, | |
485 | '-', | |
486 | ); | |
8798c35b | 487 | } |
f5e17f15 | 488 | if (!me.useCustomRemoveButton) { |
af3c0a92 LS |
489 | me.tbar.push({ |
490 | xtype: 'proxmoxStdRemoveButton', | |
491 | selModel: sm, | |
492 | delay: 5, | |
493 | callback: () => reload(), | |
494 | baseurl: baseurl + '/', | |
495 | }); | |
f5e17f15 | 496 | } |
9ce0c258 | 497 | me.tbar.push( |
9ce0c258 | 498 | '->', |
1d8306e4 TL |
499 | gettext('Search') + ':', |
500 | ' ', | |
9ce0c258 FE |
501 | { |
502 | xtype: 'textfield', | |
503 | width: 200, | |
504 | enableKeyEvents: true, | |
c8a71b9e | 505 | emptyText: gettext('Name, Format'), |
9ce0c258 | 506 | listeners: { |
c8a71b9e TL |
507 | keyup: { |
508 | buffer: 500, | |
509 | fn: function(field) { | |
510 | store.clearFilter(true); | |
511 | store.filter([ | |
512 | { | |
513 | property: 'text', | |
514 | value: field.getValue(), | |
515 | anyMatch: true, | |
516 | caseSensitive: false, | |
517 | }, | |
518 | ]); | |
519 | }, | |
520 | }, | |
521 | change: function(field, newValue, oldValue) { | |
522 | if (newValue !== this.originalValue) { | |
523 | this.triggers.clear.setVisible(true); | |
524 | } | |
525 | }, | |
526 | }, | |
527 | triggers: { | |
528 | clear: { | |
529 | cls: 'pmx-clear-trigger', | |
530 | weight: -1, | |
531 | hidden: true, | |
532 | handler: function() { | |
533 | this.triggers.clear.setVisible(false); | |
534 | this.setValue(this.originalValue); | |
535 | store.clearFilter(); | |
536 | }, | |
537 | }, | |
538 | }, | |
539 | }, | |
9ce0c258 FE |
540 | ); |
541 | ||
6ecc7420 | 542 | let availableColumns = { |
7ef0f4c3 FE |
543 | 'name': { |
544 | header: gettext('Name'), | |
545 | flex: 2, | |
546 | sortable: true, | |
547 | renderer: PVE.Utils.render_storage_content, | |
1d8306e4 | 548 | dataIndex: 'text', |
7ef0f4c3 | 549 | }, |
ef402242 DC |
550 | 'notes': { |
551 | header: gettext('Notes'), | |
7ef0f4c3 FE |
552 | flex: 1, |
553 | renderer: Ext.htmlEncode, | |
ef402242 | 554 | dataIndex: 'notes', |
7ef0f4c3 FE |
555 | }, |
556 | 'date': { | |
557 | header: gettext('Date'), | |
558 | width: 150, | |
1d8306e4 | 559 | dataIndex: 'vdate', |
7ef0f4c3 FE |
560 | }, |
561 | 'format': { | |
562 | header: gettext('Format'), | |
563 | width: 100, | |
1d8306e4 | 564 | dataIndex: 'format', |
7ef0f4c3 FE |
565 | }, |
566 | 'size': { | |
567 | header: gettext('Size'), | |
568 | width: 100, | |
569 | renderer: Proxmox.Utils.format_size, | |
1d8306e4 | 570 | dataIndex: 'size', |
7ef0f4c3 FE |
571 | }, |
572 | }; | |
573 | ||
6ecc7420 TL |
574 | if (me.hideColumns) { |
575 | me.hideColumns.forEach(key => delete availableColumns[key]); | |
ebea3f45 TL |
576 | } |
577 | if (!me.hasNotesColumn) { | |
ef402242 | 578 | delete availableColumns.notes; |
7ef0f4c3 | 579 | } |
ebea3f45 TL |
580 | if (me.extraColumns && typeof me.extraColumns === 'object') { |
581 | Object.assign(availableColumns, me.extraColumns); | |
582 | } | |
6ecc7420 | 583 | const columns = Object.values(availableColumns); |
7ef0f4c3 | 584 | |
9ce0c258 FE |
585 | Ext.apply(me, { |
586 | store: store, | |
587 | selModel: sm, | |
588 | tbar: me.tbar, | |
7ef0f4c3 | 589 | columns: columns, |
4a580e60 | 590 | listeners: { |
1d8306e4 TL |
591 | activate: reload, |
592 | }, | |
4a580e60 DM |
593 | }); |
594 | ||
595 | me.callParent(); | |
1d8306e4 | 596 | }, |
4a580e60 | 597 | }, function() { |
4a580e60 DM |
598 | Ext.define('pve-storage-content', { |
599 | extend: 'Ext.data.Model', | |
177de3de DC |
600 | fields: [ |
601 | 'volid', 'content', 'format', 'size', 'used', 'vmid', | |
ef402242 | 602 | 'channel', 'id', 'lun', 'notes', 'verification', |
177de3de DC |
603 | { |
604 | name: 'text', | |
4a580e60 | 605 | convert: function(value, record) { |
86cc7049 DC |
606 | // check for volid, because if you click on a grouping header, |
607 | // it calls convert (but with an empty volid) | |
608 | if (value || record.data.volid === null) { | |
4a580e60 DM |
609 | return value; |
610 | } | |
611 | return PVE.Utils.render_storage_content(value, {}, record); | |
1d8306e4 | 612 | }, |
12d50fcd TL |
613 | }, |
614 | { | |
615 | name: 'vdate', | |
616 | convert: function(value, record) { | |
617 | // check for volid, because if you click on a grouping header, | |
618 | // it calls convert (but with an empty volid) | |
619 | if (value || record.data.volid === null) { | |
620 | return value; | |
621 | } | |
622 | let t = record.data.content; | |
623 | if (t === "backup") { | |
624 | let v = record.data.volid; | |
625 | let match = v.match(/(\d{4}_\d{2}_\d{2})-(\d{2}_\d{2}_\d{2})/); | |
626 | if (match) { | |
a4a86fe9 | 627 | let date = match[1].replace(/_/g, '-'); |
12d50fcd TL |
628 | let time = match[2].replace(/_/g, ':'); |
629 | return date + " " + time; | |
630 | } | |
631 | } | |
a4a86fe9 DM |
632 | if (record.data.ctime) { |
633 | let ctime = new Date(record.data.ctime * 1000); | |
1d8306e4 | 634 | return Ext.Date.format(ctime, 'Y-m-d H:i:s'); |
a4a86fe9 | 635 | } |
12d50fcd | 636 | return ''; |
1d8306e4 | 637 | }, |
12d50fcd | 638 | }, |
4a580e60 | 639 | ], |
1d8306e4 | 640 | idProperty: 'volid', |
4a580e60 | 641 | }); |
4a580e60 | 642 | }); |