diff --git a/www/manager/dc/SecurityGroups.js b/www/manager/dc/SecurityGroups.js
new file mode 100644
index 00000000..e2bf6e35
--- /dev/null
+++ b/www/manager/dc/SecurityGroups.js
@@ -0,0 +1,230 @@
+Ext.define('PVE.SecurityGroupEdit', {
+ extend: 'PVE.window.Edit',
+
+ base_url: "/cluster/firewall/groups",
+
+ allow_iface: false,
+
+ initComponent : function() {
+ /*jslint confusion: true */
+ var me = this;
+
+ me.create = (me.group_name === undefined);
+
+ var subject;
+
+ me.url = '/api2/extjs' + me.base_url;
+ me.method = 'POST';
+
+ var items = [
+ {
+ xtype: 'textfield',
+ name: 'group',
+ value: me.group_name || '',
+ fieldLabel: gettext('Name'),
+ allowBlank: false
+ },
+ {
+ xtype: 'textfield',
+ name: 'comment',
+ value: me.group_comment || '',
+ fieldLabel: gettext('Comment')
+ }
+ ];
+
+ if (me.create) {
+ subject = gettext('Security Group');
+ } else {
+ subject = gettext('Security Group') + " '" + me.group_name + "'";
+ items.push({
+ xtype: 'hiddenfield',
+ name: 'rename',
+ value: me.group_name
+ });
+ }
+
+ var ipanel = Ext.create('PVE.panel.InputPanel', {
+ create: me.create,
+ items: items
+ });
+
+
+ Ext.apply(me, {
+ subject: subject,
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.SecurityGroupList', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveSecurityGroupList',
+
+ rule_panel: undefined,
+
+ addBtn: undefined,
+ removeBtn: undefined,
+ editBtn: undefined,
+
+ base_url: "/cluster/firewall/groups",
+
+ initComponent: function() {
+ /*jslint confusion: true */
+ var me = this;
+
+ if (me.rule_panel == undefined) {
+ throw "no rule panel specified";
+ }
+
+ if (me.base_url == undefined) {
+ throw "no base_url specified";
+ }
+
+ var store = new Ext.data.Store({
+ fields: [ 'group', 'comment', 'digest' ],
+ proxy: {
+ type: 'pve',
+ url: '/api2/json' + me.base_url
+ },
+ idProperty: 'group',
+ sorters: {
+ property: 'group',
+ order: 'DESC'
+ }
+ });
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var reload = function() {
+ var oldrec = sm.getSelection()[0];
+ store.load(function(records, operation, success) {
+ if (oldrec) {
+ var rec = store.findRecord('group', oldrec.data.group);
+ if (rec) {
+ sm.select(rec);
+ }
+ }
+ });
+ };
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ var win = Ext.create('PVE.SecurityGroupEdit', {
+ digest: rec.data.digest,
+ group_name: rec.data.group,
+ group_comment: rec.data.comment
+ });
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ me.editBtn = new PVE.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor
+ });
+
+ me.addBtn = new PVE.button.Button({
+ text: gettext('Create'),
+ handler: function() {
+ sm.deselectAll();
+ var win = Ext.create('PVE.SecurityGroupEdit', {});
+ win.show();
+ win.on('destroy', reload);
+ }
+ });
+
+ me.removeBtn = new PVE.button.Button({
+ text: gettext('Remove'),
+ selModel: sm,
+ disabled: true,
+ handler: function() {
+ var rec = sm.getSelection()[0];
+ if (!rec || !me.base_url) {
+ return;
+ }
+ PVE.Utils.API2Request({
+ url: me.base_url + '/' + rec.data.group +
+ '?digest=' + encodeURIComponent(rec.data.digest),
+ method: 'DELETE',
+ waitMsgTarget: me,
+ failure: function(response, options) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ callback: reload
+ });
+ }
+ });
+
+ Ext.apply(me, {
+ store: store,
+ tbar: [ '' + gettext('Group') + ':', me.addBtn, me.removeBtn, me.editBtn ],
+ selModel: sm,
+ columns: [
+ { header: gettext('Group'), dataIndex: 'group', width: 100 },
+ { header: gettext('Comment'), dataIndex: 'comment', flex: 1 }
+ ],
+ listeners: {
+ itemdblclick: run_editor,
+ select: function(sm, rec) {
+ var url = '/cluster/firewall/groups/' + rec.data.group;
+ me.rule_panel.setBaseUrl(url);
+ },
+ deselect: function() {
+ me.rule_panel.setBaseUrl(undefined);
+ },
+ show: reload
+ }
+ });
+
+ me.callParent();
+
+ store.load();
+ }
+});
+
+Ext.define('PVE.SecurityGroups', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveSecurityGroups',
+
+ title: 'Security Groups',
+
+ initComponent: function() {
+ var me = this;
+
+ var rule_panel = Ext.createWidget('pveFirewallRules', {
+ region: 'center',
+ allow_groups: false,
+ tbar_prefix: '' + gettext('Rules') + ':',
+ flex: 0.75,
+ border: false
+ });
+
+ var sglist = Ext.createWidget('pveSecurityGroupList', {
+ region: 'west',
+ rule_panel: rule_panel,
+ flex: 0.25,
+ border: false,
+ split: true
+ });
+
+
+ Ext.apply(me, {
+ layout: 'border',
+ items: [ sglist, rule_panel ],
+ listeners: {
+ show: function() {
+ sglist.fireEvent('show', sglist);
+ }
+ }
+ });
+
+ me.callParent();
+ }
+});
diff --git a/www/manager/grid/FirewallAliases.js b/www/manager/grid/FirewallAliases.js
new file mode 100644
index 00000000..a307f1d6
--- /dev/null
+++ b/www/manager/grid/FirewallAliases.js
@@ -0,0 +1,185 @@
+Ext.define('PVE.FirewallAliasEdit', {
+ extend: 'PVE.window.Edit',
+
+ base_url: undefined,
+
+ alias_name: undefined,
+
+ initComponent : function() {
+ /*jslint confusion: true */
+ var me = this;
+
+ me.create = (me.alias_name === undefined);
+
+ if (me.create) {
+ me.url = '/api2/extjs' + me.base_url;
+ me.method = 'POST';
+ } else {
+ me.url = '/api2/extjs' + me.base_url + '/' + me.alias_name;
+ me.method = 'PUT';
+ }
+
+ var items = [
+ {
+ xtype: 'textfield',
+ name: me.create ? 'name' : 'rename',
+ fieldLabel: gettext('Name'),
+ allowBlank: false
+ },
+ {
+ xtype: 'textfield',
+ name: 'cidr',
+ fieldLabel: gettext('IP/CIDR'),
+ allowBlank: false
+ },
+ {
+ xtype: 'textfield',
+ name: 'comment',
+ fieldLabel: gettext('Comment')
+ }
+ ];
+
+ var ipanel = Ext.create('PVE.panel.InputPanel', {
+ create: me.create,
+ items: items
+ });
+
+ Ext.apply(me, {
+ subject: gettext('Alias'),
+ isAdd: true,
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+
+ if (!me.create) {
+ me.load({
+ success: function(response, options) {
+ var values = response.result.data;
+ values.rename = values.name;
+ ipanel.setValues(values);
+ }
+ });
+ }
+ }
+});
+
+Ext.define('PVE.FirewallAliases', {
+ extend: 'Ext.grid.Panel',
+ alias: ['widget.pveFirewallAliases'],
+
+ base_url: undefined,
+
+ title: gettext('Aliases'),
+
+ initComponent : function() {
+ /*jslint confusion: true */
+
+ var me = this;
+
+ if (!me.base_url) {
+ throw "missing base_url configuration";
+ }
+
+ var store = new Ext.data.Store({
+ fields: [ 'name', 'cidr', 'comment', 'digest' ],
+ proxy: {
+ type: 'pve',
+ url: "/api2/json" + me.base_url
+ },
+ idProperty: 'name',
+ sorters: {
+ property: 'name',
+ order: 'DESC'
+ }
+ });
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var reload = function() {
+ var oldrec = sm.getSelection()[0];
+ store.load(function(records, operation, success) {
+ if (oldrec) {
+ var rec = store.findRecord('name', oldrec.data.name);
+ if (rec) {
+ sm.select(rec);
+ }
+ }
+ });
+ };
+
+ var run_editor = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('PVE.FirewallAliasEdit', {
+ base_url: me.base_url,
+ alias_name: rec.data.name
+ });
+
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ me.editBtn = new PVE.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor
+ });
+
+ me.addBtn = Ext.create('Ext.Button', {
+ text: gettext('Add'),
+ handler: function() {
+ var win = Ext.create('PVE.FirewallAliasEdit', {
+ base_url: me.base_url
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ });
+
+ me.removeBtn = new PVE.button.Button({
+ text: gettext('Remove'),
+ selModel: sm,
+ disabled: true,
+ handler: function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ PVE.Utils.API2Request({
+ url: me.base_url + '/' + rec.data.name,
+ method: 'DELETE',
+ waitMsgTarget: me,
+ failure: function(response, options) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ callback: reload
+ });
+ }
+ });
+
+
+ Ext.applyIf(me, {
+ store: store,
+ tbar: [ me.addBtn, me.removeBtn, me.editBtn ],
+ selModel: sm,
+ columns: [
+ { header: gettext('Name'), dataIndex: 'name', width: 100 },
+ { header: gettext('IP/CIDR'), dataIndex: 'cidr', width: 100 },
+ { header: gettext('Comment'), dataIndex: 'comment', flex: 1 }
+ ],
+ listeners: {
+ itemdblclick: run_editor
+ }
+ });
+
+ me.callParent();
+
+ me.on('show', reload);
+ }
+});
diff --git a/www/manager/grid/FirewallOptions.js b/www/manager/grid/FirewallOptions.js
new file mode 100644
index 00000000..a3c117cb
--- /dev/null
+++ b/www/manager/grid/FirewallOptions.js
@@ -0,0 +1,227 @@
+Ext.define('PVE.FirewallOptions', {
+ extend: 'PVE.grid.ObjectGrid',
+ alias: ['widget.pveFirewallOptions'],
+
+ fwtype: undefined, // 'dc', 'node' or 'vm'
+
+ base_url: undefined,
+
+ initComponent : function() {
+ /*jslint confusion: true */
+
+ var me = this;
+
+ if (!me.base_url) {
+ throw "missing base_url configuration";
+ }
+
+ if (me.fwtype === 'dc' || me.fwtype === 'node' || me.fwtype === 'vm') {
+ if (me.fwtype === 'node') {
+ me.cwidth1 = 250;
+ }
+ } else {
+ throw "unknown firewall option type";
+ }
+
+ var rows = {};
+
+ var add_boolean_row = function(name, text, labelWidth) {
+ rows[name] = {
+ header: text,
+ required: true,
+ defaultValue: 0,
+ renderer: PVE.Utils.format_boolean,
+ editor: {
+ xtype: 'pveWindowEdit',
+ subject: text,
+ fieldDefaults: { labelWidth: labelWidth || 100 },
+ items: {
+ xtype: 'pvecheckbox',
+ name: name,
+ uncheckedValue: 0,
+ fieldLabel: text
+ }
+ }
+ };
+ };
+
+ var add_integer_row = function(name, text, labelWidth, minValue) {
+ rows[name] = {
+ header: text,
+ required: true,
+ renderer: function(value) {
+ return value || PVE.Utils.defaultText;
+ },
+ editor: {
+ xtype: 'pveWindowEdit',
+ subject: text,
+ fieldDefaults: { labelWidth: labelWidth || 100 },
+ items: {
+ xtype: 'numberfield',
+ name: name,
+ minValue: minValue,
+ decimalPrecision: 0,
+ fieldLabel: text,
+ emptyText: gettext('Default'),
+ getSubmitData: function() {
+ var me = this;
+ var val = me.getSubmitValue();
+ if (val !== null && val !== '') {
+ var data = {};
+ data[name] = val;
+ return data;
+ } else {
+ return { 'delete' : name };
+ }
+ }
+ }
+ }
+ };
+ };
+
+ var add_log_row = function(name, labelWidth) {
+ rows[name] = {
+ header: name,
+ required: true,
+ defaultValue: 'nolog',
+ editor: {
+ xtype: 'pveWindowEdit',
+ subject: name,
+ fieldDefaults: { labelWidth: labelWidth || 100 },
+ items: {
+ xtype: 'pveKVComboBox',
+ name: name,
+ fieldLabel: name,
+ data: [['nolog', 'nolog'], ['info', 'info'], ['err', 'err'],
+ ['warning', 'warning'], ['crit', 'crit'], ['alert', 'alert'],
+ ['emerg', 'emerg'], ['debug', 'debug']]
+ }
+ }
+ };
+ };
+
+ add_boolean_row('enable', gettext('Enable Firewall'));
+
+ if (me.fwtype === 'node') {
+ add_boolean_row('nosmurfs', gettext('SMURFS filter'));
+ add_boolean_row('tcpflags', gettext('TCP flags filter'));
+ add_boolean_row('allow_bridge_route', gettext('Allow bridge route'), 150);
+ add_integer_row('nf_conntrack_max', 'nf_conntrack_max', 120, 32768);
+ add_integer_row('nf_conntrack_tcp_timeout_established',
+ 'nf_conntrack_tcp_timeout_established', 250, 7875);
+ add_log_row('log_level_in');
+ add_log_row('log_level_out');
+ add_log_row('tcp_flags_log_level', 120);
+ add_log_row('smurf_log_level');
+ } else if (me.fwtype === 'vm') {
+ add_boolean_row('dhcp', gettext('Enable DHCP'));
+ add_boolean_row('macfilter', gettext('MAC filter'));
+ add_log_row('log_level_in');
+ add_log_row('log_level_out');
+ }
+
+ if (me.fwtype === 'dc' || me.fwtype === 'vm') {
+ rows.policy_in = {
+ header: gettext('Input Policy'),
+ required: true,
+ defaultValue: 'DROP',
+ editor: {
+ xtype: 'pveWindowEdit',
+ subject: gettext('Input Policy'),
+ items: {
+ xtype: 'pveFirewallPolicySelector',
+ name: 'policy_in',
+ value: 'DROP',
+ fieldLabel: gettext('Input Policy')
+ }
+ }
+ };
+
+ rows.policy_out = {
+ header: gettext('Output Policy'),
+ required: true,
+ defaultValue: 'ACCEPT',
+ editor: {
+ xtype: 'pveWindowEdit',
+ subject: gettext('Output Policy'),
+ items: {
+ xtype: 'pveFirewallPolicySelector',
+ name: 'policy_out',
+ value: 'ACCEPT',
+ fieldLabel: gettext('Output Policy')
+ }
+ }
+ };
+ }
+
+ var reload = function() {
+ me.rstore.load();
+ };
+
+ var run_editor = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var rowdef = rows[rec.data.key];
+ if (!rowdef.editor) {
+ return;
+ }
+
+ var win;
+ if (Ext.isString(rowdef.editor)) {
+ win = Ext.create(rowdef.editor, {
+ pveSelNode: me.pveSelNode,
+ confid: rec.data.key,
+ url: '/api2/extjs' + me.base_url
+ });
+ } else {
+ var config = Ext.apply({
+ pveSelNode: me.pveSelNode,
+ confid: rec.data.key,
+ url: '/api2/extjs' + me.base_url
+ }, rowdef.editor);
+ win = Ext.createWidget(rowdef.editor.xtype, config);
+ win.load();
+ }
+
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ var edit_btn = new Ext.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ handler: run_editor
+ });
+
+ var set_button_status = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+
+ if (!rec) {
+ edit_btn.disable();
+ return;
+ }
+ var rowdef = rows[rec.data.key];
+ edit_btn.setDisabled(!rowdef.editor);
+ };
+
+ Ext.applyIf(me, {
+ url: "/api2/json" + me.base_url,
+ cwidth1: 150,
+ tbar: [ edit_btn ],
+ rows: rows,
+ listeners: {
+ itemdblclick: run_editor,
+ selectionchange: set_button_status
+ }
+ });
+
+ me.callParent();
+
+ me.on('show', reload);
+ }
+});
diff --git a/www/manager/grid/FirewallRules.js b/www/manager/grid/FirewallRules.js
new file mode 100644
index 00000000..281415b5
--- /dev/null
+++ b/www/manager/grid/FirewallRules.js
@@ -0,0 +1,699 @@
+Ext.define('PVE.form.FWMacroSelector', {
+ extend: 'PVE.form.ComboGrid',
+ alias: 'widget.pveFWMacroSelector',
+
+ initComponent: function() {
+ var me = this;
+
+ var store = Ext.create('Ext.data.Store', {
+ autoLoad: true,
+ fields: [ 'macro', 'descr' ],
+ idProperty: 'macro',
+ proxy: {
+ type: 'pve',
+ url: "/api2/json/cluster/firewall/macros"
+ },
+ sorters: {
+ property: 'macro',
+ order: 'DESC'
+ }
+ });
+
+ Ext.apply(me, {
+ store: store,
+ allowBlank: true,
+ autoSelect: false,
+ valueField: 'macro',
+ displayField: 'macro',
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Macro'),
+ dataIndex: 'macro',
+ hideable: false,
+ width: 100
+ },
+ {
+ header: gettext('Description'),
+ flex: 1,
+ dataIndex: 'descr'
+ }
+ ]
+ }
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.FirewallRulePanel', {
+ extend: 'PVE.panel.InputPanel',
+
+ allow_iface: false,
+
+ initComponent : function() {
+ var me = this;
+
+ me.column1 = [
+ {
+ xtype: 'pveKVComboBox',
+ name: 'type',
+ value: 'in',
+ data: [['in', 'in'], ['out', 'out']],
+ fieldLabel: gettext('Direction'),
+ allowBlank: false
+ },
+ {
+ xtype: 'pveKVComboBox',
+ name: 'action',
+ value: 'ACCEPT',
+ data: [['ACCEPT', 'ACCEPT'], ['DROP', 'DROP'], ['REJECT', 'REJECT']],
+ fieldLabel: gettext('Action'),
+ allowBlank: false
+ },
+ {
+ xtype: 'pveKVComboBox',
+ name: 'proto',
+ value: '',
+ deleteEmpty: !me.create,
+ emptyText: 'any',
+ editable: true,
+ data: [['tcp', 'TCP'], ['udp', 'UDP'], ['icmp', 'ICMP']],
+ fieldLabel: gettext('Protocol'),
+ allowBlank: true
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: '',
+ height: 7,
+ value: ''
+ },
+ {
+ xtype: 'pveIPSetSelector',
+ name: 'source',
+ autoSelect: false,
+ editable: true,
+ queryDelay: 900000000, // disable query
+ value: '',
+ fieldLabel: gettext('Source')
+ },
+ {
+ xtype: 'pveIPSetSelector',
+ name: 'dest',
+ autoSelect: false,
+ queryDelay: 900000000, // disable query
+ editable: true,
+ value: '',
+ fieldLabel: gettext('Destination')
+ }
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'pvecheckbox',
+ name: 'enable',
+ checked: false,
+ height: 22, // hack: set same height as text fields
+ uncheckedValue: 0,
+ fieldLabel: gettext('Enable')
+ },
+ {
+ xtype: 'pveFWMacroSelector',
+ name: 'macro',
+ value: '',
+ deleteEmpty: !me.create,
+ fieldLabel: gettext('Macro'),
+ allowBlank: true
+ }
+ ];
+
+ if (me.allow_iface) {
+ me.column2.push({
+ xtype: 'pvetextfield',
+ name: 'iface',
+ deleteEmpty: !me.create,
+ value: '',
+ fieldLabel: gettext('Interface')
+ });
+ } else {
+ me.column2.push({
+ xtype: 'displayfield',
+ fieldLabel: '',
+ height: 22, // hack: set same height as text fields
+ value: ''
+ });
+ }
+
+ me.column2.push([
+ {
+ xtype: 'displayfield',
+ fieldLabel: '',
+ height: 7,
+ value: ''
+ },
+ {
+ xtype: 'textfield',
+ name: 'sport',
+ value: '',
+ fieldLabel: gettext('Source port')
+ },
+ {
+ xtype: 'textfield',
+ name: 'dport',
+ height: 22, // hack: set same height as text fields
+ value: '',
+ fieldLabel: gettext('Dest. port')
+ }
+ ]);
+
+ me.columnB = [
+ {
+ xtype: 'textfield',
+ name: 'comment',
+ value: '',
+ fieldLabel: gettext('Comment')
+ }
+ ];
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.FirewallRuleEdit', {
+ extend: 'PVE.window.Edit',
+
+ base_url: undefined,
+
+ allow_iface: false,
+
+ initComponent : function() {
+ /*jslint confusion: true */
+ var me = this;
+
+ me.create = (me.rule_pos === undefined);
+
+ if (me.create) {
+ me.url = '/api2/extjs' + me.base_url;
+ me.method = 'POST';
+ } else {
+ me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString();
+ me.method = 'PUT';
+ }
+
+ var ipanel = Ext.create('PVE.FirewallRulePanel', {
+ create: me.create,
+ allow_iface: me.allow_iface,
+ rule_pos: me.rule_pos
+ });
+
+ Ext.apply(me, {
+ subject: gettext('Rule'),
+ isAdd: true,
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+
+ if (!me.create) {
+ me.load({
+ success: function(response, options) {
+ var values = response.result.data;
+ ipanel.setValues(values);
+ }
+ });
+ }
+ }
+});
+
+Ext.define('PVE.FirewallGroupRuleEdit', {
+ extend: 'PVE.window.Edit',
+
+ base_url: undefined,
+
+ allow_iface: false,
+
+ initComponent : function() {
+ /*jslint confusion: true */
+ var me = this;
+
+ me.create = (me.rule_pos === undefined);
+
+ if (me.create) {
+ me.url = '/api2/extjs' + me.base_url;
+ me.method = 'POST';
+ } else {
+ me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString();
+ me.method = 'PUT';
+ }
+
+ var column1 = [
+ {
+ xtype: 'hiddenfield',
+ name: 'type',
+ value: 'group'
+ },
+ {
+ xtype: 'pveSecurityGroupsSelector',
+ name: 'action',
+ value: '',
+ fieldLabel: gettext('Security Group'),
+ allowBlank: false
+ }
+ ];
+
+ if (me.allow_iface) {
+ column1.push({
+ xtype: 'pvetextfield',
+ name: 'iface',
+ deleteEmpty: !me.create,
+ value: '',
+ fieldLabel: gettext('Interface')
+ });
+ }
+
+ var ipanel = Ext.create('PVE.panel.InputPanel', {
+ create: me.create,
+ column1: column1,
+ column2: [
+ {
+ xtype: 'pvecheckbox',
+ name: 'enable',
+ checked: false,
+ height: 22, // hack: set same height as text fields
+ uncheckedValue: 0,
+ fieldLabel: gettext('Enable')
+ }
+ ],
+ columnB: [
+ {
+ xtype: 'textfield',
+ name: 'comment',
+ value: '',
+ fieldLabel: gettext('Comment')
+ }
+ ]
+ });
+
+ Ext.apply(me, {
+ subject: gettext('Rule'),
+ isAdd: true,
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+
+ if (!me.create) {
+ me.load({
+ success: function(response, options) {
+ var values = response.result.data;
+ ipanel.setValues(values);
+ }
+ });
+ }
+ }
+});
+
+Ext.define('PVE.FirewallRules', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveFirewallRules',
+
+ base_url: undefined,
+
+ addBtn: undefined,
+ removeBtn: undefined,
+ editBtn: undefined,
+ groupBtn: undefined,
+
+ tbar_prefix: undefined,
+
+ allow_groups: true,
+ allow_iface: false,
+
+ setBaseUrl: function(url) {
+ var me = this;
+
+ me.base_url = url;
+
+ if (url === undefined) {
+ me.addBtn.setDisabled(true);
+ if (me.groupBtn) {
+ me.groupBtn.setDisabled(true);
+ }
+ me.store.removeAll();
+ } else {
+ me.addBtn.setDisabled(false);
+ if (me.groupBtn) {
+ me.groupBtn.setDisabled(false);
+ }
+ me.store.setProxy({
+ type: 'pve',
+ url: '/api2/json' + url
+ });
+
+ me.store.load();
+ }
+ },
+
+ moveRule: function(from, to) {
+ var me = this;
+
+ if (!me.base_url) {
+ return;
+ }
+
+ PVE.Utils.API2Request({
+ url: me.base_url + "/" + from,
+ method: 'PUT',
+ params: { moveto: to },
+ waitMsgTarget: me,
+ failure: function(response, options) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ callback: function() {
+ me.store.load();
+ }
+ });
+ },
+
+ createRule: function(editor, rule) {
+ var me = this;
+
+ if (!me.base_url) {
+ return;
+ }
+
+ rule.pos = 0;
+
+ rule.enable = rule.enable ? 1 : 0;
+
+ PVE.Utils.API2Request({
+ url: me.base_url,
+ method: 'POST',
+ params: rule,
+ waitMsgTarget: me,
+ failure: function(response, options) {
+ if (editor) {
+ editor.form.markInvalid(response.result.errors);
+ } else {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ },
+ callback: function() {
+ me.store.load();
+ }
+ });
+ },
+
+ updateRule: function(editor, rule) {
+ var me = this;
+
+ if (!me.base_url) {
+ return;
+ }
+
+ rule.enable = rule.enable ? 1 : 0;
+
+ var pos = rule.pos;
+ delete rule.pos;
+
+ PVE.Utils.API2Request({
+ url: me.base_url + '/' + pos.toString(),
+ method: 'PUT',
+ params: rule,
+ waitMsgTarget: me,
+ failure: function(response, options) {
+ if (editor) {
+ editor.form.markInvalid(response.result.errors);
+ } else {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ },
+ callback: function() {
+ me.store.load();
+ }
+ });
+ },
+
+ deleteRule: function(rule) {
+ var me = this;
+
+ if (!me.base_url) {
+ return;
+ }
+
+ PVE.Utils.API2Request({
+ url: me.base_url + '/' + rule.pos.toString() +
+ '?digest=' + encodeURIComponent(rule.digest),
+ method: 'DELETE',
+ waitMsgTarget: me,
+ failure: function(response, options) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ callback: function() {
+ me.store.load();
+ }
+ });
+ },
+
+ initComponent: function() {
+ /*jslint confusion: true */
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-fw-rule'
+ });
+
+ var reload = function() {
+ store.load();
+ };
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ var type = rec.data.type;
+
+ var editor;
+ if (type === 'in' || type === 'out') {
+ editor = 'PVE.FirewallRuleEdit';
+ } else if (type === 'group') {
+ editor = 'PVE.FirewallGroupRuleEdit';
+ } else {
+ return;
+ }
+
+ var win = Ext.create(editor, {
+ digest: rec.data.digest,
+ allow_iface: me.allow_iface,
+ base_url: me.base_url,
+ rule_pos: rec.data.pos
+ });
+
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ me.editBtn = new PVE.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor
+ });
+
+ me.addBtn = Ext.create('Ext.Button', {
+ text: gettext('Add'),
+ disabled: true,
+ handler: function() {
+ var win = Ext.create('PVE.FirewallRuleEdit', {
+ allow_iface: me.allow_iface,
+ base_url: me.base_url
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ });
+
+ if (me.allow_groups) {
+ me.groupBtn = Ext.create('Ext.Button', {
+ text: gettext('Insert') + ': ' + gettext('Security Group'),
+ disabled: true,
+ handler: function() {
+ var win = Ext.create('PVE.FirewallGroupRuleEdit', {
+ allow_iface: me.allow_iface,
+ base_url: me.base_url
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ });
+ }
+
+ me.removeBtn = new PVE.button.Button({
+ text: gettext('Remove'),
+ selModel: sm,
+ disabled: true,
+ handler: function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ me.deleteRule(rec.data);
+ }
+ });
+
+ var tbar = me.tbar_prefix ? [ me.tbar_prefix ] : [];
+ tbar.push(me.addBtn);
+ if (me.groupBtn) {
+ tbar.push(me.groupBtn);
+ }
+ tbar.push([ me.removeBtn, me.editBtn ]);
+
+ var columns = [
+ {
+ // similar to xtype: 'rownumberer',
+ dataIndex: 'pos',
+ resizable: false,
+ width: 23,
+ sortable: false,
+ align: 'right',
+ hideable: false,
+ menuDisabled: true,
+ renderer: function(value, metaData, record, rowIdx, colIdx, store) {
+ metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special';
+ if (value >= 0) {
+ return value;
+ }
+ return '';
+ }
+ },
+ {
+ xtype: 'checkcolumn',
+ header: gettext('Enable'),
+ dataIndex: 'enable',
+ listeners: {
+ checkchange: function(column, record, checked) {
+ record.commit();
+ var data = {};
+ record.fields.each(function(field) {
+ data[field.name] = record.get(field.name);
+ });
+ if (!me.allow_iface || !data.iface) {
+ delete data.iface;
+ }
+ me.updateRule(undefined, data);
+ }
+ },
+ width: 50
+ },
+ {
+ header: gettext('Type'),
+ dataIndex: 'type',
+ width: 50
+ },
+ {
+ header: gettext('Action'),
+ dataIndex: 'action',
+ width: 80
+ },
+ {
+ header: gettext('Macro'),
+ dataIndex: 'macro',
+ width: 80
+ }
+ ];
+
+ if (me.allow_iface) {
+ columns.push({
+ header: gettext('Interface'),
+ dataIndex: 'iface',
+ width: 80
+ });
+ }
+
+ columns.push([
+ {
+ header: gettext('Source'),
+ dataIndex: 'source',
+ width: 100
+ },
+ {
+ header: gettext('Destination'),
+ dataIndex: 'dest',
+ width: 100
+ },
+ {
+ header: gettext('Protocol'),
+ dataIndex: 'proto',
+ width: 100
+ },
+ {
+ header: gettext('Dest. port'),
+ dataIndex: 'dport',
+ width: 100
+ },
+ {
+ header: gettext('Source port'),
+ dataIndex: 'sport',
+ width: 100
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comment',
+ flex: 1,
+ renderer: function(value) {
+ return Ext.util.Format.htmlEncode(value);
+ }
+ }
+ ]);
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ tbar: tbar,
+ viewConfig: {
+ plugins: [
+ {
+ ptype: 'gridviewdragdrop',
+ dragGroup: 'FWRuleDDGroup',
+ dropGroup: 'FWRuleDDGroup'
+ }
+ ],
+ listeners: {
+ beforedrop: function(node, data, dropRec, dropPosition) {
+ if (!dropRec) {
+ return false; // empty view
+ }
+ var moveto = dropRec.get('pos');
+ if (dropPosition === 'after') {
+ moveto++;
+ }
+ var pos = data.records[0].get('pos');
+ me.moveRule(pos, moveto);
+ return 0;
+ },
+ itemdblclick: run_editor
+ }
+ },
+ columns: columns
+ });
+
+ me.callParent();
+
+ if (me.base_url) {
+ me.setBaseUrl(me.base_url); // load
+ }
+ }
+}, function() {
+
+ Ext.define('pve-fw-rule', {
+ extend: 'Ext.data.Model',
+ fields: [ { name: 'enable', type: 'boolean' },
+ 'type', 'action', 'macro', 'source', 'dest', 'proto', 'iface',
+ 'dport', 'sport', 'comment', 'pos', 'digest' ],
+ idProperty: 'pos'
+ });
+
+});
diff --git a/www/manager/panel/Firewall.js b/www/manager/panel/Firewall.js
new file mode 100644
index 00000000..02d28fc1
--- /dev/null
+++ b/www/manager/panel/Firewall.js
@@ -0,0 +1,78 @@
+Ext.define('PVE.panel.Firewall', {
+ extend: 'PVE.panel.SubConfig',
+ alias: 'widget.pveFirewallPanel',
+
+ configPrefix: 'firewall',
+
+ fwtype: undefined, // 'dc', 'node' or 'vm'
+
+ base_url: undefined,
+
+ initComponent: function() {
+ /*jslint confusion: true */
+ var me = this;
+
+ if (!me.base_url) {
+ throw "no base_url specified";
+ }
+
+ if (!(me.fwtype === 'dc' || me.fwtype === 'node' || me.fwtype === 'vm')) {
+ throw "unknown firewall panel type";
+ }
+
+ var items = [
+ {
+ xtype: 'pveFirewallRules',
+ title: 'Rules',
+ allow_iface: true,
+ base_url: me.base_url + '/rules',
+ itemId: 'rules'
+ }
+ ];
+
+ if (me.fwtype === 'dc') {
+ items.push({
+ xtype: 'pveSecurityGroups',
+ title: 'Security Groups',
+ itemId: 'sg'
+ });
+ items.push({
+ xtype: 'pveFirewallAliases',
+ base_url: '/cluster/firewall/aliases',
+ itemId: 'aliases'
+ });
+ items.push({
+ xtype: 'pveIPSet',
+ base_url: '/cluster/firewall/ipset',
+ itemId: 'ipset'
+ });
+ }
+
+ items.push({
+ xtype: 'pveFirewallOptions',
+ title: 'Options',
+ base_url: me.base_url + '/options',
+ fwtype: me.fwtype,
+ itemId: 'options'
+ });
+
+ if (me.fwtype !== 'dc') {
+ items.push({
+ title: 'Log',
+ itemId: 'fwlog',
+ xtype: 'pveLogView',
+ url: '/api2/extjs' + me.base_url + '/log'
+ });
+ }
+
+ Ext.apply(me, {
+ defaults: {
+ border: false,
+ pveSelNode: me.pveSelNode
+ },
+ items: items
+ });
+
+ me.callParent();
+ }
+});
\ No newline at end of file
diff --git a/www/manager/panel/IPSet.js b/www/manager/panel/IPSet.js
new file mode 100644
index 00000000..afd55b83
--- /dev/null
+++ b/www/manager/panel/IPSet.js
@@ -0,0 +1,426 @@
+Ext.define('PVE.IPSetList', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveIPSetList',
+
+ ipset_panel: undefined,
+
+ base_url: undefined,
+
+ addBtn: undefined,
+ removeBtn: undefined,
+ editBtn: undefined,
+
+ initComponent: function() {
+ /*jslint confusion: true */
+ var me = this;
+
+ if (me.ipset_panel == undefined) {
+ throw "no rule panel specified";
+ }
+
+ if (me.base_url == undefined) {
+ throw "no base_url specified";
+ }
+
+ var store = new Ext.data.Store({
+ fields: [ 'name', 'comment', 'digest' ],
+ proxy: {
+ type: 'pve',
+ url: "/api2/json" + me.base_url
+ },
+ idProperty: 'name',
+ sorters: {
+ property: 'name',
+ order: 'DESC'
+ }
+ });
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var reload = function() {
+ var oldrec = sm.getSelection()[0];
+ store.load(function(records, operation, success) {
+ if (oldrec) {
+ var rec = store.findRecord('name', oldrec.data.name);
+ if (rec) {
+ sm.select(rec);
+ }
+ }
+ });
+ };
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ var win = Ext.create('PVE.window.Edit', {
+ subject: "IPSet '" + rec.data.name + "'",
+ url: me.base_url,
+ method: 'POST',
+ digest: rec.data.digest,
+ items: [
+ {
+ xtype: 'hiddenfield',
+ name: 'rename',
+ value: rec.data.name
+ },
+ {
+ xtype: 'textfield',
+ name: 'name',
+ value: rec.data.name,
+ fieldLabel: gettext('Name'),
+ allowBlank: false
+ },
+ {
+ xtype: 'textfield',
+ name: 'comment',
+ value: rec.data.comment,
+ fieldLabel: gettext('Comment')
+ }
+ ]
+ });
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ me.editBtn = new PVE.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor
+ });
+
+ me.addBtn = new PVE.button.Button({
+ text: gettext('Create'),
+ handler: function() {
+ sm.deselectAll();
+ var win = Ext.create('PVE.window.Edit', {
+ subject: 'IPSet',
+ url: me.base_url,
+ method: 'POST',
+ items: [
+ {
+ xtype: 'textfield',
+ name: 'name',
+ value: '',
+ fieldLabel: gettext('Name'),
+ allowBlank: false
+ },
+ {
+ xtype: 'textfield',
+ name: 'comment',
+ value: '',
+ fieldLabel: gettext('Comment')
+ }
+ ]
+ });
+ win.show();
+ win.on('destroy', reload);
+
+ }
+ });
+
+ me.removeBtn = new PVE.button.Button({
+ text: gettext('Remove'),
+ selModel: sm,
+ disabled: true,
+ handler: function() {
+ var rec = sm.getSelection()[0];
+ if (!rec || !me.base_url) {
+ return;
+ }
+ PVE.Utils.API2Request({
+ url: me.base_url + '/' + rec.data.name,
+ method: 'DELETE',
+ waitMsgTarget: me,
+ failure: function(response, options) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ callback: reload
+ });
+ }
+ });
+
+ Ext.apply(me, {
+ store: store,
+ tbar: [ 'IPSet:', me.addBtn, me.removeBtn, me.editBtn ],
+ selModel: sm,
+ columns: [
+ { header: 'IPSet', dataIndex: 'name', width: 100 },
+ { header: gettext('Comment'), dataIndex: 'comment', flex: 1 }
+ ],
+ listeners: {
+ itemdblclick: run_editor,
+ select: function(sm, rec) {
+ var url = me.base_url + '/' + rec.data.name;
+ me.ipset_panel.setBaseUrl(url);
+ },
+ deselect: function() {
+ me.ipset_panel.setBaseUrl(undefined);
+ },
+ show: reload
+ }
+ });
+
+ me.callParent();
+
+ store.load();
+ }
+});
+
+Ext.define('PVE.IPSetCidrEdit', {
+ extend: 'PVE.window.Edit',
+
+ cidr: undefined,
+
+ initComponent : function() {
+ /*jslint confusion: true */
+ var me = this;
+
+ me.create = (me.cidr === undefined);
+
+ if (me.create) {
+ me.url = '/api2/extjs' + me.base_url;
+ me.method = 'POST';
+ } else {
+ me.url = '/api2/extjs' + me.base_url + '/' + me.cidr;
+ me.method = 'PUT';
+ }
+
+ var ipanel = Ext.create('PVE.panel.InputPanel', {
+ create: me.create,
+ column1: [
+ {
+ xtype: me.create ? 'textfield' : 'displayfield',
+ name: 'cidr',
+ height: 22, // hack: set same height as text fields
+ value: '',
+ fieldLabel: gettext('IP/CIDR')
+ }
+ ],
+ column2: [
+ {
+ xtype: 'pvecheckbox',
+ name: 'nomatch',
+ checked: false,
+ height: 22, // hack: set same height as text fields
+ uncheckedValue: 0,
+ fieldLabel: gettext('nomatch')
+ }
+ ],
+ columnB: [
+ {
+ xtype: 'textfield',
+ name: 'comment',
+ value: '',
+ fieldLabel: gettext('Comment')
+ }
+ ]
+ });
+
+ Ext.apply(me, {
+ subject: gettext('IP/CIDR'),
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+
+ if (!me.create) {
+ me.load({
+ success: function(response, options) {
+ var values = response.result.data;
+ ipanel.setValues(values);
+ }
+ });
+ }
+ }
+});
+
+Ext.define('PVE.IPSetGrid', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveIPSetGrid',
+
+ base_url: undefined,
+
+ addBtn: undefined,
+ removeBtn: undefined,
+ editBtn: undefined,
+
+ setBaseUrl: function(url) {
+ var me = this;
+
+ me.base_url = url;
+
+ if (url === undefined) {
+ me.addBtn.setDisabled(true);
+ me.store.removeAll();
+ } else {
+ me.addBtn.setDisabled(false);
+ me.store.setProxy({
+ type: 'pve',
+ url: '/api2/json' + url
+ });
+
+ me.store.load();
+ }
+ },
+
+ initComponent: function() {
+ /*jslint confusion: true */
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-ipset'
+ });
+
+ var reload = function() {
+ store.load();
+ };
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ var win = Ext.create('PVE.IPSetCidrEdit', {
+ base_url: me.base_url,
+ cidr: rec.data.cidr
+ });
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ me.editBtn = new PVE.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor
+ });
+
+ me.addBtn = new PVE.button.Button({
+ text: gettext('Add'),
+ disabled: true,
+ handler: function() {
+ if (!me.base_url) {
+ return;
+ }
+ var win = Ext.create('PVE.IPSetCidrEdit', {
+ base_url: me.base_url
+ });
+ win.show();
+ win.on('destroy', reload);
+ }
+ });
+
+ me.removeBtn = new PVE.button.Button({
+ text: gettext('Remove'),
+ selModel: sm,
+ disabled: true,
+ handler: function() {
+ var rec = sm.getSelection()[0];
+ if (!rec || !me.base_url) {
+ return;
+ }
+
+ PVE.Utils.API2Request({
+ url: me.base_url + '/' + rec.data.cidr,
+ method: 'DELETE',
+ waitMsgTarget: me,
+ failure: function(response, options) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ callback: reload
+ });
+ }
+ });
+
+ Ext.apply(me, {
+ tbar: [ 'IP/CIDR:', me.addBtn, me.removeBtn, me.editBtn ],
+ store: store,
+ selModel: sm,
+ listeners: {
+ itemdblclick: run_editor
+ },
+ columns: [
+ {
+ xtype: 'rownumberer'
+ },
+ {
+ header: gettext('IP/CIDR'),
+ dataIndex: 'cidr',
+ width: 150,
+ renderer: function(value, metaData, record) {
+ if (record.data.nomatch) {
+ return '! ' + value;
+ }
+ return value;
+ }
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comment',
+ flex: 1,
+ renderer: function(value) {
+ return Ext.util.Format.htmlEncode(value);
+ }
+ }
+ ]
+ });
+
+ me.callParent();
+
+ if (me.base_url) {
+ me.setBaseUrl(me.base_url); // load
+ }
+ }
+}, function() {
+
+ Ext.define('pve-ipset', {
+ extend: 'Ext.data.Model',
+ fields: [ { name: 'nomatch', type: 'boolean' },
+ 'cidr', 'comment' ],
+ idProperty: 'cidr'
+ });
+
+});
+
+Ext.define('PVE.IPSet', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveIPSet',
+
+ title: 'IPSet',
+
+ initComponent: function() {
+ var me = this;
+
+ var ipset_panel = Ext.createWidget('pveIPSetGrid', {
+ region: 'center',
+ flex: 0.5,
+ border: false
+ });
+
+ var ipset_list = Ext.createWidget('pveIPSetList', {
+ region: 'west',
+ ipset_panel: ipset_panel,
+ base_url: me.base_url,
+ flex: 0.5,
+ border: false,
+ split: true
+ });
+
+ Ext.apply(me, {
+ layout: 'border',
+ items: [ ipset_list, ipset_panel ],
+ listeners: {
+ show: function() {
+ ipset_list.fireEvent('show', ipset_list);
+ }
+ }
+ });
+
+ me.callParent();
+ }
+});
diff --git a/www/manager/panel/SubConfigPanel.js b/www/manager/panel/SubConfigPanel.js
new file mode 100644
index 00000000..849fd041
--- /dev/null
+++ b/www/manager/panel/SubConfigPanel.js
@@ -0,0 +1,80 @@
+Ext.define('PVE.panel.SubConfig', {
+ extend: 'Ext.tab.Panel',
+ alias: ['widget.pvePanelSubConfig'],
+
+ configPrefix: undefined,
+
+ getHState: function(itemId) {
+ /*jslint confusion: true */
+ var me = this;
+
+ if (!itemId) {
+ itemId = me.getActiveTab().itemId;
+ }
+
+ var first = me.items.get(0);
+ var ntab;
+
+ // Note: '' is alias for first tab.
+ if (itemId === first.itemId) {
+ ntab = me.configPrefix;
+ } else {
+ ntab = me.configPrefix + '-' + itemId;
+ }
+
+ return { value: ntab };
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.phstateid) {
+ throw "no parent history state specified";
+ }
+
+ var sp = Ext.state.Manager.getProvider();
+ var state = sp.get(me.phstateid);
+
+ var hsregex = /^([^\-\s]+)-(\S+)?$/;
+
+ if (state && state.value) {
+ var res = hsregex.exec(state.value);
+ if (res && res[1] && res[2] && res[1] === me.configPrefix) {
+ me.activeTab = res[2];
+ }
+ }
+
+ Ext.apply(me, {
+ plain: true,
+ tabPosition: 'bottom',
+ listeners: {
+ afterrender: function(tp) {
+ var first = tp.items.get(0);
+ if (first) {
+ first.fireEvent('show', first);
+ }
+ },
+ tabchange: function(tp, newcard, oldcard) {
+ var state = me.getHState(newcard.itemId);
+ sp.set(me.phstateid, state);
+ }
+ }
+ });
+
+ me.callParent();
+
+ var statechange = function(sp, key, state) {
+ if ((key === me.phstateid) && state) {
+ var first = me.items.get(0);
+ var atab = me.getActiveTab().itemId;
+ var res = hsregex.exec(state.value);
+ var ntab = (res && res[1]) ? res[1] : first.itemId;
+ if (ntab && (atab != ntab)) {
+ me.setActiveTab(ntab);
+ }
+ }
+ };
+
+ me.mon(sp, 'statechange', statechange);
+ }
+});