diff options
author | Petr Vobornik <pvoborni@redhat.com> | 2012-12-14 16:39:20 +0100 |
---|---|---|
committer | Petr Vobornik <pvoborni@redhat.com> | 2013-05-06 16:22:17 +0200 |
commit | 693dc560620d52dc24a0ab89e20147b10ed4f469 (patch) | |
tree | a748bc7a89f76d8ea4cdf1dc1a91895192a7ea56 /install/ui/src/freeipa/navigation | |
parent | a4d9e19c79b60b8f7269141374b2e3b6c0d66c45 (diff) | |
download | freeipa-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.js | 245 | ||||
-rw-r--r-- | install/ui/src/freeipa/navigation/Router.js | 337 | ||||
-rw-r--r-- | install/ui/src/freeipa/navigation/menu_spec.js | 107 |
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 |