]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/grid/FirewallRules.js
ui: ScheduleSimulator: split date and time into two columns
[pve-manager.git] / www / manager6 / grid / FirewallRules.js
1 Ext.define('PVE.form.FWMacroSelector', {
2 extend: 'Proxmox.form.ComboGrid',
3 alias: 'widget.pveFWMacroSelector',
4 allowBlank: true,
5 autoSelect: false,
6 valueField: 'macro',
7 displayField: 'macro',
8 listConfig: {
9 columns: [
10 {
11 header: gettext('Macro'),
12 dataIndex: 'macro',
13 hideable: false,
14 width: 100,
15 },
16 {
17 header: gettext('Description'),
18 renderer: Ext.String.htmlEncode,
19 flex: 1,
20 dataIndex: 'descr',
21 },
22 ],
23 },
24 initComponent: function() {
25 var me = this;
26
27 var store = Ext.create('Ext.data.Store', {
28 autoLoad: true,
29 fields: ['macro', 'descr'],
30 idProperty: 'macro',
31 proxy: {
32 type: 'proxmox',
33 url: "/api2/json/cluster/firewall/macros",
34 },
35 sorters: {
36 property: 'macro',
37 direction: 'ASC',
38 },
39 });
40
41 Ext.apply(me, {
42 store: store,
43 });
44
45 me.callParent();
46 },
47 });
48
49 Ext.define('PVE.form.ICMPTypeSelector', {
50 extend: 'Proxmox.form.ComboGrid',
51 alias: 'widget.pveICMPTypeSelector',
52 allowBlank: true,
53 autoSelect: false,
54 valueField: 'name',
55 displayField: 'name',
56 listConfig: {
57 columns: [
58 {
59 header: gettext('Type'),
60 dataIndex: 'type',
61 hideable: false,
62 sortable: false,
63 width: 50,
64 },
65 {
66 header: gettext('Name'),
67 dataIndex: 'name',
68 hideable: false,
69 sortable: false,
70 flex: 1,
71 },
72 ],
73 },
74 setName: function(value) {
75 this.name = value;
76 },
77 });
78
79 let ICMP_TYPE_NAMES_STORE = Ext.create('Ext.data.Store', {
80 field: ['type', 'name'],
81 data: [
82 { type: 'any', name: 'any' },
83 { type: '0', name: 'echo-reply' },
84 { type: '3', name: 'destination-unreachable' },
85 { type: '3/0', name: 'network-unreachable' },
86 { type: '3/1', name: 'host-unreachable' },
87 { type: '3/2', name: 'protocol-unreachable' },
88 { type: '3/3', name: 'port-unreachable' },
89 { type: '3/4', name: 'fragmentation-needed' },
90 { type: '3/5', name: 'source-route-failed' },
91 { type: '3/6', name: 'network-unknown' },
92 { type: '3/7', name: 'host-unknown' },
93 { type: '3/9', name: 'network-prohibited' },
94 { type: '3/10', name: 'host-prohibited' },
95 { type: '3/11', name: 'TOS-network-unreachable' },
96 { type: '3/12', name: 'TOS-host-unreachable' },
97 { type: '3/13', name: 'communication-prohibited' },
98 { type: '3/14', name: 'host-precedence-violation' },
99 { type: '3/15', name: 'precedence-cutoff' },
100 { type: '4', name: 'source-quench' },
101 { type: '5', name: 'redirect' },
102 { type: '5/0', name: 'network-redirect' },
103 { type: '5/1', name: 'host-redirect' },
104 { type: '5/2', name: 'TOS-network-redirect' },
105 { type: '5/3', name: 'TOS-host-redirect' },
106 { type: '8', name: 'echo-request' },
107 { type: '9', name: 'router-advertisement' },
108 { type: '10', name: 'router-solicitation' },
109 { type: '11', name: 'time-exceeded' },
110 { type: '11/0', name: 'ttl-zero-during-transit' },
111 { type: '11/1', name: 'ttl-zero-during-reassembly' },
112 { type: '12', name: 'parameter-problem' },
113 { type: '12/0', name: 'ip-header-bad' },
114 { type: '12/1', name: 'required-option-missing' },
115 { type: '13', name: 'timestamp-request' },
116 { type: '14', name: 'timestamp-reply' },
117 { type: '17', name: 'address-mask-request' },
118 { type: '18', name: 'address-mask-reply' },
119 ],
120 });
121 let ICMPV6_TYPE_NAMES_STORE = Ext.create('Ext.data.Store', {
122 field: ['type', 'name'],
123 data: [
124 { type: '1', name: 'destination-unreachable' },
125 { type: '1/0', name: 'no-route' },
126 { type: '1/1', name: 'communication-prohibited' },
127 { type: '1/2', name: 'beyond-scope' },
128 { type: '1/3', name: 'address-unreachable' },
129 { type: '1/4', name: 'port-unreachable' },
130 { type: '1/5', name: 'failed-policy' },
131 { type: '1/6', name: 'reject-route' },
132 { type: '2', name: 'packet-too-big' },
133 { type: '3', name: 'time-exceeded' },
134 { type: '3/0', name: 'ttl-zero-during-transit' },
135 { type: '3/1', name: 'ttl-zero-during-reassembly' },
136 { type: '4', name: 'parameter-problem' },
137 { type: '4/0', name: 'bad-header' },
138 { type: '4/1', name: 'unknown-header-type' },
139 { type: '4/2', name: 'unknown-option' },
140 { type: '128', name: 'echo-request' },
141 { type: '129', name: 'echo-reply' },
142 { type: '133', name: 'router-solicitation' },
143 { type: '134', name: 'router-advertisement' },
144 { type: '135', name: 'neighbour-solicitation' },
145 { type: '136', name: 'neighbour-advertisement' },
146 { type: '137', name: 'redirect' },
147 ],
148 });
149
150 Ext.define('PVE.FirewallRulePanel', {
151 extend: 'Proxmox.panel.InputPanel',
152
153 allow_iface: false,
154
155 list_refs_url: undefined,
156
157 onGetValues: function(values) {
158 var me = this;
159
160 // hack: editable ComboGrid returns nothing when empty, so we need to set ''
161 // Also, disabled text fields return nothing, so we need to set ''
162
163 Ext.Array.each(['source', 'dest', 'macro', 'proto', 'sport', 'dport', 'icmp-type', 'log'], function(key) {
164 if (values[key] === undefined) {
165 values[key] = '';
166 }
167 });
168
169 delete values.modified_marker;
170
171 return values;
172 },
173
174 initComponent: function() {
175 var me = this;
176
177 if (!me.list_refs_url) {
178 throw "no list_refs_url specified";
179 }
180
181 me.column1 = [
182 {
183 // hack: we use this field to mark the form 'dirty' when the
184 // record has errors- so that the user can safe the unmodified
185 // form again.
186 xtype: 'hiddenfield',
187 name: 'modified_marker',
188 value: '',
189 },
190 {
191 xtype: 'proxmoxKVComboBox',
192 name: 'type',
193 value: 'in',
194 comboItems: [['in', 'in'], ['out', 'out']],
195 fieldLabel: gettext('Direction'),
196 allowBlank: false,
197 },
198 {
199 xtype: 'proxmoxKVComboBox',
200 name: 'action',
201 value: 'ACCEPT',
202 comboItems: [['ACCEPT', 'ACCEPT'], ['DROP', 'DROP'], ['REJECT', 'REJECT']],
203 fieldLabel: gettext('Action'),
204 allowBlank: false,
205 },
206 ];
207
208 if (me.allow_iface) {
209 me.column1.push({
210 xtype: 'proxmoxtextfield',
211 name: 'iface',
212 deleteEmpty: !me.isCreate,
213 value: '',
214 fieldLabel: gettext('Interface'),
215 });
216 } else {
217 me.column1.push({
218 xtype: 'displayfield',
219 fieldLabel: '',
220 value: '',
221 });
222 }
223
224 me.column1.push(
225 {
226 xtype: 'displayfield',
227 fieldLabel: '',
228 height: 7,
229 value: '',
230 },
231 {
232 xtype: 'pveIPRefSelector',
233 name: 'source',
234 autoSelect: false,
235 editable: true,
236 base_url: me.list_refs_url,
237 value: '',
238 fieldLabel: gettext('Source'),
239 maxLength: 512,
240 maxLengthText: gettext('Too long, consider using IP sets.'),
241 },
242 {
243 xtype: 'pveIPRefSelector',
244 name: 'dest',
245 autoSelect: false,
246 editable: true,
247 base_url: me.list_refs_url,
248 value: '',
249 fieldLabel: gettext('Destination'),
250 maxLength: 512,
251 maxLengthText: gettext('Too long, consider using IP sets.'),
252 },
253 );
254
255
256 me.column2 = [
257 {
258 xtype: 'proxmoxcheckbox',
259 name: 'enable',
260 checked: false,
261 uncheckedValue: 0,
262 fieldLabel: gettext('Enable'),
263 },
264 {
265 xtype: 'pveFWMacroSelector',
266 name: 'macro',
267 fieldLabel: gettext('Macro'),
268 editable: true,
269 allowBlank: true,
270 listeners: {
271 change: function(f, value) {
272 if (value === null) {
273 me.down('field[name=proto]').setDisabled(false);
274 me.down('field[name=sport]').setDisabled(false);
275 me.down('field[name=dport]').setDisabled(false);
276 } else {
277 me.down('field[name=proto]').setDisabled(true);
278 me.down('field[name=proto]').setValue('');
279 me.down('field[name=sport]').setDisabled(true);
280 me.down('field[name=sport]').setValue('');
281 me.down('field[name=dport]').setDisabled(true);
282 me.down('field[name=dport]').setValue('');
283 }
284 },
285 },
286 },
287 {
288 xtype: 'pveIPProtocolSelector',
289 name: 'proto',
290 autoSelect: false,
291 editable: true,
292 value: '',
293 fieldLabel: gettext('Protocol'),
294 listeners: {
295 change: function(f, value) {
296 if (value === 'icmp' || value === 'icmpv6' || value === 'ipv6-icmp') {
297 me.down('field[name=dport]').setHidden(true);
298 me.down('field[name=dport]').setDisabled(true);
299 if (value === 'icmp') {
300 me.down('#icmpv4-type').setHidden(false);
301 me.down('#icmpv4-type').setDisabled(false);
302 me.down('#icmpv6-type').setHidden(true);
303 me.down('#icmpv6-type').setDisabled(true);
304 } else {
305 me.down('#icmpv6-type').setHidden(false);
306 me.down('#icmpv6-type').setDisabled(false);
307 me.down('#icmpv4-type').setHidden(true);
308 me.down('#icmpv4-type').setDisabled(true);
309 }
310 } else {
311 me.down('#icmpv4-type').setHidden(true);
312 me.down('#icmpv4-type').setDisabled(true);
313 me.down('#icmpv6-type').setHidden(true);
314 me.down('#icmpv6-type').setDisabled(true);
315 me.down('field[name=dport]').setHidden(false);
316 me.down('field[name=dport]').setDisabled(false);
317 }
318 },
319 },
320 },
321 {
322 xtype: 'displayfield',
323 fieldLabel: '',
324 height: 7,
325 value: '',
326 },
327 {
328 xtype: 'textfield',
329 name: 'sport',
330 value: '',
331 fieldLabel: gettext('Source port'),
332 },
333 {
334 xtype: 'textfield',
335 name: 'dport',
336 value: '',
337 fieldLabel: gettext('Dest. port'),
338 },
339 {
340 xtype: 'pveICMPTypeSelector',
341 name: 'icmp-type',
342 id: 'icmpv4-type',
343 autoSelect: false,
344 editable: true,
345 hidden: true,
346 disabled: true,
347 value: '',
348 fieldLabel: gettext('ICMP type'),
349 store: ICMP_TYPE_NAMES_STORE,
350 },
351 {
352 xtype: 'pveICMPTypeSelector',
353 name: 'icmp-type',
354 id: 'icmpv6-type',
355 autoSelect: false,
356 editable: true,
357 hidden: true,
358 disabled: true,
359 value: '',
360 fieldLabel: gettext('ICMP type'),
361 store: ICMPV6_TYPE_NAMES_STORE,
362 },
363 ];
364
365 me.advancedColumn1 = [
366 {
367 xtype: 'pveFirewallLogLevels',
368 },
369 ];
370
371 me.columnB = [
372 {
373 xtype: 'textfield',
374 name: 'comment',
375 value: '',
376 fieldLabel: gettext('Comment'),
377 },
378 ];
379
380 me.callParent();
381 },
382 });
383
384 Ext.define('PVE.FirewallRuleEdit', {
385 extend: 'Proxmox.window.Edit',
386
387 base_url: undefined,
388 list_refs_url: undefined,
389
390 allow_iface: false,
391
392 initComponent: function() {
393 var me = this;
394
395 if (!me.base_url) {
396 throw "no base_url specified";
397 }
398 if (!me.list_refs_url) {
399 throw "no list_refs_url specified";
400 }
401
402 me.isCreate = me.rule_pos === undefined;
403
404 if (me.isCreate) {
405 me.url = '/api2/extjs' + me.base_url;
406 me.method = 'POST';
407 } else {
408 me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString();
409 me.method = 'PUT';
410 }
411
412 var ipanel = Ext.create('PVE.FirewallRulePanel', {
413 isCreate: me.isCreate,
414 list_refs_url: me.list_refs_url,
415 allow_iface: me.allow_iface,
416 rule_pos: me.rule_pos,
417 });
418
419 Ext.apply(me, {
420 subject: gettext('Rule'),
421 isAdd: true,
422 items: [ipanel],
423 });
424
425 me.callParent();
426
427 if (!me.isCreate) {
428 me.load({
429 success: function(response, options) {
430 var values = response.result.data;
431 ipanel.setValues(values);
432 // set icmp-type again after protocol has been set
433 if (values["icmp-type"] !== undefined) {
434 ipanel.setValues({ "icmp-type": values["icmp-type"] });
435 }
436 if (values.errors) {
437 var field = me.query('[isFormField][name=modified_marker]')[0];
438 field.setValue(1);
439 Ext.Function.defer(function() {
440 var form = ipanel.up('form').getForm();
441 form.markInvalid(values.errors);
442 }, 100);
443 }
444 },
445 });
446 } else if (me.rec) {
447 ipanel.setValues(me.rec.data);
448 }
449 },
450 });
451
452 Ext.define('PVE.FirewallGroupRuleEdit', {
453 extend: 'Proxmox.window.Edit',
454
455 base_url: undefined,
456
457 allow_iface: false,
458
459 initComponent: function() {
460 var me = this;
461
462 me.isCreate = me.rule_pos === undefined;
463
464 if (me.isCreate) {
465 me.url = '/api2/extjs' + me.base_url;
466 me.method = 'POST';
467 } else {
468 me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString();
469 me.method = 'PUT';
470 }
471
472 var column1 = [
473 {
474 xtype: 'hiddenfield',
475 name: 'type',
476 value: 'group',
477 },
478 {
479 xtype: 'pveSecurityGroupsSelector',
480 name: 'action',
481 value: '',
482 fieldLabel: gettext('Security Group'),
483 allowBlank: false,
484 },
485 ];
486
487 if (me.allow_iface) {
488 column1.push({
489 xtype: 'proxmoxtextfield',
490 name: 'iface',
491 deleteEmpty: !me.isCreate,
492 value: '',
493 fieldLabel: gettext('Interface'),
494 });
495 }
496
497 var ipanel = Ext.create('Proxmox.panel.InputPanel', {
498 isCreate: me.isCreate,
499 column1: column1,
500 column2: [
501 {
502 xtype: 'proxmoxcheckbox',
503 name: 'enable',
504 checked: false,
505 uncheckedValue: 0,
506 fieldLabel: gettext('Enable'),
507 },
508 ],
509 columnB: [
510 {
511 xtype: 'textfield',
512 name: 'comment',
513 value: '',
514 fieldLabel: gettext('Comment'),
515 },
516 ],
517 });
518
519 Ext.apply(me, {
520 subject: gettext('Rule'),
521 isAdd: true,
522 items: [ipanel],
523 });
524
525 me.callParent();
526
527 if (!me.isCreate) {
528 me.load({
529 success: function(response, options) {
530 var values = response.result.data;
531 ipanel.setValues(values);
532 },
533 });
534 }
535 },
536 });
537
538 Ext.define('PVE.FirewallRules', {
539 extend: 'Ext.grid.Panel',
540 alias: 'widget.pveFirewallRules',
541
542 onlineHelp: 'chapter_pve_firewall',
543
544 stateful: true,
545 stateId: 'grid-firewall-rules',
546
547 base_url: undefined,
548 list_refs_url: undefined,
549
550 addBtn: undefined,
551 removeBtn: undefined,
552 editBtn: undefined,
553 groupBtn: undefined,
554
555 tbar_prefix: undefined,
556
557 allow_groups: true,
558 allow_iface: false,
559
560 setBaseUrl: function(url) {
561 var me = this;
562
563 me.base_url = url;
564
565 if (url === undefined) {
566 me.addBtn.setDisabled(true);
567 if (me.groupBtn) {
568 me.groupBtn.setDisabled(true);
569 }
570 me.store.removeAll();
571 } else {
572 me.addBtn.setDisabled(false);
573 me.removeBtn.baseurl = url + '/';
574 if (me.groupBtn) {
575 me.groupBtn.setDisabled(false);
576 }
577 me.store.setProxy({
578 type: 'proxmox',
579 url: '/api2/json' + url,
580 });
581
582 me.store.load();
583 }
584 },
585
586 moveRule: function(from, to) {
587 var me = this;
588
589 if (!me.base_url) {
590 return;
591 }
592
593 Proxmox.Utils.API2Request({
594 url: me.base_url + "/" + from,
595 method: 'PUT',
596 params: { moveto: to },
597 waitMsgTarget: me,
598 failure: function(response, options) {
599 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
600 },
601 callback: function() {
602 me.store.load();
603 },
604 });
605 },
606
607 updateRule: function(rule) {
608 var me = this;
609
610 if (!me.base_url) {
611 return;
612 }
613
614 rule.enable = rule.enable ? 1 : 0;
615
616 var pos = rule.pos;
617 delete rule.pos;
618 delete rule.errors;
619
620 Proxmox.Utils.API2Request({
621 url: me.base_url + '/' + pos.toString(),
622 method: 'PUT',
623 params: rule,
624 waitMsgTarget: me,
625 failure: function(response, options) {
626 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
627 },
628 callback: function() {
629 me.store.load();
630 },
631 });
632 },
633
634
635 initComponent: function() {
636 var me = this;
637
638 if (!me.list_refs_url) {
639 throw "no list_refs_url specified";
640 }
641
642 var store = Ext.create('Ext.data.Store', {
643 model: 'pve-fw-rule',
644 });
645
646 var reload = function() {
647 store.load();
648 };
649
650 var sm = Ext.create('Ext.selection.RowModel', {});
651
652 var run_editor = function() {
653 var rec = sm.getSelection()[0];
654 if (!rec) {
655 return;
656 }
657 var type = rec.data.type;
658
659 var editor;
660 if (type === 'in' || type === 'out') {
661 editor = 'PVE.FirewallRuleEdit';
662 } else if (type === 'group') {
663 editor = 'PVE.FirewallGroupRuleEdit';
664 } else {
665 return;
666 }
667
668 var win = Ext.create(editor, {
669 digest: rec.data.digest,
670 allow_iface: me.allow_iface,
671 base_url: me.base_url,
672 list_refs_url: me.list_refs_url,
673 rule_pos: rec.data.pos,
674 });
675
676 win.show();
677 win.on('destroy', reload);
678 };
679
680 me.editBtn = Ext.create('Proxmox.button.Button', {
681 text: gettext('Edit'),
682 disabled: true,
683 selModel: sm,
684 handler: run_editor,
685 });
686
687 me.addBtn = Ext.create('Ext.Button', {
688 text: gettext('Add'),
689 disabled: true,
690 handler: function() {
691 var win = Ext.create('PVE.FirewallRuleEdit', {
692 allow_iface: me.allow_iface,
693 base_url: me.base_url,
694 list_refs_url: me.list_refs_url,
695 });
696 win.on('destroy', reload);
697 win.show();
698 },
699 });
700
701 var run_copy_editor = function() {
702 let rec = sm.getSelection()[0];
703 if (!rec) {
704 return;
705 }
706 let type = rec.data.type;
707 if (!(type === 'in' || type === 'out')) {
708 return;
709 }
710
711 let win = Ext.create('PVE.FirewallRuleEdit', {
712 allow_iface: me.allow_iface,
713 base_url: me.base_url,
714 list_refs_url: me.list_refs_url,
715 rec: rec,
716 });
717 win.show();
718 win.on('destroy', reload);
719 };
720
721 me.copyBtn = Ext.create('Proxmox.button.Button', {
722 text: gettext('Copy'),
723 selModel: sm,
724 enableFn: ({ data }) => data.type === 'in' || data.type === 'out',
725 disabled: true,
726 handler: run_copy_editor,
727 });
728
729 if (me.allow_groups) {
730 me.groupBtn = Ext.create('Ext.Button', {
731 text: gettext('Insert') + ': ' +
732 gettext('Security Group'),
733 disabled: true,
734 handler: function() {
735 var win = Ext.create('PVE.FirewallGroupRuleEdit', {
736 allow_iface: me.allow_iface,
737 base_url: me.base_url,
738 });
739 win.on('destroy', reload);
740 win.show();
741 },
742 });
743 }
744
745 me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
746 selModel: sm,
747 baseurl: me.base_url + '/',
748 confirmMsg: false,
749 getRecordName: function(rec) {
750 var rule = rec.data;
751 return rule.pos.toString() +
752 '?digest=' + encodeURIComponent(rule.digest);
753 },
754 callback: function() {
755 me.store.load();
756 },
757 });
758
759 let tbar = me.tbar_prefix ? [me.tbar_prefix] : [];
760 tbar.push(me.addBtn, me.copyBtn);
761 if (me.groupBtn) {
762 tbar.push(me.groupBtn);
763 }
764 tbar.push(me.removeBtn, me.editBtn);
765
766 let render_errors = function(name, value, metaData, record) {
767 let errors = record.data.errors;
768 if (errors && errors[name]) {
769 metaData.tdCls = 'proxmox-invalid-row';
770 let html = '<p>' + Ext.htmlEncode(errors[name]) + '</p>';
771 metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + html + '"';
772 }
773 return value;
774 };
775
776 let columns = [
777 {
778 // similar to xtype: 'rownumberer',
779 dataIndex: 'pos',
780 resizable: false,
781 minWidth: 65,
782 maxWidth: 83,
783 flex: 1,
784 sortable: false,
785 hideable: false,
786 menuDisabled: true,
787 renderer: function(value, metaData, record, rowIdx, colIdx) {
788 metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special';
789 let dragHandle = "<i class='pve-grid-fa fa fa-fw fa-reorder cursor-move'></i>";
790 if (value >= 0) {
791 return dragHandle + value;
792 }
793 return dragHandle;
794 },
795 },
796 {
797 xtype: 'checkcolumn',
798 header: gettext('On'),
799 dataIndex: 'enable',
800 listeners: {
801 checkchange: function(column, recordIndex, checked) {
802 var record = me.getStore().getData().items[recordIndex];
803 record.commit();
804 var data = {};
805 Ext.Array.forEach(record.getFields(), function(field) {
806 data[field.name] = record.get(field.name);
807 });
808 if (!me.allow_iface || !data.iface) {
809 delete data.iface;
810 }
811 me.updateRule(data);
812 },
813 },
814 width: 40,
815 },
816 {
817 header: gettext('Type'),
818 dataIndex: 'type',
819 renderer: function(value, metaData, record) {
820 return render_errors('type', value, metaData, record);
821 },
822 minWidth: 60,
823 maxWidth: 80,
824 flex: 2,
825 },
826 {
827 header: gettext('Action'),
828 dataIndex: 'action',
829 renderer: function(value, metaData, record) {
830 return render_errors('action', value, metaData, record);
831 },
832 minWidth: 80,
833 maxWidth: 200,
834 flex: 2,
835 },
836 {
837 header: gettext('Macro'),
838 dataIndex: 'macro',
839 renderer: function(value, metaData, record) {
840 return render_errors('macro', value, metaData, record);
841 },
842 minWidth: 80,
843 flex: 2,
844 },
845 ];
846
847 if (me.allow_iface) {
848 columns.push({
849 header: gettext('Interface'),
850 dataIndex: 'iface',
851 renderer: function(value, metaData, record) {
852 return render_errors('iface', value, metaData, record);
853 },
854 minWidth: 80,
855 flex: 2,
856 });
857 }
858
859 columns.push(
860 {
861 header: gettext('Protocol'),
862 dataIndex: 'proto',
863 renderer: function(value, metaData, record) {
864 return render_errors('proto', value, metaData, record);
865 },
866 width: 75,
867 },
868 {
869 header: gettext('Source'),
870 dataIndex: 'source',
871 renderer: function(value, metaData, record) {
872 return render_errors('source', value, metaData, record);
873 },
874 minWidth: 100,
875 flex: 2,
876 },
877 {
878 header: gettext('S.Port'),
879 dataIndex: 'sport',
880 renderer: function(value, metaData, record) {
881 return render_errors('sport', value, metaData, record);
882 },
883 width: 75,
884 },
885 {
886 header: gettext('Destination'),
887 dataIndex: 'dest',
888 renderer: function(value, metaData, record) {
889 return render_errors('dest', value, metaData, record);
890 },
891 minWidth: 100,
892 flex: 2,
893 },
894 {
895 header: gettext('D.Port'),
896 dataIndex: 'dport',
897 renderer: function(value, metaData, record) {
898 return render_errors('dport', value, metaData, record);
899 },
900 width: 75,
901 },
902 {
903 header: gettext('Log level'),
904 dataIndex: 'log',
905 renderer: function(value, metaData, record) {
906 return render_errors('log', value, metaData, record);
907 },
908 width: 100,
909 },
910 {
911 header: gettext('Comment'),
912 dataIndex: 'comment',
913 flex: 10,
914 minWidth: 75,
915 renderer: function(value, metaData, record) {
916 let comment = render_errors('comment', Ext.util.Format.htmlEncode(value), metaData, record) || '';
917 if (comment.length * 12 > metaData.column.cellWidth) {
918 comment = `<span data-qtip="${comment}">${comment}</span>`;
919 }
920 return comment;
921 },
922 },
923 );
924
925 Ext.apply(me, {
926 store: store,
927 selModel: sm,
928 tbar: tbar,
929 viewConfig: {
930 plugins: [
931 {
932 ptype: 'gridviewdragdrop',
933 dragGroup: 'FWRuleDDGroup',
934 dropGroup: 'FWRuleDDGroup',
935 },
936 ],
937 listeners: {
938 beforedrop: function(node, data, dropRec, dropPosition) {
939 if (!dropRec) {
940 return false; // empty view
941 }
942 let moveto = dropRec.get('pos');
943 if (dropPosition === 'after') {
944 moveto++;
945 }
946 let pos = data.records[0].get('pos');
947 me.moveRule(pos, moveto);
948 return 0;
949 },
950 itemdblclick: run_editor,
951 },
952 },
953 sortableColumns: false,
954 columns: columns,
955 });
956
957 me.callParent();
958
959 if (me.base_url) {
960 me.setBaseUrl(me.base_url); // load
961 }
962 },
963 }, function() {
964 Ext.define('pve-fw-rule', {
965 extend: 'Ext.data.Model',
966 fields: [
967 { name: 'enable', type: 'boolean' },
968 'type',
969 'action',
970 'macro',
971 'source',
972 'dest',
973 'proto',
974 'iface',
975 'dport',
976 'sport',
977 'comment',
978 'pos',
979 'digest',
980 'errors',
981 ],
982 idProperty: 'pos',
983 });
984 });