From 151d842e3e9497363a84583f28c8c750714da8d1 Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edewata@redhat.com>
Date: Thu, 3 Apr 2014 16:41:26 -0400
Subject: [PATCH] Refactored UI framework.

A new tps.js has been added to store TPS-specific classes including
PropertiesTable and EntryWithPropertiesPage.

The Navigation has been refactored to simplify page registration.
The render() method now has to be called separately after instance
creation.

The Table has been refactored to improve reusability. The code that
handles a generic array of entry objects has been moved from the
PropertiesTable into the Table class. The code that handles a
Collection of Models has been moved into ModelTable. The Table now
supports entry sorting and attribute mapping. The Table also
supports view and edit modes. In view mode the Table will be
read-only. In edit mode the action buttons will appear.

The EntryPage has been refactored to handle a generic set of fields.
The editable fields can be specified in a list. The code related to
enable/disable buttons and properties table has been moved to
EntryWithPropertiesPage.

Some unused classes have been removed. Incorrect colum names in
the HTML templates have been fixed.

Ticket #654
---
 base/server/share/webapps/pki/js/pki-ui.js         | 510 +++++++++------------
 .../shared/webapps/tps/js/authenticator.js         |   8 +-
 base/tps-tomcat/shared/webapps/tps/js/cert.js      |   2 +-
 .../tps-tomcat/shared/webapps/tps/js/connection.js |   8 +-
 base/tps-tomcat/shared/webapps/tps/js/group.js     |   2 +-
 .../shared/webapps/tps/js/profile-mapping.js       |   8 +-
 base/tps-tomcat/shared/webapps/tps/js/profile.js   |   8 +-
 base/tps-tomcat/shared/webapps/tps/js/selftest.js  |   2 +-
 base/tps-tomcat/shared/webapps/tps/js/token.js     |   2 +-
 base/tps-tomcat/shared/webapps/tps/js/tps.js       | 171 +++++++
 base/tps-tomcat/shared/webapps/tps/js/user.js      |   2 +-
 .../shared/webapps/tps/ui/authenticator.html       |   2 +-
 .../shared/webapps/tps/ui/connection.html          |   2 +-
 base/tps-tomcat/shared/webapps/tps/ui/index.html   |  71 ++-
 .../shared/webapps/tps/ui/profile-mapping.html     |   2 +-
 base/tps-tomcat/shared/webapps/tps/ui/profile.html |   2 +-
 16 files changed, 471 insertions(+), 331 deletions(-)
 create mode 100644 base/tps-tomcat/shared/webapps/tps/js/tps.js

