summaryrefslogtreecommitdiffstats
path: root/install/ui/src/freeipa/navigation
diff options
context:
space:
mode:
authorPetr Vobornik <pvoborni@redhat.com>2012-12-14 16:39:20 +0100
committerPetr Vobornik <pvoborni@redhat.com>2013-05-06 16:22:17 +0200
commit693dc560620d52dc24a0ab89e20147b10ed4f469 (patch)
treea748bc7a89f76d8ea4cdf1dc1a91895192a7ea56 /install/ui/src/freeipa/navigation
parenta4d9e19c79b60b8f7269141374b2e3b6c0d66c45 (diff)
downloadfreeipa-693dc560620d52dc24a0ab89e20147b10ed4f469.tar.gz
freeipa-693dc560620d52dc24a0ab89e20147b10ed4f469.tar.xz
freeipa-693dc560620d52dc24a0ab89e20147b10ed4f469.zip
Menu and application controller refactoring
https://fedorahosted.org/freeipa/ticket/3235 https://fedorahosted.org/freeipa/ticket/3236
Diffstat (limited to 'install/ui/src/freeipa/navigation')
-rw-r--r--install/ui/src/freeipa/navigation/Menu.js245
-rw-r--r--install/ui/src/freeipa/navigation/Router.js337
-rw-r--r--install/ui/src/freeipa/navigation/menu_spec.js107
3 files changed, 689 insertions, 0 deletions
diff --git a/install/ui/src/freeipa/navigation/Menu.js b/install/ui/src/freeipa/navigation/Menu.js
new file mode 100644
index 000000000..7b1a0ecce
--- /dev/null
+++ b/install/ui/src/freeipa/navigation/Menu.js
@@ -0,0 +1,245 @@
+/* Authors:
+ * Petr Vobornik <pvoborni@redhat.com>
+ *
+ * Copyright (C) 2012 Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+*/
+
+
+define(['dojo/_base/declare',
+ 'dojo/store/Memory',
+ 'dojo/_base/array',
+ 'dojo/_base/lang',
+ 'dojo/store/Observable',
+ 'dojo/Evented',
+ '../_base/i18n',
+ '../ipa' // TODO: remove dependance
+ ], function(declare, Memory_store, array, lang, Observable, Evented, i18n, IPA) {
+
+/**
+ * Menu store
+ *
+ * Maintains menu hierarchy and selection state.
+ */
+return declare([Evented], {
+
+ /**
+ * Following properties can be specified in menu item spec:
+ * @property {String} name
+ * @property {String} label
+ * @property {String} title
+ * @property {Number} position: position among siblings
+ * @property {menu_item_spec_array} children
+ * @property {String} entity: entity name
+ * @property {String} facet: facet name
+ * @property {Boolean} hidden: menu item is no visible, but can serve for
+ * other evaluations (nested entities)
+ *
+ * Following properties are not in created menu item:
+ * - children
+ *
+ *
+ * Following properties can be stored in menu item at runtime:
+ *
+ * @property {Boolean} selected
+ * @property {String} parent: name of parent menu item
+ * @property {String} selected_child: last selected child. Can be set even
+ * if the child is not selected
+ *
+ */
+
+ /**
+ * Menu name
+ * @type String
+ */
+ name: null,
+
+ /**
+ * Dojo Store of menu items
+ * @type: Store
+ */
+ items: null,
+
+ /**
+ * Delimiter used in name creation
+ * To avoid having multiple menu items with the same name.
+ * @type String
+ */
+ path_delimiter: '/',
+
+ /**
+ * Selected menu items
+ * @type Array of menu items
+ */
+ selected: [],
+
+ /**
+ * Default search options: sort by position
+ *
+ * @type Object
+ */
+ search_options: { sort: [{attribute:'position'}]},
+
+ /**
+ * Takes a spec of menu item.
+ * Normalizes item's name, parent, adds children if specified
+ *
+ * @param {menu_item} items
+ * @param {String|menu_item} parent
+ * @param {Object} options
+ */
+ add_item: function(item, parent, options ) {
+
+ item = lang.clone(item); //don't modify original spec
+
+ // each item must have a name and a label
+ // FIXME: consider to move entity and facet stuff outside of this object
+ item.name = item.name || item.facet || item.entity;
+ if (!name) throw 'Missing menu item property: name';
+ if (item.label) item.label = i18n.message(item.label);
+ if (item.title) item.title = i18n.message(item.title);
+
+ if (item.entity) {
+ // FIXME: replace with 'entities' module in future
+ var entity = IPA.get_entity(item.entity);
+ if (!entity) return; //quit
+ //item.name = entity.name;
+ if (!item.label) item.label = entity.label;
+ if (!item.title) item.title = entity.title;
+ } //else if (item.facet) {
+ // TODO: uncomment when facet repository implemented
+// var facet = facets.(item.facet);
+// item.name = facet.name;
+// if (!item.label) item.label = facet.label;
+// if (!item.title) item.title = facet.title;
+// }
+
+ item.selected = false;
+
+ // check parent
+ if (typeof parent === 'string') {
+ parent = this.items.get(parent);
+ if (!parent) throw 'Menu item\'s parent doesn\t exist';
+ } else if (typeof parent === 'object') {
+ if (!this.items.getIdentity(parent)) {
+ throw 'Supplied parent isn\'t menu item';
+ }
+ }
+
+ var parent_name = parent ? parent.name : null;
+ var siblings = this.items.query({ parent: parent_name });
+ if (!item.position) item.position = siblings.total;
+ // TODO: add reordering of siblings when position set
+
+ item.parent = parent_name;
+ if (parent) {
+ // names have to be unique
+ item.name = parent.name + this.path_delimiter + item.name;
+ }
+
+ // children will be added separatelly
+ var children = item.children;
+ delete item.children;
+
+ // finally add the item
+ this.items.add(item);
+
+ // add children
+ if (children) {
+ array.forEach(children, function(child) {
+ this.add_item(child, item);
+ }, this);
+ }
+ },
+
+ add_items: function(/* Array */ items) {
+ array.forEach(items, function(item) {
+ this.add_item(item);
+ }, this);
+ },
+
+ /**
+ * Query internal data store by using default search options.
+ *
+ * @param Object Query filter
+ * @return QueryResult
+ */
+ query: function(query) {
+ return this.items.query(query, this.search_options);
+ },
+
+ /**
+ * Marks item and all its parents as selected.
+ */
+ _select: function(item) {
+
+ item.selected = true;
+ this.selected.push(item);
+ this.items.put(item);
+
+ if (item.parent) {
+ var parent = this.items.get(item.parent);
+ this._select(parent);
+ }
+ },
+
+ /**
+ * Selects a menu item and all it's ancestors.
+ * @param {string|menu_item} Menu item to select
+ */
+ select: function(item) {
+
+ if (typeof item == 'string') {
+ item = this.items.get(item);
+ }
+
+ // FIXME: consider to raise an exception
+ if (!item || !this.items.getIdentity(item)) return false;
+
+ // deselect previous
+ var old_selection = lang.clone(this.selected);
+ array.forEach(this.selected, function(mi) {
+ mi.selected = false;
+ this.items.put(mi);
+ }, this);
+ this.selected = [];
+
+ // select new
+ this._select(item);
+
+ var select_state = {
+ item: item,
+ new_selection: this.selected,
+ old_selection: old_selection
+ };
+
+ this.emit('selected', select_state);
+ return select_state;
+ },
+
+ constructor: function(spec) {
+ spec = spec || {};
+ this.items = new Observable( new Memory_store({
+ idProperty: 'name'
+ }));
+
+ spec = lang.clone(spec);
+ this.add_items(spec.items || []);
+ delete spec.items;
+ declare.safeMixin(this, spec);
+ }
+}); //declare freeipa.menu
+}); //define \ No newline at end of file
diff --git a/install/ui/src/freeipa/navigation/Router.js b/install/ui/src/freeipa/navigation/Router.js
new file mode 100644
index 000000000..286ac4634
--- /dev/null
+++ b/install/ui/src/freeipa/navigation/Router.js
@@ -0,0 +1,337 @@
+/* Authors:
+ * Petr Vobornik <pvoborni@redhat.com>
+ *
+ * Copyright (C) 2012 Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+*/
+
+define(['dojo/_base/declare',
+ 'dojo/router',
+ 'dojo/_base/lang',
+ 'dojo/_base/array',
+ 'dojo/io-query',
+ 'dojo/topic',
+ '../entities',
+ '../facets',
+ '../ipa' //TODO: remove dependancy
+ ],
+ function(declare, router, lang, array, ioquery, topic, entities, facets, IPA) {
+
+ /**
+ * Class navigation
+ * This component keeps menu and routes in sync. It signalizes
+ * other components to display facet by sending 'show-facet' event.
+ * Other components can use navigate_to_* methods to change currently
+ * displayed facet. This change can be canceled in 'facet-change'
+ * event handler.
+ */
+ var navigation = declare(null, {
+
+ /**
+ * Holds references to register route handlers.
+ * Can be used for unregistering routes.
+ * @type Array
+ */
+ route_handlers: [],
+
+ /**
+ * Prefix of all routes for this navigation. Useful for multiple
+ * navigation objects in one application.
+ * @type String
+ */
+ route_prefix: '',
+
+ /**
+ * Variations of entity routes
+ */
+ entity_routes: [
+ '/e/:entity/:facet/:pkeys/*args',
+ '/e/:entity/:facet//*args',
+ '/e/:entity/:facet/:pkeys',
+ '/e/:entity/:facet',
+ '/e/:entity'
+ ],
+
+ /**
+ * Variations of simple page routes
+ */
+ page_routes: [
+ '/p/:page/*args',
+ '/p/:page'
+ ],
+
+ /**
+ * Used during facet changing. Set it to true in 'facet-change'
+ * event handler to stop the change.
+ * @type Boolean
+ */
+ canceled: false,
+
+ /**
+ * Flag to indicate that next hash change should not invoke showing a
+ * facet.
+ * Main purpose: updating hash.
+ * @type Boolen
+ */
+ ignore_next: false,
+
+
+ /**
+ * Register a route-handler pair to a dojo.router
+ * Handler will be run in context of this object
+ *
+ * @param {string|array} route or routes to register
+ * @param {function} handler to be associated with the route(s)
+ */
+ register_route: function(route, handler) {
+ // TODO: add multiple routes for one handler
+ route = this.route_prefix + route;
+ this.route_handlers.push(router.register(route, lang.hitch(this, handler)));
+ },
+
+ /**
+ * Initializates router
+ * - registers handlers
+ */
+ init_router: function() {
+
+ // entity pages
+ array.forEach(this.entity_routes, function(route) {
+ this.register_route(route, this.entity_route_handler);
+ }, this);
+
+ // special pages
+ this.register_route(this.page_routes, this.page_route_handler);
+ },
+
+ /**
+ * Handler for entity routes
+ * Shouldn't be invoked directly.
+ */
+ entity_route_handler: function(event) {
+
+ if (this.check_clear_ignore()) return;
+
+ var entity_name = event.params.entity;
+ var facet_name = event.params.facet;
+ var pkeys = this._decode_pkeys(event.params.pkeys || '');
+ var args = ioquery.queryToObject(event.params.args || '');
+ args.pkeys = pkeys;
+
+ // set new facet state
+ //var entity = entities.get(entity_name);
+ var entity = IPA.get_entity(entity_name); // TODO: replace with prev line
+ var facet = entity.get_facet(facet_name);
+ facet.set_state(args);
+
+ this.show_facet(facet);
+ },
+
+ /**
+ * General facet route handler
+ * Shouldn't be invoked directly.
+ */
+ page_route_handler: function(event) {
+
+ if (this.check_clear_ignore()) return;
+
+ var facet_name = event.params.page;
+ var args = ioquery.queryToObject(event.params.args || '');
+
+// // Find menu item
+// var items = this.menu.items.query({ page: facet_name });
+//
+// // Select menu item
+// if (items.total > 0) {
+// this.menu.select(items[items.total-1]);
+// }
+
+ // set new facet state
+ var facet = facets.get(facet_name);
+ facet.set_state(args);
+
+ this.show_facet(facet);
+ },
+
+ /**
+ * Used for switching to entitie's facets. Current target facet
+ * state is used as params (pkeys, args) when none of pkeys and args
+ * are used (useful for switching to previous page with keeping the context).
+ */
+ navigate_to_entity_facet: function(entity_name, facet_name, pkeys, args) {
+
+ //var entity = entities.get(entity_name);
+ var entity = IPA.get_entity(entity_name); // TODO: replace with prev line
+ var facet = entity.get_facet(facet_name);
+
+ if (!facet) return false; // TODO: maybe replace with exception
+
+ // Use current state if none supplied
+ if (!pkeys && !args) {
+ args = facet.get_state();
+ }
+ args = args || {};
+
+ // Facets may be nested and require more pkeys than supplied.
+ args.pkeys = facet.get_pkeys(pkeys);
+
+ var hash = this._create_entity_facet_hash(facet, args);
+ return this.navigate_to_hash(hash, facet);
+ },
+
+ /**
+ * Navigate to other facet.
+ */
+ navigate_to_facet: function(facet_name, args) {
+
+ // TODO: uncoment when `facets` are implemented
+// var facet = facets.get(facet_name);
+// if (!args) args = facet.get_args();
+// var hash = this._create_facet_hash(facet, { args: args });
+// return this.navigate_to_hash(hash, facet);
+ },
+
+ /**
+ * Low level function.
+ *
+ * Public usage should be limited reinitializing canceled navigations.
+ */
+ navigate_to_hash: function(hash, facet) {
+
+ this.canceled = false;
+ topic.publish('facet-change', { facet: facet, hash: hash });
+ if (this.canceled) {
+ topic.publish('facet-change-canceled', { facet: facet, hash : hash });
+ return false;
+ }
+ this.update_hash(hash, false);
+ return true;
+ },
+
+ /**
+ * Changes hash to supplied
+ *
+ * @param {String} Hash to set
+ * @param {Boolean} Whether to suppress following hash change handler
+ */
+ update_hash: function(hash, ignore_change) {
+ this.ignore_next = !!ignore_change;
+ router.go(hash);
+ },
+
+ /**
+ * Returns and resets `ignore_next` property.
+ */
+ check_clear_ignore: function() {
+ var ignore = this.ignore_next;
+ this.ignore_next = false;
+ return ignore;
+ },
+
+ /**
+ * Creates from facet state appropriate hash.
+ */
+ _create_entity_facet_hash: function(facet, state) {
+ state = lang.clone(state);
+ var entity_name = facet.entity.name;
+ var pkeys = this._encode_pkeys(state.pkeys || []);
+ delete state.pkeys;
+ var args = ioquery.objectToQuery(state || {});
+
+ var hash = [this.route_prefix, 'e', entity_name, facet.name];
+ if (!IPA.is_empty(args)) hash.push(pkeys, args);
+ else if (!IPA.is_empty(pkeys)) hash.push(pkeys);
+
+ hash = hash.join('/');
+ return hash;
+ },
+
+ /**
+ * Creates hash of general facet.
+ */
+ _create_facet_hash: function(facet, state) {
+ var args = ioquery.objectToQuery(state.args || {});
+ var hash = [this.route_prefix, 'p', facet.name];
+
+ if (!IPA.is_empty(args)) hash.push(args);
+ hash = hash.join('/');
+ return hash;
+ },
+
+ /**
+ * Creates hash from supplied facet and state.
+ *
+ * @param {facet} facet
+ * @param {Object} state
+ */
+ create_hash: function(facet, state) {
+ if (facet.entity) return this._create_entity_facet_hash(facet, state);
+ else return this._create_facet_hash(facet, state);
+ },
+
+
+ /**
+ * Tells other component to show given facet.
+ */
+ show_facet: function(facet) {
+
+ topic.publish('facet-show', {
+ facet: facet
+ });
+ },
+
+ /**
+ * URI Encodes array items and delimits them by '&'
+ * Example: ['foo ', 'bar'] => 'foo%20&bar'
+ */
+ _encode_pkeys: function(pkeys) {
+
+ var ret = [];
+ array.forEach(pkeys, function(pkey) {
+ ret.push(encodeURIComponent(pkey));
+ });
+ return ret.join('&');
+ },
+
+ /**
+ * Splits strings by '&' and return an array of URI decoded parts.
+ * Example: 'foo%20&bar' => ['foo ', 'bar']
+ */
+ _decode_pkeys: function(str) {
+
+ var keys = str.split('&');
+ for (var i=0; i<keys.length; i++) {
+ keys[i] = decodeURIComponent(keys[i]);
+ }
+ return keys;
+ },
+
+ /**
+ * Starts routing
+ */
+ startup: function() {
+ router.startup();
+ },
+
+ constructor: function(spec) {
+ spec = spec || {};
+ this.init_router();
+ }
+
+ });
+
+ return navigation;
+});
diff --git a/install/ui/src/freeipa/navigation/menu_spec.js b/install/ui/src/freeipa/navigation/menu_spec.js
new file mode 100644
index 000000000..06e49597f
--- /dev/null
+++ b/install/ui/src/freeipa/navigation/menu_spec.js
@@ -0,0 +1,107 @@
+/* Authors:
+ * Petr Vobornik <pvoborni@redhat.com>
+ *
+ * Copyright (C) 2012 Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+*/
+
+define([], function() {
+
+var nav = {};
+ nav.admin = {
+ name: 'admin',
+ items: [
+ {
+ name: 'identity',
+ label: '@i18n:tabs.identity',
+ children: [
+ { entity: 'user' },
+ { entity: 'group' },
+ { entity: 'host' },
+ { entity: 'hostgroup' },
+ { entity: 'netgroup' },
+ { entity: 'service' },
+ {
+ name:'dns',
+ label: '@i18n:tabs.dns',
+ children: [
+ { entity: 'dnszone' },
+ { entity: 'dnsconfig' },
+ { entity: 'dnsrecord', hidden:true }
+ ]
+ }
+ ]
+ },
+ {name: 'policy', label: '@i18n:tabs.policy', children: [
+ {name: 'hbac', label: '@i18n:tabs.hbac', children: [
+ {entity: 'hbacrule'},
+ {entity: 'hbacsvc'},
+ {entity: 'hbacsvcgroup'},
+ {entity: 'hbactest'}
+ ]},
+ {name: 'sudo', label: '@i18n:tabs.sudo', children: [
+ {entity: 'sudorule'},
+ {entity: 'sudocmd'},
+ {entity: 'sudocmdgroup'}
+ ]},
+ {
+ name:'automount',
+ label: '@i18n:tabs.automount',
+ entity: 'automountlocation',
+ children:[
+ {entity: 'automountlocation', hidden:true},
+ {entity: 'automountmap', hidden: true},
+ {entity: 'automountkey', hidden: true}]
+ },
+ {entity: 'pwpolicy'},
+ {entity: 'krbtpolicy'},
+ {entity: 'selinuxusermap'},
+ {name: 'automember', label: '@i18n:tabs.automember',
+ children: [
+ { name: 'amgroup', entity: 'automember',
+ facet: 'searchgroup', label: '@i18n:objects.automember.usergrouprules'},
+ { name: 'amhostgroup', entity: 'automember',
+ facet: 'searchhostgroup', label: '@i18n:objects.automember.hostgrouprules'}
+ ]}
+ ]},
+ {name: 'ipaserver', label: '@i18n:tabs.ipaserver', children: [
+ {name: 'rolebased', label: '@i18n:tabs.role', children: [
+ {entity: 'role'},
+ {entity: 'privilege'},
+ {entity: 'permission'}
+ ]},
+ {entity: 'selfservice'},
+ {entity: 'delegation'},
+ {entity: 'idrange'},
+ {entity: 'trust'},
+ {entity: 'config'}
+ ]}
+ ]
+};
+
+nav.self_service = {
+ name: 'self-service',
+ items: [
+ {
+ name: 'identity',
+ label: '@i18n:tabs.identity',
+ children: [{entity: 'user'}]
+ }
+ ]
+};
+
+return nav;
+}); \ No newline at end of file