/* --- 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) 2013 Red Hat, Inc. * All rights reserved. * --- END COPYRIGHT BLOCK --- * * @author Endi S. Dewata */ var PKI = { substitute: function(content, map) { var newContent = ""; // substitute ${attribute} with attribute value var pattern = /\${([^}]*)}/; while (content.length) { // search for ${attribute} pattern var index = content.search(pattern); if (index < 0) { newContent += content; break; } var name = RegExp.$1; var value = map[name]; // replace pattern occurrence with attribute value newContent += content.substring(0, index) + (value === undefined ? "" : value); // process the remaining content content = content.substring(index + name.length + 3); } return newContent; }, logout: function(options) { options = options || {}; if (window.crypto && typeof window.crypto.logout === "function") { // Firefox window.crypto.logout(); if (options.success) options.success.call(); } else { var result = document.execCommand("ClearAuthenticationCache", false); if (result) { // IE if (options.success) options.success.call(); } else { // logout not supported if (options.error) options.error.call(); } } } }; var Model = Backbone.Model.extend({ parseResponse: function(response) { return response; }, parse: function(response, options) { return this.parseResponse(response); }, createRequest: function(attributes) { return attributes; }, save: function(attributes, options) { var self = this; if (attributes == undefined) attributes = self.attributes; // convert attributes into JSON request var request = self.createRequest(attributes); // remove old attributes if (self.isNew()) self.clear(); // send JSON request Model.__super__.save.call(self, request, options); } }); var Collection = Backbone.Collection.extend({ urlRoot: null, initialize: function(options) { var self = this; self.options = options; self.links = {}; self.query({}); }, url: function() { return this.currentURL; }, parse: function(response) { var self = this; // get total entries self.total = self.getTotal(response); // parse links var links = self.getLinks(response); links = links == undefined ? [] : [].concat(links); self.parseLinks(links); // convert entries into models var models = []; var entries = self.getEntries(response); entries = entries == undefined ? [] : [].concat(entries); _(entries).each(function(entry) { var model = self.parseEntry(entry); models.push(model); }); return models; }, getTotal: function(response) { return response.total; }, getEntries: function(response) { return null; }, getLinks: function(response) { return null; }, parseEntry: function(entry) { return null; }, parseLinks: function(links) { var self = this; self.links = {}; _(links).each(function(link) { var name = link.rel; var href = link.href; self.links[name] = href; }); }, link: function(name) { return this.links[name]; }, go: function(name) { var self = this; if (self.links[name] == undefined) return; self.currentURL = self.links[name]; }, query: function(params) { var self = this; // add default options into the params _.defaults(params, self.options); // generate query string var query = ""; _(params).each(function(value, name) { // skip null or empty string, but don't skip 0 if (value === null || value === "") return; query = query == "" ? "?" : query + "&"; query = query + name + "=" + encodeURIComponent(value); }); self.currentURL = self.urlRoot + query; } }); var Page = Backbone.View.extend({ initialize: function(options) { var self = this; Page.__super__.initialize.call(self, options); self.url = options.url; }, open: function() { var self = this; // load template self.$el.load(self.url, function(response, status, xhr) { // load content self.load(); }); }, load: function() { } }); var Dialog = Backbone.View.extend({ initialize: function(options) { var self = this; Dialog.__super__.initialize.call(self, options); self.body = self.$(".modal-body"); self.title = options.title; self.readonly = options.readonly; // by default all fields are editable if (self.readonly == undefined) self.readonly = []; self.actions = options.actions; if (self.actions == undefined) { // by default all buttons are active self.actions = []; self.$(".modal-footer button").each(function(index) { var button = $(this); var action = button.attr("name"); self.actions.push(action); }); } self.handlers = {}; // add default handlers self.handlers["cancel"] = function() { self.close(); }; self.handlers["close"] = function() { self.close(); }; self.$el.modal({ show: false }); }, render: function() { var self = this; if (self.title) { self.$(".modal-title").text(self.title); } // setup input fields // TODO: handle drop-down lists $("input, textarea", self.body).each(function(index) { var input = $(this); var name = input.attr("name"); if (_.contains(self.readonly, name)) { input.attr("readonly", "readonly"); } else { input.removeAttr("readonly"); } }); // setup buttons self.$(".modal-footer button").each(function(index) { var button = $(this); var action = button.attr("name"); if (_.contains(self.actions, action)) { // enable buttons for specified actions button.show(); button.click(function(e) { var handler = self.handlers[action]; handler.call(self); e.preventDefault(); }); } else { // hide unused buttons button.hide(); } }); self.load(); }, handler: function(name, handler) { var self = this; self.handlers[name] = handler; }, open: function() { var self = this; self.render(); self.$el.modal("show"); }, close: function() { var self = this; self.$el.modal("hide"); // remove event handlers self.$(".modal-footer button").each(function(index) { var button = $(this); button.off("click"); }); self.trigger("close"); }, load: function() { var self = this; // load input fields $("input, select, textarea", self.body).each(function(index) { var input = $(this); self.loadField(input); }); }, loadField: function(input) { var self = this; var name = input.attr("name"); var value = self.entry[name]; if (value === undefined) value = ""; input.val(value); }, save: function() { var self = this; // save input fields self.$(".modal-body input").each(function(index) { var input = $(this); self.saveField(input); }); // save drop-down lists self.$(".modal-body select").each(function(index) { var input = $(this); self.saveField(input); }); }, saveField: function(input) { var self = this; var name = input.attr("name"); var value = input.val(); self.entry[name] = value; } }); var ErrorDialog = Backbone.View.extend({ initialize: function(options) { var self = this; ErrorDialog.__super__.initialize.call(self, options); self.title = options.title; self.content = options.content; }, render: function() { var self = this; if (self.title) { self.$(".modal-title").text(self.title); } if (self.content) { self.$("span[name=content]").html(self.content); } self.$("button[name=close]").click(function(e) { self.close(); e.preventDefault(); }); }, open: function() { var self = this; self.render(); self.$el.show(); }, close: function() { var self = this; self.$el.hide(); } }); var TableItem = Backbone.View.extend({ initialize: function(options) { var self = this; TableItem.__super__.initialize.call(self, options); self.table = options.table; self.reset(); }, reset: function() { var self = this; $("td", self.$el).each(function(index) { var td = $(this); var name = td.attr("name"); if (td.hasClass("pki-select-column")) { // uncheck checkbox and reset the value var checkbox = $("input[type='checkbox']", td); checkbox.attr("checked", false); checkbox.val(""); // hide checkbox by hiding the label $("label", td).hide(); } else { // empty the content td.html(" "); } }); }, render: function() { var self = this; var template = self.table.template; var templateCheckbox = $("input[type='checkbox']", template); var prefix = templateCheckbox.attr("id") + "-"; var templateTDs = $("td", self.table.template); $("td", self.$el).each(function(index) { var td = $(this); var name = td.attr("name"); var templateTD = $(templateTDs[index]); if (td.hasClass("pki-select-column")) { // generate a unique input ID based on entry ID var entryID = self.get("id") var inputID = prefix + entryID; // set the checkbox ID and value var checkbox = $("input[type='checkbox']", td); checkbox.attr("id", inputID); checkbox.attr("checked", false); checkbox.val(entryID); // point the label to the checkbox and make it visible var label = $("label", td); label.attr("for", inputID); label.show(); } else { self.renderColumn(td, templateTD); } }); }, isSelected: function() { var self = this; var checkbox = $("td.pki-select-column input", self.$el); // skip blank rows var value = checkbox.val(); if (value == "") return false; return checkbox.prop("checked"); }, get: function(name) { var self = this; var attribute = self.table.columnMappings[name] || name; return self.entry[attribute]; }, renderColumn: function(td, templateTD) { var self = this; // copy content from template var content = templateTD.html(); var newContent = ""; // substitute ${attribute} with attribute value var pattern = /\${([^}]*)}/; while (content.length) { // search for ${attribute} pattern var index = content.search(pattern); if (index < 0) { newContent += content; break; } // get attribute name var fullName = RegExp.$1; // split attribute names var names = fullName.split("."); // get the value from the leaf object var value; for (var i=0; i= 0) matches = true; }); return matches; }, renderControls: function() { var self = this; if (self.mode == "view") { self.addButton.hide(); self.removeButton.hide(); } else { // self.mode == "edit" self.addButton.show(); self.removeButton.show(); } // 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(); } }, getSelectedRows: function() { var self = this; return _.filter(self.items, function(item) { return item.isSelected(); }); }, 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.entry); // redraw table self.render(); dialog.close(); }); } dialog.entry = _.clone(item.entry); dialog.open(); }, add: function() { var self = this; var dialog = self.addDialog; dialog.entry = {}; dialog.handler("add", function() { // save new entry dialog.save(); self.addEntry(dialog.entry); // redraw table self.render(); dialog.close(); }); dialog.open(); }, addEntry: function(entry) { var self = this; self.entries.push(entry); }, 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; // if collection is undefined, don't fetch data, just draw the controls if (!self.collection) { self.renderControls(); return; } // set query based on current page, page size, and filter self.collection.query({ start: (self.page - 1) * self.pageSize, size: self.pageSize, filter: self.searchField.val() }); // fetch data based on query self.collection.fetch({ reset: true, success: function(collection, response, options) { // update controls self.renderControls(); // display entries _(self.items).each(function(item, index) { self.renderRow(item, index); }); }, error: function(collection, response, options) { new ErrorDialog({ el: $("#error-dialog"), title: "HTTP Error " + response.responseJSON.Code, content: response.responseJSON.Message }).open(); } }); }, renderRow: function(item, index) { var self = this; if (index < self.collection.length) { // show entry in existing row var model = self.collection.at(index); item.entry = _.clone(model.attributes); item.render(); } else { // clear unused row item.reset(); } }, totalEntries: function() { var self = this; return self.collection.total; }, open: function(item) { var self = this; var model = self.collection.get(item.entry.id); var dialog = self.editDialog; dialog.entry = item.entry; dialog.handler("save", function() { // save attribute changes dialog.save(); model.set(dialog.entry); // if nothing has changed, return var changedAttributes = model.changedAttributes(); if (!changedAttributes) return; // save changed attributes with PATCH model.save(changedAttributes, { patch: true, wait: true, success: function(model, response, options) { // redraw table after saving entries self.render(); dialog.close(); }, error: function(model, response, options) { if (response.status == 200) { // redraw table after saving entries self.render(); dialog.close(); return; } new ErrorDialog({ el: $("#error-dialog"), title: "HTTP Error " + response.responseJSON.Code, content: response.responseJSON.Message }).open(); } }); }); // load data from server model.fetch({ success: function(model, response, options) { dialog.open(); }, error: function(model, response, options) { new ErrorDialog({ el: $("#error-dialog"), title: "HTTP Error " + response.responseJSON.Code, content: response.responseJSON.Message }).open(); } }); }, add: function() { var self = this; var dialog = self.addDialog; var model = self.collection.model.call(self.collection); dialog.entry = _.clone(model.attributes); dialog.handler("add", function() { // save new attributes dialog.save(); var entry = {}; _.each(dialog.entry, function(value, key) { if (value == "") return; entry[key] = value; }); // save new entry with POST model.save(entry, { wait: true, success: function(model, response, options) { // redraw table after adding new entry self.render(); dialog.close(); }, error: function(model, response, options) { if (response.status == 201) { // redraw table after adding new entry self.render(); dialog.close(); return; } new ErrorDialog({ el: $("#error-dialog"), title: "HTTP Error " + response.responseJSON.Code, content: response.responseJSON.Message }).open(); } }); }); dialog.open(); }, remove: function(items) { var self = this; // remove selected entries _.each(items, function(id, index) { var model = self.collection.get(id); model.destroy({ wait: true, success: function(model, response, options) { self.render(); }, error: function(model, response, options) { new ErrorDialog({ el: $("#error-dialog"), title: "HTTP Error " + response.responseJSON.Code, content: response.responseJSON.Message }).open(); } }); }); } }); 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.title = options.title; self.editable = options.editable || []; self.parentPage = options.parentPage; self.parentHash = options.parentHash; }, load: function() { var self = this; self.setup(); self.render(); }, setup: function() { var self = this; self.actions = self.$(".pki-actions"); self.viewMenu = $(".pki-actions-menu[name='view']", self.actions); self.editAction = $("[name='edit']", self.viewMenu); self.editMenu = $(".pki-actions-menu[name='edit']", self.actions); self.cancelAction = $("[name='cancel']", self.editMenu); self.saveAction = $("[name='save']", self.editMenu); self.idField = self.$("input[name='id']"); self.statusField = self.$("input[name='status']"); $("a", self.editAction).click(function(e) { self.mode = "edit"; self.render(); e.preventDefault(); }); self.cancelAction.click(function(e) { self.cancel(); e.preventDefault(); }); self.saveAction.click(function(e) { self.save(); e.preventDefault(); }); }, render: function() { var self = this; if (self.mode == "add") { self.renderContent(); return; } self.model.fetch({ success: function(model, response, options) { self.renderContent(); } }); }, renderContent: function() { var self = this; if (self.mode == "add") { // Use blank entry. self.entry = {}; // Replace title. self.$("span[name='title']").text(self.title); } else { // Use fetched entry. self.entry = _.clone(self.model.attributes); // Update title with entry attributes. self.$("span[name='title']").each(function() { var title = $(this); var text = title.text(); title.text(PKI.substitute(text, self.entry)); }); } if (self.mode == "view") { // All fields are read-only. self.$(".pki-fields input").each(function(index) { var input = $(this); input.attr("readonly", "readonly"); }); self.viewMenu.show(); self.editMenu.hide(); } else { // 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"); } }); self.viewMenu.hide(); self.editMenu.show(); } self.$(".pki-fields input").each(function(index) { var input = $(this); self.loadField(input); }); }, loadField: function(input) { var self = this; var name = input.attr("name"); var value = self.entry[name]; if (value === undefined) value = ""; if (value instanceof Date) value = value.toUTCString(); input.val(value); }, close: function() { var self = this; if (self.parentHash) { window.location.hash = self.parentHash; } else if (self.parentPage) { self.parentPage.open(); } else { self.mode = "view"; self.render(); } }, cancel: function() { var self = this; self.close(); }, save: function() { var self = this; self.saveFields(); if (self.mode == "add") { // save new entry with POST self.model.save(self.entry, { wait: true, success: function(model, response, options) { self.close(); }, error: function(model, response, options) { if (response.status == 201) { self.close(); return; } new ErrorDialog({ el: $("#error-dialog"), title: "HTTP Error " + response.responseJSON.Code, content: response.responseJSON.Message }).open(); } }); } else { // save changed entry with PATCH self.model.save(self.entry, { patch: true, wait: true, success: function(model, response, options) { self.close(); }, error: function(model, response, options) { if (response.status == 200) { self.close(); return; } new ErrorDialog({ el: $("#error-dialog"), title: "HTTP Error " + response.responseJSON.Code, content: response.responseJSON.Message }).open(); } }); } }, 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"); var value = input.val(); // save all values including empty ones self.entry[name] = value; } });