[pve-devel] [PATCH WIP widget-toolkit] introduce abstractions for /etc/hosts view
Leo Nunner
l.nunner at proxmox.com
Thu Sep 14 12:03:38 CEST 2023
Remove the textarea and instead introduce a gridview. It shows all the
values that are being returned by the API endpoint:
- Line
The actual line number inside the file.
- Enabled
Whether the line is commented out or not.
- IP
The IP address which is being mapped.
- Hosts
The list of hostnames for the IP.
- Value
The raw line value as it is stored in /etc/hosts.
Entries can be added/edited/removed, and their 'Enabeld' status can be
toggled via the 'Toggle' button.
Signed-off-by: Leo Nunner <l.nunner at proxmox.com>
---
src/Makefile | 1 +
src/node/HostsView.js | 329 ++++++++++++++++++++++++++++--------
src/panel/HostsEditPanel.js | 137 +++++++++++++++
3 files changed, 401 insertions(+), 66 deletions(-)
create mode 100644 src/panel/HostsEditPanel.js
diff --git a/src/Makefile b/src/Makefile
index 21fbe76..637bde4 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -77,6 +77,7 @@ JSSRC= \
panel/StatusView.js \
panel/TfaView.js \
panel/NotesView.js \
+ panel/HostsEditPanel.js \
window/Edit.js \
window/PasswordEdit.js \
window/SafeDestroy.js \
diff --git a/src/node/HostsView.js b/src/node/HostsView.js
index 9adb6b2..8415d4a 100644
--- a/src/node/HostsView.js
+++ b/src/node/HostsView.js
@@ -1,66 +1,102 @@
+Ext.define('pve-etc-hosts-entry', {
+ extend: 'Ext.data.Model',
+ fields: [
+ { name: 'enabled', type: 'boolean' },
+ { name: 'ip', type: 'string' },
+ { name: 'hosts', type: 'string' },
+ { name: 'value', type: 'string' },
+ { name: 'line', type: 'int' },
+ ],
+});
+
+Ext.define('Proxmox.node.HostsEditWindow', {
+ extend: 'Proxmox.window.Edit',
+
+ width: 600,
+
+ line: undefined,
+
+ initComponent: function() {
+ var me = this;
+
+ Ext.apply(me, {
+ subject: "Hosts entry",
+ defaultFocus: 'textfield[name=ip]',
+ });
+
+ Ext.apply(me, {
+ items: [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'enabled',
+ dataIndex: 'enabled',
+ fieldLabel: 'Enable',
+ uncheckedValue: 0,
+ },
+ {
+ xtype: me.isCreate ? 'numberfield' : 'hiddenfield',
+ fieldLabel: 'Line',
+ name: 'line',
+ dataIndex: 'line',
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('IP'),
+ name: 'ip',
+ dataIndex: 'ip',
+ allowBlank: false,
+ vtype: 'IP64Address',
+ },
+ {
+ xtype: 'fieldcontainer',
+ fieldLabel: gettext('Hostnames'),
+ items: [
+ {
+ xtype: 'proxmoxHostsEditPanel', name: 'hosts',
+ },
+ ],
+ },
+ ],
+ });
+
+ let base_url = `/api2/extjs/nodes/${me.nodename}/hosts`;
+ if (me.isCreate) {
+ me.url = base_url;
+ me.method = 'POST';
+ } else {
+ me.url = base_url + `/${me.line}`;
+ me.method = 'PUT';
+ }
+
+ me.callParent();
+
+ let hostsPanel = me.down("proxmoxHostsEditPanel");
+ let hostsController = hostsPanel.getController();
+
+ me.setValues({ line: me.line });
+
+ if (!me.isCreate) { // do we already have data?
+ me.load();
+ } else { // if not, we create a single empty host entry
+ hostsController.addHost();
+ }
+ },
+});
+
Ext.define('Proxmox.node.HostsView', {
- extend: 'Ext.panel.Panel',
+ extend: 'Ext.grid.Panel',
xtype: 'proxmoxNodeHostsView',
reload: function() {
let me = this;
- me.store.load();
+ let view = me.getView();
+ view.store.load();
},
- tbar: [
- {
- text: gettext('Save'),
- disabled: true,
- itemId: 'savebtn',
- handler: function() {
- let view = this.up('panel');
- Proxmox.Utils.API2Request({
- params: {
- digest: view.digest,
- data: view.down('#hostsfield').getValue(),
- },
- method: 'POST',
- url: '/nodes/' + view.nodename + '/hosts',
- waitMsgTarget: view,
- success: function(response, opts) {
- view.reload();
- },
- failure: function(response, opts) {
- Ext.Msg.alert('Error', response.htmlStatus);
- },
- });
- },
- },
- {
- text: gettext('Revert'),
- disabled: true,
- itemId: 'resetbtn',
- handler: function() {
- let view = this.up('panel');
- view.down('#hostsfield').reset();
- },
- },
- ],
+ layout: 'fit',
- layout: 'fit',
-
- items: [
- {
- xtype: 'textarea',
- itemId: 'hostsfield',
- fieldStyle: {
- 'font-family': 'monospace',
- 'white-space': 'pre',
- },
- listeners: {
- dirtychange: function(ta, dirty) {
- let view = this.up('panel');
- view.down('#savebtn').setDisabled(!dirty);
- view.down('#resetbtn').setDisabled(!dirty);
- },
- },
- },
- ],
+ nodename: undefined,
initComponent: function() {
let me = this;
@@ -70,26 +106,187 @@ Ext.define('Proxmox.node.HostsView', {
}
me.store = Ext.create('Ext.data.Store', {
+ model: 'pve-etc-hosts-entry',
proxy: {
- type: 'proxmox',
- url: "/api2/json/nodes/" + me.nodename + "/hosts",
+ type: 'proxmox',
+ url: `/api2/json/nodes/${me.nodename}/hosts`,
},
+ sorters: [
+ {
+ property: 'line',
+ direction: 'ASC',
+ },
+ ],
});
- me.callParent();
+ var sm = Ext.create('Ext.selection.RowModel', {});
- Proxmox.Utils.monStoreErrors(me, me.store);
-
- me.mon(me.store, 'load', function(store, records, success) {
- if (!success || records.length < 1) {
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec || !(rec.data.ip || rec.data.hosts)) {
return;
}
- me.digest = records[0].data.digest;
- let data = records[0].data.data;
- me.down('#hostsfield').setValue(data);
- me.down('#hostsfield').resetOriginalValue();
+
+ let win = Ext.create('Proxmox.node.HostsEditWindow', {
+ isCreate: false,
+ nodename: me.nodename,
+ line: rec.data.line,
+ });
+ win.on('destroy', me.reload, me);
+ win.show();
+ };
+
+ Ext.apply(me, {
+ tbar: [
+ {
+ text: gettext('Add'),
+ itemId: 'addbtn',
+ handler: function() {
+ let items = me.store.getData().items;
+ let maxLine = items.reduce((a, v) => Math.max(a, v.data.line), -Infinity);
+ let win = Ext.create('Proxmox.node.HostsEditWindow', {
+ isCreate: true,
+ nodename: me.nodename,
+ line: maxLine + 1,
+ });
+ win.on('destroy', me.reload, me);
+ win.show();
+ },
+ },
+ {
+ text: gettext('Edit'),
+ itemId: 'editbtn',
+ handler: run_editor,
+ },
+ {
+ text: gettext('Delete'),
+ itemId: 'deletebtn',
+ handler: function() {
+ let rec = sm.getSelection()[0];
+
+ Proxmox.Utils.API2Request({
+ method: 'DELETE',
+ url: `/nodes/${me.nodename}/hosts/${rec.data.line}?move=1`,
+ waitMsgTarget: me,
+ success: function(response, opts) {
+ me.reload();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ });
+ },
+ },
+ {
+ text: gettext('Toggle'),
+ itemId: 'togglebtn',
+ handler: function() {
+ let rec = sm.getSelection()[0];
+ let params = rec.data;
+ params.enabled = params.enabled ? 0 : 1;
+
+ Proxmox.Utils.API2Request({
+ method: 'PUT',
+ url: `/nodes/${me.nodename}/hosts/${rec.data.line}`,
+ params: params,
+ waitMsgTarget: me,
+ success: function(response, opts) {
+ me.reload();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ });
+ },
+ },
+ '->',
+ gettext('Search') + ':',
+ ' ',
+ {
+ xtype: 'textfield',
+ width: 200,
+ enableKeyEvents: true,
+ emptyText: gettext("IP, FQDN"),
+ listeners: {
+ keyup: {
+ buffer: 500,
+ fn: function(field) {
+ let needle = field.getValue().toLocaleLowerCase();
+ me.store.clearFilter(true);
+ me.store.filter([
+ {
+ filterFn: ({ data }) =>
+ data.ip?.toLocaleLowerCase().includes(needle) ||
+ data.hosts?.toLocaleLowerCase().includes(needle),
+ },
+ ]);
+ },
+ },
+ change: function(field, newValue, oldValue) {
+ if (newValue !== this.originalValue) {
+ this.triggers.clear.setVisible(true);
+ }
+ },
+ },
+ triggers: {
+ clear: {
+ cls: 'pmx-clear-trigger',
+ weight: -1,
+ hidden: true,
+ handler: function() {
+ this.triggers.clear.setVisible(false);
+ this.setValue(this.originalValue);
+ me.store.clearFilter();
+ },
+ },
+ },
+ },
+ ],
+ });
+
+ Ext.apply(me, {
+ bodystyle: {
+ width: '100% !important',
+ },
+ columns: [
+ {
+ text: gettext('Line'),
+ dataIndex: 'line',
+ width: 70,
+ },
+ {
+ header: gettext('Enabled'),
+ dataIndex: 'enabled',
+ align: 'center',
+ renderer: Proxmox.Utils.renderEnabledIcon,
+ width: 90,
+ },
+ {
+ text: gettext('IP'),
+ dataIndex: 'ip',
+ width: 150,
+ },
+ {
+ text: gettext('Hosts'),
+ dataIndex: 'hosts',
+ flex: 1,
+ },
+ {
+ text: gettext('Value'),
+ dataIndex: 'value',
+ flex: 1,
+ },
+ ],
+ });
+
+ Ext.apply(me, {
+ selModel: sm,
+ listeners: {
+ itemdblclick: run_editor,
+ },
});
+ me.callParent();
me.reload();
},
});
diff --git a/src/panel/HostsEditPanel.js b/src/panel/HostsEditPanel.js
new file mode 100644
index 0000000..34e99e1
--- /dev/null
+++ b/src/panel/HostsEditPanel.js
@@ -0,0 +1,137 @@
+Ext.define('Proxmox.node.HostsEntryEditor', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'proxmoxHostsEntryEditor',
+
+ layout: 'hbox',
+ border: false,
+
+ margin: '5 5 5 5',
+
+ initComponent: function() {
+ let me = this;
+
+ Ext.apply(me, {
+ items: [
+ {
+ xtype: 'proxmoxtextfield',
+ width: 200,
+ margin: '0 5 0 0 ',
+ value: me.value,
+ allowBlank: false,
+ isFormField: false,
+ },
+ {
+ xtype: 'button',
+ iconCls: 'fa fa-trash-o',
+ cls: 'removeLinkBtn',
+ handler: function() {
+ let parent = this.up('proxmoxHostsEntryEditor');
+ if (parent.removeBtnHandler !== undefined) {
+ parent.removeBtnHandler();
+ }
+ },
+ },
+ ],
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('Proxmox.node.HostsEdit', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'proxmoxHostsEditPanel',
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ loadData: function(data) {
+ let me = this;
+ let hosts = data.split(",");
+
+ let view = me.getView();
+ view.query("proxmoxHostsEntryEditor").forEach((e) => view.remove(e.id));
+
+ hosts.forEach((host) => {
+ me.addHost(host);
+ });
+ },
+
+ addHost: function(value) {
+ let me = this;
+ let view = me.getView();
+ let hostEdit = Ext.create('Proxmox.node.HostsEntryEditor', {
+ value: value,
+ removeBtnHandler: function() {
+ view.remove(this);
+ view.getController().setMarkerValue();
+ },
+ });
+ view.add(hostEdit);
+ },
+
+ addBtnHandler: function() {
+ let me = this;
+ me.addHost("");
+ me.setMarkerValue();
+ },
+
+ setMarkerValue() {
+ let me = this;
+ let view = me.getView();
+ view.inUpdate = true;
+ me.lookup('marker').setValue(me.calculateValue());
+ view.inUpdate = false;
+ },
+
+ calculateValue: function() {
+ let me = this;
+ let view = me.getView();
+
+ let hosts = [];
+ Ext.Array.each(view.query('proxmoxtextfield'), function(field) {
+ hosts.push(field.value);
+ });
+ return hosts.join(",");
+ },
+
+ control: {
+ "proxmoxtextfield": {
+ change: "setMarkerValue",
+ },
+ },
+ },
+
+ items: [
+ {
+ xtype: 'hiddenfield',
+ reference: 'marker',
+ name: 'hosts',
+ setValue: function(value) {
+ let me = this;
+ let panel = me.up('proxmoxHostsEditPanel');
+
+ if (!panel.inUpdate) {
+ panel.getController().loadData(value);
+ }
+
+ return Ext.form.field.Hidden.prototype.setValue.call(this, value);
+ },
+ },
+ ],
+
+ dockedItems: [{
+ xtype: 'toolbar',
+ dock: 'bottom',
+ defaultButtonUI: 'default',
+ border: false,
+ padding: '6 0 6 0',
+ items: [
+ {
+ xtype: 'button',
+ text: gettext('Add'),
+ handler: 'addBtnHandler',
+ },
+ ],
+ }],
+});
--
2.39.2
More information about the pve-devel
mailing list