diff --git a/base/server/share/webapps/pki/js/pki-ui.js b/base/server/share/webapps/pki/js/pki-ui.js
index 949096c96076c79e856ecb6cb6b48378226f4bfc..56a53ab8ab7df6b9ee5dca1169ea8321bf905864 100644
--- a/base/server/share/webapps/pki/js/pki-ui.js
+++ b/base/server/share/webapps/pki/js/pki-ui.js
@@ -147,8 +147,7 @@ var Navigation = Backbone.View.extend({
     initialize: function(options) {
         var self = this;
         Navigation.__super__.initialize.call(self, options);
-
-        self.pages = options.pages;
+        self.pages = options.pages || {};
         self.homePage = options.homePage;
 
         $("li", self.$el).each(function(index) {
@@ -169,8 +168,10 @@ var Navigation = Backbone.View.extend({
                 e.preventDefault();
             });
         });
-
-        if (self.homePage) self.load(self.homePage);
+    },
+    page: function(name, page) {
+        var self = this;
+        self.pages[name] = page;
     },
     load: function(name) {
         var self = this;
@@ -182,6 +183,10 @@ var Navigation = Backbone.View.extend({
         }
 
         page.open();
+    },
+    render: function() {
+        var self = this;
+        if (self.homePage) self.load(self.homePage);
     }
 });
 
@@ -233,7 +238,7 @@ var Dialog = Backbone.View.extend({
         self.$("input").each(function(index) {
             var input = $(this);
             var name = input.attr("name");
-            if ( _.contains(self.readonly, name)) {
+            if (_.contains(self.readonly, name)) {
                 input.attr("readonly", "readonly");
             } else {
                 input.removeAttr("readonly");
@@ -295,7 +300,7 @@ var Dialog = Backbone.View.extend({
         var self = this;
         var name = input.attr("name");
         var value = self.attributes[name];
-        if (!value) value = "";
+        if (value === undefined) value = "";
         input.val(value);
     },
     save: function() {
@@ -355,18 +360,19 @@ var TableItem = Backbone.View.extend({
             var name = td.attr("name");
 
             if (td.hasClass("pki-select-column")) {
-                // generate a unique id based on model id
-                var id = prefix + self.id();
+                // generate a unique input ID based on entry ID
+                var entryID = self.get("id")
+                var inputID = prefix + entryID;
 
-                // set the unique id and value for checkbox
+                // set the checkbox ID and value
                 var checkbox = $("input[type='checkbox']", td);
-                checkbox.attr("id", id);
+                checkbox.attr("id", inputID);
                 checkbox.attr("checked", false);
-                checkbox.val(self.id());
+                checkbox.val(entryID);
 
                 // point the label to the checkbox and make it visible
                 var label = $("label", td);
-                label.attr("for", id);
+                label.attr("for", inputID);
                 label.show();
 
             } else if (name == "id") {
@@ -377,9 +383,10 @@ var TableItem = Backbone.View.extend({
             }
         });
     },
-    id: function() {
+    get: function(name) {
         var self = this;
-        return self.model.id;
+        var attribute = self.table.columnMappings[name] || name;
+        return self.entry[attribute];
     },
     renderIDColumn: function(td) {
         var self = this;
@@ -388,7 +395,7 @@ var TableItem = Backbone.View.extend({
         td.empty();
         $("<a/>", {
             href: "#",
-            text: self.id(),
+            text: self.get("id"),
             click: function(e) {
                 self.table.open(self);
                 e.preventDefault();
@@ -400,7 +407,7 @@ var TableItem = Backbone.View.extend({
 
         // show content in plain text
         var name = td.attr("name");
-        td.text(self.model.get(name));
+        td.text(self.get(name));
     }
 });
 
@@ -409,6 +416,10 @@ var Table = Backbone.View.extend({
         var self = this;
 
         Table.__super__.initialize.call(self, options);
+        self.entries = options.entries || [];
+        self.columnMappings = options.columnMappings || {};
+        self.mode = options.mode || "view";
+
         self.addDialog = options.addDialog;
         self.editDialog = options.editDialog;
         self.viewDialog = options.viewDialog;
@@ -522,20 +533,147 @@ var Table = Backbone.View.extend({
             e.preventDefault();
         });
     },
-    readOnly: function(value) {
+    render: function() {
         var self = this;
-        if (value === undefined) {
-            var display = self.buttons.css("display");
-            value = display == "none";
 
-        } else if (value) {
+        // perform manual filter
+        var filter = self.searchField.val();
+        self.filteredEntries = [];
+
+        _(self.entries).each(function(item, index) {
+            if (!self.matchesFilter(item, filter)) return;
+            self.filteredEntries.push(item);
+        });
+
+        self.sort();
+
+        // update controls
+        self.renderControls();
+
+        // display entries
+        _(self.items).each(function(item, index) {
+            self.renderRow(item, index);
+        });
+    },
+    sort: function() {
+        var self = this;
+        // by default the list is not sorted
+    },
+    matchesFilter: function(entry, filter) {
+        var self = this;
+
+        // check filter against all values in the entry
+        var matches = false;
+        _(entry).each(function(value, key) {
+            if (entry.name.indexOf(filter) >= 0) matches = true;
+        });
+
+        return matches;
+    },
+    renderControls: function() {
+        var self = this;
+
+        if (self.mode == "view") {
             self.buttons.hide();
 
-        } else {
+        } else { // self.mode == "edit"
             self.buttons.show();
         }
 
-        return value;
+        // clear selection
+        self.selectAllCheckbox.attr("checked", false);
+
+        // display total entries
+        self.totalEntriesField.text(self.totalEntries());
+
+        // display current page number
+        self.pageField.val(self.page);
+
+        // calculate and display total number of pages
+        self.totalPages = Math.floor(Math.max(0, self.totalEntries() - 1) / self.pageSize) + 1;
+        self.totalPagesField.text(self.totalPages);
+    },
+    renderRow: function(item, index) {
+        var self = this;
+        var i = (self.page - 1) * self.pageSize + index;
+        if (i < self.filteredEntries.length) {
+            // show entry in existing row
+            item.entry = self.filteredEntries[i];
+            item.render();
+
+        } else {
+            // clear unused row
+            item.reset();
+        }
+    },
+    totalEntries: function() {
+        var self = this;
+        return self.filteredEntries.length;
+    },
+    open: function(item) {
+        var self = this;
+
+        var dialog;
+        if (self.mode == "view") {
+            dialog = self.viewDialog;
+
+        } else { // self.mode == "edit"
+            dialog = self.editDialog;
+
+            dialog.handler("save", function() {
+
+                // save changes
+                dialog.save();
+                _.extend(item.entry, dialog.attributes);
+
+                // redraw table
+                self.render();
+                dialog.close();
+            });
+        }
+
+        dialog.attributes = _.clone(item.entry);
+
+        dialog.open();
+    },
+    add: function() {
+        var self = this;
+
+        var dialog = self.addDialog;
+        dialog.attributes = {};
+
+        dialog.handler("add", function() {
+
+            // save new entry
+            dialog.save();
+            self.entries.push(dialog.attributes);
+
+            // redraw table
+            self.render();
+            dialog.close();
+        });
+
+        dialog.open();
+    },
+    remove: function(items) {
+        var self = this;
+
+        // remove selected entries
+        self.entries = _.reject(self.entries, function(entry) {
+            return _.contains(items, entry.id);
+        });
+
+        // redraw table
+        self.render();
+    }
+});
+
+var ModelTable = Table.extend({
+    initialize: function(options) {
+        var self = this;
+        options.mode = options.mode || "edit";
+        ModelTable.__super__.initialize.call(self, options);
+        self.collection = options.collection;
     },
     render: function() {
         var self = this;
@@ -565,27 +703,12 @@ var Table = Backbone.View.extend({
             }
         });
     },
-    renderControls: function() {
-        var self = this;
-
-        // clear selection
-        self.selectAllCheckbox.attr("checked", false);
-
-        // display total entries
-        self.totalEntriesField.text(self.totalEntries());
-
-        // display current page number
-        self.pageField.val(self.page);
-
-        // calculate and display total number of pages
-        self.totalPages = Math.floor(Math.max(0, self.totalEntries() - 1) / self.pageSize) + 1;
-        self.totalPagesField.text(self.totalPages);
-    },
     renderRow: function(item, index) {
         var self = this;
         if (index < self.collection.length) {
             // show entry in existing row
-            item.model = self.collection.at(index);
+            var model = self.collection.at(index);
+            item.entry = _.clone(model.attributes);
             item.render();
 
         } else {
@@ -600,9 +723,10 @@ var Table = Backbone.View.extend({
     open: function(item) {
         var self = this;
 
+        var model = self.collection.get(item.entry.id);
+
         var dialog = self.editDialog;
-        var model = item.model;
-        dialog.attributes = _.clone(model.attributes);
+        dialog.attributes = item.entry;
 
         dialog.handler("save", function() {
 
@@ -699,146 +823,25 @@ var Table = Backbone.View.extend({
     }
 });
 
-var PropertiesTableItem = TableItem.extend({
-    initialize: function(options) {
-        var self = this;
-        PropertiesTableItem.__super__.initialize.call(self, options);
-    },
-    id: function() {
-        var self = this;
-        return self.property.name;
-    },
-    renderColumn: function(td) {
-        var self = this;
-        td.text(self.property.value);
-    }
-});
-
-var PropertiesTable = Table.extend({
-    initialize: function(options) {
-        var self = this;
-        options.tableItem = options.tableItem || PropertiesTableItem;
-        PropertiesTable.__super__.initialize.call(self, options);
-        self.properties = options.properties;
-    },
-    render: function() {
-        var self = this;
-
-        // perform manual filter
-        var filter = self.searchField.val();
-        if (filter === "") {
-            self.entries = self.properties;
-
-        } else {
-            self.entries = [];
-            _(self.properties).each(function(item, index) {
-                // select properties whose names or values contain the filter
-                if (item.name.indexOf(filter) >= 0 || item.value.indexOf(filter) >= 0) {
-                    self.entries.push(item);
-                }
-            });
-        }
-
-        self.renderControls();
-
-        _(self.items).each(function(item, index) {
-            self.renderRow(item, index);
-        });
-    },
-    renderRow: function(item, index) {
-        var self = this;
-        var i = (self.page - 1) * self.pageSize + index;
-        if (i < self.entries.length) {
-            // show entry in existing row
-            item.property = self.entries[i];
-            item.render();
-
-        } else {
-            // clear unused row
-            item.reset();
-        }
-    },
-    totalEntries: function() {
-        var self = this;
-        return self.entries.length;
-    },
-    open: function(item) {
-        var self = this;
-
-        var dialog;
-        if (self.readOnly()) {
-            dialog = self.viewDialog;
-
-        } else {
-            dialog = self.editDialog;
-
-            dialog.handler("save", function() {
-
-                // save property changes
-                dialog.save();
-                _.extend(item.property, dialog.attributes);
-
-                // redraw table after saving property changes
-                self.render();
-                dialog.close();
-            });
-		}
-
-        dialog.attributes = _.clone(item.property);
-
-        dialog.open();
-    },
-    add: function() {
-        var self = this;
-
-        var dialog = self.addDialog;
-        dialog.attributes = {};
-
-        dialog.handler("add", function() {
-
-            // save new property
-            dialog.save();
-            self.properties.push(dialog.attributes);
-
-            // sort properties by name
-            self.properties = _.sortBy(self.properties, function(property) {
-                return property.name;
-            });
-
-            // redraw table after adding new property
-            self.render();
-            dialog.close();
-        });
-
-        dialog.open();
-    },
-    remove: function(items) {
-        var self = this;
-
-        // remove selected properties
-        self.properties = _.reject(self.properties, function(property) {
-            return _.contains(items, property.name);
-        });
-
-        // redraw table after removing properties
-        self.render();
-    }
-});
-
 var EntryPage = Page.extend({
     initialize: function(options) {
         var self = this;
         EntryPage.__super__.initialize.call(self, options);
         self.model = options.model;
         self.mode = options.mode || "view";
+        self.editable = options.editable || [];
     },
     load: function() {
         var self = this;
 
+        self.setup();
+        self.render();
+    },
+    setup: function() {
+        var self = this;
+
         self.menu = self.$(".pki-menu");
         self.editLink = $("a[name='edit']", self.menu);
-        self.enableLink = $("a[name='enable']", self.menu);
-        self.disableLink = $("a[name='disable']", self.menu);
 
         self.buttons = self.$(".pki-buttons");
         self.cancelButton = $("button[name='cancel']", self.buttons);
@@ -847,45 +850,12 @@ var EntryPage = Page.extend({
         self.idField = self.$("input[name='id']");
         self.statusField = self.$("input[name='status']");
 
-        var table = self.$("table[name='properties']");
-        self.propertiesButtons = $(".pki-table-buttons", table);
-        self.addButton = $("button[name='add']", self.propertiesButtons);
-        self.removeButton = $("button[name='remove']", self.propertiesButtons);
-
         self.editLink.click(function(e) {
             self.mode = "edit";
             self.render();
             e.preventDefault();
         });
 
-        self.enableLink.click(function(e) {
-            var message = "Are you sure you want to enable this entry?";
-            if (!confirm(message)) return;
-            self.model.enable({
-                success: function(data,textStatus, jqXHR) {
-                    self.attributes = _.clone(self.model.attributes);
-                    self.render();
-                },
-                error: function(jqXHR, textStatus, errorThrown) {
-                    alert("ERROR: " + textStatus);
-                }
-            });
-        });
-
-        self.disableLink.click(function(e) {
-            var message = "Are you sure you want to disable this entry?";
-            if (!confirm(message)) return;
-            self.model.disable({
-                success: function(data,textStatus, jqXHR) {
-                    self.attributes = _.clone(self.model.attributes);
-                    self.render();
-                },
-                error: function(jqXHR, textStatus, errorThrown) {
-                    alert("ERROR: " + textStatus);
-                }
-            });
-        });
-
         self.cancelButton.click(function(e) {
             self.cancel();
             e.preventDefault();
@@ -896,54 +866,17 @@ var EntryPage = Page.extend({
             e.preventDefault();
         });
 
-        var dialog = self.$("#property-dialog");
-
-        var addDialog = new Dialog({
-            el: dialog,
-            title: "Add Property",
-            actions: ["cancel", "add"]
-        });
-
-        var editDialog = new Dialog({
-            el: dialog,
-            title: "Edit Property",
-            readonly: ["name"],
-            actions: ["cancel", "save"]
-        });
-
-        var viewDialog = new Dialog({
-            el: dialog,
-            title: "Property",
-            readonly: ["name", "value"],
-            actions: ["close"]
-        });
-
-        self.propertiesTable = new PropertiesTable({
-            el: table,
-            addDialog: addDialog,
-            editDialog: editDialog,
-            viewDialog: viewDialog,
-            pageSize: 10
-        });
-
-        self.render();
     },
     render: function() {
         var self = this;
 
-        // in add mode, render blank form
         if (self.mode == "add") {
-            self.attributes = {};
-            self.propertiesTable.properties = [];
             self.renderContent();
             return;
         }
 
-        // in view and edit modes, fetch entry then render the content
         self.model.fetch({
             success: function(model, response, options) {
-                self.attributes = _.clone(self.model.attributes);
-                self.propertiesTable.properties = self.attributes.properties;
                 self.renderContent();
             }
         });
@@ -952,57 +885,62 @@ var EntryPage = Page.extend({
         var self = this;
 
         if (self.mode == "add") {
-            // In add mode all fields are editable.
+            // Use empty attributes.
+            self.attributes = {};
+
+            // All fields are editable.
             self.$(".pki-fields input").each(function(index) {
                 var input = $(this);
                 input.removeAttr("readonly");
             });
 
         } else {
-            // In view and edit modes show entry ID in the title.
+            // Use entry attributes.
+            self.attributes = _.clone(self.model.attributes);
+
+            // Show entry ID in the title.
             self.$("span[name='id']").text(self.attributes.id);
 
-            // In view mode all fields are read-only. In edit mode
-            // currently all fields are read-only too, but it might
-            // need to be changed later to suppot editable fields.
-            self.$(".pki-fields input").each(function(index) {
-                var input = $(this);
-                input.attr("readonly", "readonly");
-            });
-        }
+            if (self.mode == "edit") {
+                // Show editable fields.
+                self.$(".pki-fields input").each(function(index) {
+                    var input = $(this);
+                    var name = input.attr("name");
+                    if (_.contains(self.editable, name)) {
+                        input.removeAttr("readonly");
+                    } else {
+                        input.attr("readonly", "readonly");
+                    }
+                });
 
-        var status = self.attributes.status;
-        if (status == "Disabled") {
-            self.enableLink.show();
-            self.disableLink.hide();
-        } else if (status == "Enabled") {
-            self.enableLink.hide();
-            self.disableLink.show();
+            } else { // self.mode == "view"
+                // All fields are read-only.
+                self.$(".pki-fields input").each(function(index) {
+                    var input = $(this);
+                    input.attr("readonly", "readonly");
+                });
+            }
         }
 
         if (self.mode == "view") {
             self.menu.show();
             self.buttons.hide();
-            self.propertiesButtons.hide();
 
         } else {
             self.menu.hide();
             self.buttons.show();
-            self.propertiesButtons.show();
         }
 
         self.$(".pki-fields input").each(function(index) {
             var input = $(this);
             self.renderField(input);
         });
-
-        self.propertiesTable.render();
     },
     renderField: function(input) {
         var self = this;
         var name = input.attr("name");
         var value = self.attributes[name];
-        if (!value) value = "";
+        if (value === undefined) value = "";
         input.val(value);
     },
     close: function() {
@@ -1017,22 +955,11 @@ var EntryPage = Page.extend({
     save: function() {
         var self = this;
 
-        self.$(".pki-fields input").each(function(index) {
-            var input = $(this);
-            self.saveField(input);
-        });
-
-        var attributes = {};
-        _.each(self.attributes, function(value, name) {
-            if (value == "") return;
-            attributes[name] = value;
-        });
-
-        attributes.properties = self.propertiesTable.properties;
+        self.saveFields();
 
         if (self.mode == "add") {
             // save new entry with POST
-            self.model.save(attributes, {
+            self.model.save(self.attributes, {
                 wait: true,
                 success: function(model, response, options) {
                     self.close();
@@ -1047,7 +974,7 @@ var EntryPage = Page.extend({
             });
         } else {
             // save changed attributes with PATCH
-            self.model.save(attributes, {
+            self.model.save(self.attributes, {
                 patch: true,
                 wait: true,
                 success: function(model, response, options) {
@@ -1063,24 +990,23 @@ var EntryPage = Page.extend({
             });
         }
     },
+    saveFields: function() {
+        var self = this;
+
+        self.$(".pki-fields input").each(function(index) {
+            var input = $(this);
+            self.saveField(input);
+        });
+    },
     saveField: function(input) {
         var self = this;
+
         var name = input.attr("name");
-        self.attributes[name] = input.val();
+        var value = input.val();
+        if (value == "") {
+            delete self.attributes[name];
+        } else {
+            self.attributes[name] = value;
+        }
     }
 });
-
-var AddEntryPage = EntryPage.extend({
-    initialize: function(options) {
-        var self = this;
-        options.mode = "add";
-        AddEntryPage.__super__.initialize.call(self, options);
-        self.parentPage = options.parentPage;
-    },
-    close: function() {
-        var self = this;
-        self.parentPage.$el.load(self.parentPage.url, function(response, status, xhr) {
-            self.parentPage.load();
-        });
-    }
-});;
\ No newline at end of file
diff --git a/base/tps-tomcat/shared/webapps/tps/js/authenticator.js b/base/tps-tomcat/shared/webapps/tps/js/authenticator.js
index 64da4b93809f331b79d3b1fd6eb092d4b75f3989..37ef595b9f3cabbf5baede79bfbb049d48e25d22 100644
--- a/base/tps-tomcat/shared/webapps/tps/js/authenticator.js
+++ b/base/tps-tomcat/shared/webapps/tps/js/authenticator.js
@@ -82,7 +82,7 @@ var AuthenticatorCollection = Collection.extend({
     }
 });
 
-var AuthenticatorsTable = Table.extend({
+var AuthenticatorsTable = ModelTable.extend({
     initialize: function(options) {
         var self = this;
         AuthenticatorsTable.__super__.initialize.call(self, options);
@@ -91,10 +91,10 @@ var AuthenticatorsTable = Table.extend({
     open: function(item) {
         var self = this;
 
-        var page = new EntryPage({
+        var page = new EntryWithPropertiesPage({
             el: self.parentPage.$el,
             url: "authenticator.html",
-            model: item.model
+            model: self.collection.get(item.entry.id)
         });
 
         page.open();
@@ -102,7 +102,7 @@ var AuthenticatorsTable = Table.extend({
     add: function() {
         var self = this;
 
-        var page = new AddEntryPage({
+        var page = new EntryWithPropertiesPage({
             el: self.parentPage.$el,
             url: "authenticator.html",
             model: new AuthenticatorModel(),
diff --git a/base/tps-tomcat/shared/webapps/tps/js/cert.js b/base/tps-tomcat/shared/webapps/tps/js/cert.js
index e1f8d53df6887188f73f2964893c42b34b3a4bfc..2ef0c3a63f697a7bf087d19739a535e90d41b45f 100644
--- a/base/tps-tomcat/shared/webapps/tps/js/cert.js
+++ b/base/tps-tomcat/shared/webapps/tps/js/cert.js
@@ -83,7 +83,7 @@ var CertificatePage = Page.extend({
             "keyType", "status", "createTime", "modifyTime"]
         });
 
-        var table = new Table({
+        var table = new ModelTable({
             el: $("table[name='certificates']"),
             collection: new CertificateCollection(),
             editDialog: editDialog
diff --git a/base/tps-tomcat/shared/webapps/tps/js/connection.js b/base/tps-tomcat/shared/webapps/tps/js/connection.js
index 83bd694a6980e39f3646cc83acf54038daf5b683..77d9c0a8d24e0cdd582392bcab1dd44d6ca9b94a 100644
--- a/base/tps-tomcat/shared/webapps/tps/js/connection.js
+++ b/base/tps-tomcat/shared/webapps/tps/js/connection.js
@@ -82,7 +82,7 @@ var ConnectionCollection = Collection.extend({
     }
 });
 
-var ConnectionsTable = Table.extend({
+var ConnectionsTable = ModelTable.extend({
     initialize: function(options) {
         var self = this;
         ConnectionsTable.__super__.initialize.call(self, options);
@@ -91,10 +91,10 @@ var ConnectionsTable = Table.extend({
     open: function(item) {
         var self = this;
 
-        var page = new EntryPage({
+        var page = new EntryWithPropertiesPage({
             el: self.parentPage.$el,
             url: "connection.html",
-            model: item.model
+            model: self.collection.get(item.entry.id)
         });
 
         page.open();
@@ -102,7 +102,7 @@ var ConnectionsTable = Table.extend({
     add: function() {
         var self = this;
 
-        var page = new AddEntryPage({
+        var page = new EntryWithPropertiesPage({
             el: self.parentPage.$el,
             url: "connection.html",
             model: new ConnectionModel(),
diff --git a/base/tps-tomcat/shared/webapps/tps/js/group.js b/base/tps-tomcat/shared/webapps/tps/js/group.js
index 95056860b0da8895148b585480692715f422149c..feaaa435d21583f3642700aafb4ab710fdaac53c 100644
--- a/base/tps-tomcat/shared/webapps/tps/js/group.js
+++ b/base/tps-tomcat/shared/webapps/tps/js/group.js
@@ -72,7 +72,7 @@ var GroupPage = Page.extend({
             actions: ["cancel", "save"]
         });
 
-        var table = new Table({
+        var table = new ModelTable({
             el: $("table[name='groups']"),
             collection: new GroupCollection(),
             addDialog: addDialog,
diff --git a/base/tps-tomcat/shared/webapps/tps/js/profile-mapping.js b/base/tps-tomcat/shared/webapps/tps/js/profile-mapping.js
index a3cea331dc91a95ecfa1a37dc35ee95e709e5b32..b7023faf1a27a6746d2e32f9eee74e203546b4b4 100644
--- a/base/tps-tomcat/shared/webapps/tps/js/profile-mapping.js
+++ b/base/tps-tomcat/shared/webapps/tps/js/profile-mapping.js
@@ -82,7 +82,7 @@ var ProfileMappingCollection = Collection.extend({
     }
 });
 
-var ProfileMappingsTable = Table.extend({
+var ProfileMappingsTable = ModelTable.extend({
     initialize: function(options) {
         var self = this;
         ProfileMappingsTable.__super__.initialize.call(self, options);
@@ -91,10 +91,10 @@ var ProfileMappingsTable = Table.extend({
     open: function(item) {
         var self = this;
 
-        var page = new EntryPage({
+        var page = new EntryWithPropertiesPage({
             el: self.parentPage.$el,
             url: "profile-mapping.html",
-            model: item.model
+            model: self.collection.get(item.entry.id)
         });
 
         page.open();
@@ -102,7 +102,7 @@ var ProfileMappingsTable = Table.extend({
     add: function() {
         var self = this;
 
-        var page = new AddEntryPage({
+        var page = new EntryWithPropertiesPage({
             el: self.parentPage.$el,
             url: "profile-mapping.html",
             model: new ProfileMappingModel(),
diff --git a/base/tps-tomcat/shared/webapps/tps/js/profile.js b/base/tps-tomcat/shared/webapps/tps/js/profile.js
index 719a7148828fef50d397f152a8ba410d5b01bbd3..707b4410b86224afaffc9aa222b2c5628c934067 100644
--- a/base/tps-tomcat/shared/webapps/tps/js/profile.js
+++ b/base/tps-tomcat/shared/webapps/tps/js/profile.js
@@ -82,7 +82,7 @@ var ProfileCollection = Collection.extend({
     }
 });
 
-var ProfilesTable = Table.extend({
+var ProfilesTable = ModelTable.extend({
     initialize: function(options) {
         var self = this;
         ProfilesTable.__super__.initialize.call(self, options);
@@ -91,10 +91,10 @@ var ProfilesTable = Table.extend({
     open: function(item) {
         var self = this;
 
-        var page = new EntryPage({
+        var page = new EntryWithPropertiesPage({
             el: self.parentPage.$el,
             url: "profile.html",
-            model: item.model
+            model: self.collection.get(item.entry.id)
         });
 
         page.open();
@@ -102,7 +102,7 @@ var ProfilesTable = Table.extend({
     add: function() {
         var self = this;
 
-        var page = new AddEntryPage({
+        var page = new EntryWithPropertiesPage({
             el: self.parentPage.$el,
             url: "profile.html",
             model: new ProfileModel(),
diff --git a/base/tps-tomcat/shared/webapps/tps/js/selftest.js b/base/tps-tomcat/shared/webapps/tps/js/selftest.js
index 39c0ceb37c737bd22d1890ad5960d5c898a90b86..89f18bcbf17a6ccc902c5da02f5c310debe22fc8 100644
--- a/base/tps-tomcat/shared/webapps/tps/js/selftest.js
+++ b/base/tps-tomcat/shared/webapps/tps/js/selftest.js
@@ -70,7 +70,7 @@ var SelfTestPage = Page.extend({
             readonly: ["id", "enabledAtStartup", "criticalAtStartup", "enabledOnDemand", "criticalOnDemand"]
         });
 
-        var table = new Table({
+        var table = new ModelTable({
             el: $("table[name='selftests']"),
             collection: new SelfTestCollection(),
             editDialog: editDialog
diff --git a/base/tps-tomcat/shared/webapps/tps/js/token.js b/base/tps-tomcat/shared/webapps/tps/js/token.js
index 9da95e622f5a92d96b4a8fbb140eb421759920c7..aa51c73b207deeb07feb0c77de4e1e0e8f96ab6e 100644
--- a/base/tps-tomcat/shared/webapps/tps/js/token.js
+++ b/base/tps-tomcat/shared/webapps/tps/js/token.js
@@ -93,7 +93,7 @@ var TokenPage = Page.extend({
             actions: ["cancel", "save"]
         });
 
-        var table = new Table({
+        var table = new ModelTable({
             el: $("table[name='tokens']"),
             collection: new TokenCollection(),
             addDialog: addDialog,
diff --git a/base/tps-tomcat/shared/webapps/tps/js/tps.js b/base/tps-tomcat/shared/webapps/tps/js/tps.js
new file mode 100644
index 0000000000000000000000000000000000000000..9c4792ebeb78e0226014147936882929bf7e9b7e
--- /dev/null
+++ b/base/tps-tomcat/shared/webapps/tps/js/tps.js
@@ -0,0 +1,171 @@
+/* --- BEGIN COPYRIGHT BLOCK ---
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ * All rights reserved.
+ * --- END COPYRIGHT BLOCK ---
+ *
+ * @author Endi S. Dewata
+ */
+
+var PropertiesTable = Table.extend({
+    initialize: function(options) {
+        var self = this;
+        options.columnMappings = {
+            id: "name"
+        };
+        PropertiesTable.__super__.initialize.call(self, options);
+    },
+    sort: function() {
+        var self = this;
+
+        // sort properties by name
+        self.filteredEntries = _.sortBy(self.filteredEntries, function(entry) {
+            return entry.name;
+        });
+    },
+    remove: function(items) {
+        var self = this;
+
+        // remove selected entries
+        self.entries = _.reject(self.entries, function(entry) {
+            return _.contains(items, entry.name);
+        });
+
+        // redraw table
+        self.render();
+    }
+});
+
+var EntryWithPropertiesPage = EntryPage.extend({
+    initialize: function(options) {
+        var self = this;
+        EntryWithPropertiesPage.__super__.initialize.call(self, options);
+        self.parentPage = options.parentPage;
+    },
+    setup: function() {
+        var self = this;
+
+        EntryWithPropertiesPage.__super__.setup.call(self);
+
+        self.enableLink = $("a[name='enable']", self.menu);
+        self.disableLink = $("a[name='disable']", self.menu);
+
+        self.enableLink.click(function(e) {
+            var message = "Are you sure you want to enable this entry?";
+            if (!confirm(message)) return;
+            self.model.enable({
+                success: function(data, textStatus, jqXHR) {
+                    self.attributes = _.clone(self.model.attributes);
+                    self.render();
+                },
+                error: function(jqXHR, textStatus, errorThrown) {
+                    alert("ERROR: " + textStatus);
+                }
+            });
+        });
+
+        self.disableLink.click(function(e) {
+            var message = "Are you sure you want to disable this entry?";
+            if (!confirm(message)) return;
+            self.model.disable({
+                success: function(data, textStatus, jqXHR) {
+                    self.attributes = _.clone(self.model.attributes);
+                    self.render();
+                },
+                error: function(jqXHR, textStatus, errorThrown) {
+                    alert("ERROR: " + textStatus);
+                }
+            });
+        });
+
+        var dialog = self.$("#property-dialog");
+
+        var addDialog = new Dialog({
+            el: dialog,
+            title: "Add Property",
+            actions: ["cancel", "add"]
+        });
+
+        var editDialog = new Dialog({
+            el: dialog,
+            title: "Edit Property",
+            readonly: ["name"],
+            actions: ["cancel", "save"]
+        });
+
+        var viewDialog = new Dialog({
+            el: dialog,
+            title: "Property",
+            readonly: ["name", "value"],
+            actions: ["close"]
+        });
+
+        var table = self.$("table[name='properties']");
+        self.addButton = $("button[name='add']", table);
+        self.removeButton = $("button[name='remove']", table);
+
+        self.propertiesTable = new PropertiesTable({
+            el: table,
+            addDialog: addDialog,
+            editDialog: editDialog,
+            viewDialog: viewDialog,
+            pageSize: 10
+        });
+    },
+    renderContent: function() {
+        var self = this;
+
+        EntryWithPropertiesPage.__super__.renderContent.call(self);
+
+        var status = self.attributes.status;
+        if (status == "Disabled") {
+            self.enableLink.show();
+            self.disableLink.hide();
+        } else if (status == "Enabled") {
+            self.enableLink.hide();
+            self.disableLink.show();
+        }
+
+        if (self.mode == "add") {
+            self.propertiesTable.mode = "edit";
+            self.propertiesTable.entries = [];
+
+        } else if (self.mode == "edit") {
+            self.propertiesTable.mode = "edit";
+            self.propertiesTable.entries = self.attributes.properties;
+
+        } else { // self.mode == "view"
+            self.propertiesTable.mode = "view";
+            self.propertiesTable.entries = self.attributes.properties;
+        }
+
+        self.propertiesTable.render();
+    },
+    saveFields: function() {
+        var self = this;
+
+        EntryWithPropertiesPage.__super__.saveFields.call(self);
+
+        self.attributes.properties = self.propertiesTable.entries;
+    },
+    close: function() {
+        var self = this;
+        if (self.parentPage) {
+            self.parentPage.open();
+        } else {
+            EntryWithPropertiesPage.__super__.close.call(self);
+        }
+    }
+});
diff --git a/base/tps-tomcat/shared/webapps/tps/js/user.js b/base/tps-tomcat/shared/webapps/tps/js/user.js
index 6be78fbabb14ff036bd9c068872e151e0ef8320c..326bd5374341f4585d5e0f0e87389bd4f8336a67 100644
--- a/base/tps-tomcat/shared/webapps/tps/js/user.js
+++ b/base/tps-tomcat/shared/webapps/tps/js/user.js
@@ -138,7 +138,7 @@ var UserPage = Page.extend({
             actions: ["cancel", "save"]
         });
 
-        var table = new Table({
+        var table = new ModelTable({
             el: $("table[name='users']"),
             collection: new UserCollection(),
             addDialog: addDialog,
diff --git a/base/tps-tomcat/shared/webapps/tps/ui/authenticator.html b/base/tps-tomcat/shared/webapps/tps/ui/authenticator.html
index c5be7dfd939dc718f8d69c32cc7524c6b31dfcdf..4c36552eba21f558539e03a516608b636981c12f 100644
--- a/base/tps-tomcat/shared/webapps/tps/ui/authenticator.html
+++ b/base/tps-tomcat/shared/webapps/tps/ui/authenticator.html
@@ -71,7 +71,7 @@
     <tr>
         <td class="pki-select-column"><input id="authenticator_select" type="checkbox"><label for="authenticator_select">&nbsp;</label></td>
         <td name="id" class="pki-property-name-column"><a href="#">&nbsp;</a></td>
-        <td name="status">&nbsp;</td>
+        <td name="value">&nbsp;</td>
     </tr>
 </tbody>
 <tfoot>
diff --git a/base/tps-tomcat/shared/webapps/tps/ui/connection.html b/base/tps-tomcat/shared/webapps/tps/ui/connection.html
index 8c24cc72a011edcbd3a7be490156accb0e6ffa05..ad8e64836399eff720bef6e9c8cdd61da59eff17 100644
--- a/base/tps-tomcat/shared/webapps/tps/ui/connection.html
+++ b/base/tps-tomcat/shared/webapps/tps/ui/connection.html
@@ -71,7 +71,7 @@
     <tr>
         <td class="pki-select-column"><input id="connection_select" type="checkbox"><label for="connection_select">&nbsp;</label></td>
         <td name="id" class="pki-property-name-column"><a href="#">&nbsp;</a></td>
-        <td name="status">&nbsp;</td>
+        <td name="value">&nbsp;</td>
     </tr>
 </tbody>
 <tfoot>
diff --git a/base/tps-tomcat/shared/webapps/tps/ui/index.html b/base/tps-tomcat/shared/webapps/tps/ui/index.html
index d49e267652c7a7ca8ad57ad81d51bae7a42b0504..1eda85dad6830861dc850119b42477c939bb50ea 100644
--- a/base/tps-tomcat/shared/webapps/tps/ui/index.html
+++ b/base/tps-tomcat/shared/webapps/tps/ui/index.html
@@ -25,6 +25,7 @@
     <script src="/pki/js/backbone.js"></script>
     <script src="/pki/js/bootstrap.js"></script>
     <script src="/pki/js/pki-ui.js"></script>
+    <script src="/tps/js/tps.js"></script>
     <script src="/tps/js/account.js"></script>
     <script src="/tps/js/activity.js"></script>
     <script src="/tps/js/authenticator.js"></script>
@@ -38,22 +39,10 @@
     <script src="/tps/js/user.js"></script>
     <script>
 $(function() {
-    var content = $("#content");
     var account = new Account();
-    new Navigation({
+
+    var navigation = new Navigation({
         el: $("#navigation"),
-        pages: {
-            activities: new ActivityPage({ el: content, url: "activities.html" }),
-            authenticators: new AuthenticatorsPage({ el: content, url: "authenticators.html" }),
-            certs: new CertificatePage({ el: content, url: "certs.html" }),
-            connections: new ConnectionsPage({ el: content, url: "connections.html" }),
-            groups: new GroupPage({ el: content, url: "groups.html" }),
-            profiles: new ProfilesPage({ el: content, url: "profiles.html" }),
-            profileMappings: new ProfileMappingsPage({ el: content, url: "profile-mappings.html" }),
-            selftests: new SelfTestPage({ el: content, url: "selftests.html" }),
-            tokens: new TokenPage({ el: content, url: "tokens.html" }),
-            users: new UserPage({ el: content, url: "users.html" })
-        },
         logout: function() {
             account.logout({
                 success: function() {
@@ -67,6 +56,60 @@ $(function() {
         },
         homePage: "tokens"
     });
+
+    var content = $("#content");
+
+    navigation.page("activities", new ActivityPage({
+        el: content,
+        url: "activities.html"
+    }));
+
+    navigation.page("authenticators", new AuthenticatorsPage({
+        el: content,
+        url: "authenticators.html"
+    }));
+
+    navigation.page("certs", new CertificatePage({
+        el: content,
+        url: "certs.html"
+    }));
+
+    navigation.page("connections", new ConnectionsPage({
+        el: content,
+        url: "connections.html"
+    }));
+
+    navigation.page("groups", new GroupPage({
+        el: content,
+        url: "groups.html"
+    }));
+
+    navigation.page("profiles", new ProfilesPage({
+        el: content,
+        url: "profiles.html"
+    }));
+
+    navigation.page("profileMappings", new ProfileMappingsPage({
+        el: content,
+        url: "profile-mappings.html"
+    }));
+
+    navigation.page("selftests", new SelfTestPage({
+        el: content,
+        url: "selftests.html"
+    }));
+
+    navigation.page("tokens", new TokenPage({
+        el: content,
+        url: "tokens.html"
+    }));
+
+    navigation.page("users", new UserPage({
+        el: content,
+        url: "users.html"
+    }));
+
+    navigation.render();
 });
     </script>
 </head>
diff --git a/base/tps-tomcat/shared/webapps/tps/ui/profile-mapping.html b/base/tps-tomcat/shared/webapps/tps/ui/profile-mapping.html
index 4bd76b7313fb725fce70bb47f79487a30903680b..2af535bcebf103ec26e7829458aad8e26783122d 100644
--- a/base/tps-tomcat/shared/webapps/tps/ui/profile-mapping.html
+++ b/base/tps-tomcat/shared/webapps/tps/ui/profile-mapping.html
@@ -71,7 +71,7 @@
     <tr>
         <td class="pki-select-column"><input id="profile_select" type="checkbox"><label for="profile_select">&nbsp;</label></td>
         <td name="id" class="pki-property-name-column"><a href="#">&nbsp;</a></td>
-        <td name="status">&nbsp;</td>
+        <td name="value">&nbsp;</td>
     </tr>
 </tbody>
 <tfoot>
diff --git a/base/tps-tomcat/shared/webapps/tps/ui/profile.html b/base/tps-tomcat/shared/webapps/tps/ui/profile.html
index 51f9bf6c0b1b205da95631ddde51c8deb76f393c..4cd2f1dba12e9d574034c9faf2251df55a7c1cd4 100644
--- a/base/tps-tomcat/shared/webapps/tps/ui/profile.html
+++ b/base/tps-tomcat/shared/webapps/tps/ui/profile.html
@@ -71,7 +71,7 @@
     <tr>
         <td class="pki-select-column"><input id="profile_select" type="checkbox"><label for="profile_select">&nbsp;</label></td>
         <td name="id" class="pki-property-name-column"><a href="#">&nbsp;</a></td>
-        <td name="status">&nbsp;</td>
+        <td name="value">&nbsp;</td>
     </tr>
 </tbody>
 <tfoot>
-- 
1.8.4.2

