Menu and application controller refactoring

https://fedorahosted.org/freeipa/ticket/3235
https://fedorahosted.org/freeipa/ticket/3236
This commit is contained in:
Petr Vobornik
2012-12-14 16:39:20 +01:00
parent a4d9e19c79
commit 693dc56062
11 changed files with 1735 additions and 189 deletions

View File

@@ -28,43 +28,6 @@
</script>
</head>
<body>
<div id="container">
<div id="background">
<div id="background-header"></div>
<div id="background-navigation"></div>
<div id="background-left"></div>
<div id="background-center"></div>
<div id="background-right"></div>
</div>
<div id="header">
<span class="header-logo">
<a href="#"><img src="images/ipa-logo.png" /><img src="images/ipa-banner.png" /></a>
</span>
<span class="header-right">
<span class="header-passwordexpires"></span>
<span id="loggedinas" class="header-loggedinas" style="visibility:hidden;">
<a href="#"><span id="login_header">Logged in as</span>: <span class="login"></span></a>
</span>
<span class="header-loggedinas" style="visibility:hidden;">
| <a href="#logout" id="logout">Logout</a>
</span>
<span id="header-network-activity-indicator" class="network-activity-indicator">
<img src="images/spinner-header.gif" />
</span>
</span>
</div>
<div id="navigation"></div>
<div id="content"></div>
</div>
</body>
<body></body>
</html>

View File

@@ -280,7 +280,7 @@ body {
}
/* ---- Navigation ---- */
#navigation {
.navigation {
position: absolute;
top: 34px;
left: 6px;
@@ -288,64 +288,61 @@ body {
height: 102px;
}
#navigation.tabs-3 {
.navigation ul {
list-style-type: none;
}
.navigation .submenu li {
float: left;
position: relative;
list-style: none;
white-space:nowrap;
}
/*
.navigation.tabs-3 {
height: 150px;
}
}*/
div.tabs {
.submenu {
width: 100%;
min-height: 4em;
background: transparent;
/* min-height: 4em;
background: transparent;*/
}
.tabs.ui-tabs, .tabs .ui-tabs {
padding: 0;
}
/* ---- Navigation level 1 ---- */
/* ---- Tabs level 1 ---- */
.tabs.ui-widget {
border: none;
}
.tabs1 > .ui-tabs-nav {
background: transparent;
}
.tabs1 > .ui-tabs-nav > .ui-state-hover {
background: url(images/hover-tab.png);
}
.tabs1 > .ui-tabs-nav {
padding: 33px 0 0;
.menu-level-1 > ul {
height: 38px;
padding: 34px 0 0;
margin: 0;
border: none;
/* border: none;*/
}
.tabs1 > .ui-tabs-nav li {
-moz-border-radius: 0 !important;
-webkit-border-radius: 0 !important;
border-radius: 0 !important;
.menu-level-1 > ul > li {
height: 36px;
padding: 0 18px;
border: 1px solid #A0A0A0;
background: none;
border-bottom:none;
background-image: url(images/mainnav-tab-off.png);
margin: 0 0.4em 0 0;
text-align: center;
vertical-align:baseline;
}
.tabs1 > .ui-tabs-nav > li.ui-tabs-selected {
padding: 0;
background-image: url(images/mainnav-tab-on.png);
text-align: center;
.menu-level-1 > ul > li.ui-state-hover,
.menu-level-1 > ul > li:hover {
background: url(images/hover-tab.png);
}
.tabs1 > .ui-tabs-nav > li > a {
-moz-border-radius: 0 !important;
-webkit-border-radius: 0 !important;
.menu-level-1 > ul > li.selected {
padding-bottom: 1px;
background-image: url(images/mainnav-tab-on.png);
}
.menu-level-1 > ul > li > a {
font-family: "Overpass Bold","Liberation Sans", Arial, sans-serif;
min-width: 5em;
height: 20px;
line-height: 38px;
color: #858585;
margin: 0 auto;
text-align:center;
@@ -353,54 +350,41 @@ div.tabs {
text-shadow: 1px 1px 0 #FFFFFF;
}
.tabs1 > .ui-tabs-nav > li > a:link,
span.main-nav-off > a:visited{
color: #858585;
}
.tabs1 > .ui-tabs-nav > li.ui-tabs-selected > a {
.menu-level-1 > ul > li.selected > a {
color: #1e5e05;
}
.tabs1 .ui-tabs-panel {
/* ---- Navigation level 2 ---- */
.menu-level-2 {
display: block;
border-width: 0;
padding: 0 0 0 0;
background-color: transparent;
}
/* ---- Tabs level 2 ---- */
.tabs2 {
}
.tabs2 > .ui-tabs-nav {
.menu-level-2 > ul {
padding: 5px 24px 1px;
margin: 0;
height: 25px;
border: none;
-moz-border-radius: 0;
-webkit-border-radius: 0;
border-radius: 0;
background: transparent;
}
.tabs2 > .ui-tabs-nav > li {
.menu-level-2 > ul > li {
width: auto;
margin: 0;
background: none repeat scroll 0 0 transparent !important;
color: white;
border: none;
padding-top: 3px;
}
.tabs2 > .ui-tabs-nav > li.ui-tabs-selected {
.menu-level-2 > ul > li.selected {
background: url(images/nav-arrow.png) no-repeat scroll center 2.1em transparent !important;
height: 3.1em;
height: 31px;
border: none;
margin: 0;
}
.tabs2 > .ui-tabs-nav > li > a {
.menu-level-2 > ul > li > a {
width:auto;
padding: 0.3em 0.8em ;
-moz-border-radius: 2em !important;
@@ -412,38 +396,28 @@ span.main-nav-off > a:visited{
margin: 0 0.3em;
}
.tabs2 > .ui-tabs-nav li > a:link,
span.main-nav-off > a:visited {
color: #333333;
}
.tabs2 > .ui-tabs-nav > li.ui-tabs-selected > a,
.tabs2 > .ui-tabs-nav > li > a:hover {
.menu-level-2 > ul > li.selected > a,
.menu-level-2 > ul > li > a:hover {
background-color:#EEEEEE;
color: #164304;
text-shadow: 1px 1px 0 #FFFFFF;
}
/* ---- Tabs level 3 ---- */
.tabs3 {
/* ---- Navigation level 3 ---- */
.menu-level-3 {
height: 28px;
}
.tabs3 > .ui-tabs-nav {
padding: 1em 22px 0.1em;
border: none;
background: transparent;
.menu-level-3 > ul {
padding: 0 22px 0.1em;
}
.tabs3 > .ui-tabs-nav > li {
background: transparent;
border: 0;
.menu-level-3 > ul > li {
margin: 0 2.4em 1px 0;
}
.tabs3 > .ui-tabs-nav > li > a {
.menu-level-3 > ul > li > a {
width: auto;
margin: 0;
padding: 0.3em 0 0.3em 0;
@@ -453,7 +427,7 @@ span.main-nav-off > a:visited {
color: #858585;
}
.tabs3 > .ui-tabs-nav > li.ui-tabs-selected > a {
.menu-level-3 > ul > li.selected > a {
font-family: "Overpass Bold", "Liberation Sans", Arial, sans-serif;
color: #1e5e05;
}
@@ -467,7 +441,7 @@ span.main-nav-off > a:visited {
bottom: 10px;
}
#content.tabs-3 {
#content.nav-space-3 {
top: 175px;
}

View File

@@ -132,5 +132,7 @@
+process src/libs/jquery.ordered-map.js
+process src/freeipa/*.js
+process src/freeipa/_base/*.js
+process src/freeipa/navigation/*.js
+process src/freeipa/widgets/*.js
+process src/*.js
+process ./*.js

View File

@@ -0,0 +1,323 @@
/* 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/>.
*/
/**
* Application controller
*
* Controls interaction between navigation, menu and facets.
*/
define(['dojo/_base/declare',
'dojo/_base/lang',
'dojo/_base/array',
'dojo/on',
'dojo/topic',
'dojo/query',
'dojo/dom-class',
'./widgets/App',
'./ipa',
'./navigation/Menu',
'./navigation/Router',
'./navigation/menu_spec'
],
function(declare, lang, array, on, topic, query, dom_class,
App_widget, IPA, Menu, Router, menu_spec) {
/**
* Main application
*
* This class serves as top level widget. It creates basic UI: controls
* rendering of header, footer and placeholder for facets.
*/
var App = declare(null, {
app_widget: null,
router: null,
menu: null,
initialized: false,
facet_changing: false,
init: function() {
this.menu = new Menu();
this.router = new Router();
this.app_widget = new App_widget();
this.app_widget.menu_widget.set_menu(this.menu);
this.app_widget.container_node = query('body')[0];
on(this.app_widget.menu_widget, 'item-select', lang.hitch(this, this.on_menu_click));
on(this.app_widget, 'profile-click', lang.hitch(this, this.on_profile));
on(this.app_widget, 'logout-click', lang.hitch(this, this.on_logout));
on(this.menu, 'selected', lang.hitch(this, this.on_menu_select));
topic.subscribe('facet-show', lang.hitch(this, this.on_facet_show));
topic.subscribe('facet-change', lang.hitch(this, this.on_facet_change));
topic.subscribe('facet-change-canceled', lang.hitch(this, this.on_facet_canceled));
topic.subscribe('phase-error', lang.hitch(this, this.on_phase_error));
topic.subscribe('facet-state-change', lang.hitch(this, this.on_facet_state_changed));
this.app_widget.render();
},
/**
* Gets:
* * metadata
* * server configuration
* * user information
*/
get_configuration: function(success_handler, error_handler) {
IPA.init({ on_success: success_handler, on_error: error_handler});
},
/**
* Deduces current application profile - administraion or self-service.
* Initializes profiles's menu.
*/
choose_profile: function() {
// TODO: change IPA.whoami.cn[0] to something readable
this.update_logged_in(true, IPA.whoami.cn[0]);
var selfservice = this.is_selfservice();
this.app_widget.menu_widget.ignore_changes = true;
if (selfservice) {
this.menu.name = menu_spec.self_service.name;
this.menu.add_items(menu_spec.self_service.items);
} else {
this.menu.name = menu_spec.admin.name;
this.menu.add_items(menu_spec.admin.items);
}
this.app_widget.menu_widget.ignore_changes = false;
this.app_widget.menu_widget.render();
this.app_widget.menu_widget.select(this.menu.selected);
// now we are ready for displaying a facet
// cat match a facet if hash is set
this.router.startup();
// choose default facet if not defined by route
if (!this.current_facet) {
if (selfservice) {
this.on_profile();
} else {
this.router.navigate_to_entity_facet('user', 'search');
}
}
},
is_selfservice: function() {
var whoami = IPA.whoami;
var self_service = true;
if (whoami.hasOwnProperty('memberof_group') &&
whoami.memberof_group.indexOf('admins') !== -1) {
self_service = false;
} else if (whoami.hasOwnProperty('memberofindirect_group')&&
whoami.memberofindirect_group.indexOf('admins') !== -1) {
self_service = false;
} else if (whoami.hasOwnProperty('memberof_role') &&
whoami.memberof_role.length > 0) {
self_service = false;
} else if (whoami.hasOwnProperty('memberofindirect_role') &&
whoami.memberofindirect_role.length > 0) {
self_service = false;
}
IPA.is_selfservice = self_service; // quite ugly, needed for users
return self_service;
},
update_logged_in: function(logged_in, fullname) {
this.app_widget.set('logged', logged_in);
this.app_widget.set('fullname', fullname);
},
on_profile: function() {
this.router.navigate_to_entity_facet('user', 'details', [IPA.whoami.uid[0]]);
},
on_logout: function(event) {
IPA.logout();
},
on_phase_error: function(error) {
// FIXME: CHANGE!!!
window.alert('Initialization error, have a coffee and relax.');
// var container = $('#content').empty();
// container.append('<p>Error: '+error_thrown.name+'</p>');
// container.append('<p>'+error_thrown.message+'</p>');
},
on_facet_change: function(event) {
//this.facet_changing = true;
var new_facet = event.facet;
var current_facet = this.current_facet;
if (current_facet && !current_facet.can_leave()) {
var permit_clb = function() {
// Some facet's might not call reset before this call but after
// so they are still dirty. Calling reset prevent's opening of
// dirty dialog again.
if (current_facet.is_dirty()) current_facet.reset(); //TODO change
this.router.navigate_to_hash(event.hash, event.facet);
};
var dialog = current_facet.show_leave_dialog(permit_clb);
this.router.canceled = true;
dialog.open();
}
},
on_facet_canceled: function(event) {
},
on_facet_state_changed: function(event) {
if (event.facet === this.current_facet) {
var hash = this.router.create_hash(event.facet, event.state);
this.router.update_hash(hash, true);
}
},
on_facet_show: function(event) {
var facet = event.facet;
// update menu
var menu_item = this._find_menu_item(facet);
if (menu_item) this.menu.select(menu_item);
if (!facet.container) {
facet.container_node = this.app_widget.content_node;
}
if (this.current_facet) {
this.current_facet.hide();
}
this.current_facet = facet;
facet.show();
},
_find_menu_item: function(facet) {
var items;
// entity facets
if (facet.entity) {
items = this.menu.query({ entity: facet.entity.name, facet: facet.name });
}
// normal facets
if (!items.total) {
items = this.menu.query({ facet: facet.name });
}
// entity fallback
if (!items.total && facet.entity) {
items = this.menu.query({ entity: facet.entity.name });
}
// fallback: Top level item
if (!items.total) {
items = this.menu.query({ parent: null });
}
// select first
if (items.total) {
return items[0];
}
},
/**
* Tries to find menu item with assigned facet and navigate to it.
*/
on_menu_click: function(menu_item) {
this._navigate_to_menu_item(menu_item);
},
_navigate_to_menu_item: function(menu_item) {
if (menu_item.entity) {
// entity pages
this.router.navigate_to_entity_facet(
menu_item.entity,
menu_item.facet,
menu_item.pkeys,
menu_item.args);
} else if (menu_item.facet) {
// concrete facets
this.router.navigate_to_facet(menu_item.facet, menu_item.args);
} else {
// categories, select first posible child
var children = this.menu.query({parent: menu_item.name });
if (children.total) {
var success = false;
for (var i=0; i<children.total;i++) {
success = this._navigate_to_menu_item(children[i]);
if (success) break;
}
} else {
return false;
}
}
return true;
},
/**
* Watches menu changes and adjusts facet space when there is
* a need for larger menu space.
*
* Show extended menu space when:
* * there is 3+ levels of menu
*
* Don't show when:
* * all items of levels 3+ are hidden
*/
on_menu_select: function(select_state) {
var has_visible = function(query_result) {
for (var i=0; i<query_result.total; i++) {
if (!query_result[i].hidden) return true;
}
return false;
};
var item = select_state.item;
var visible_simblings = has_visible(this.menu.query({parent: item.parent}));
var visible_children = has_visible(this.menu.query({parent: item.name}));
var levels = select_state.new_selection.length;
var three_levels = levels >= 3 && (visible_children > 0 || visible_simblings > 0);
dom_class.toggle(this.app_widget.content_node,
'nav-space-3',
three_levels);
}
});
return App;
});

View File

@@ -18,12 +18,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
//
// AMD Wrapper for json2 library
//
/**
* Application wrapper
*/
define([
//core
'dojo/_base/lang',
'dojo/Deferred',
'./phases',
'./Application_controller',
'exports', // for circullar deps
'./ipa',
'./jquery',
'./navigation',
@@ -50,78 +54,55 @@ define([
'./trust',
'./user',
'dojo/domReady!'
],function(IPA, $) {
],function(lang, Deferred, phases, Application_controller, exports) {
/* main loop (hashchange event handler) */
function window_hashchange(evt){
IPA.nav.update();
}
var app = {
function create_navigation() {
var whoami = IPA.whoami;
var factory;
/**
* Application instance
*/
app: null,
/**
* Application class
*/
App_class: Application_controller,
if (whoami.hasOwnProperty('memberof_group') &&
whoami.memberof_group.indexOf('admins') !== -1) {
factory = IPA.admin_navigation;
} else if (whoami.hasOwnProperty('memberofindirect_group')&&
whoami.memberofindirect_group.indexOf('admins') !== -1) {
factory = IPA.admin_navigation;
} else if (whoami.hasOwnProperty('memberof_role') &&
whoami.memberof_role.length > 0) {
factory = IPA.admin_navigation;
} else if (whoami.hasOwnProperty('memberofindirect_role') &&
whoami.memberofindirect_role.length > 0) {
factory = IPA.admin_navigation;
} else {
factory = IPA.self_serv_navigation;
}
/**
* Phases registration
*/
register_phases: function() {
return factory({
container: $('#navigation'),
content: $('#content')
phases.on('app-init', lang.hitch(this, function() {
var app = this.app = new this.App_class();
app.init();
return app;
}));
phases.on('metadata', lang.hitch(this, function() {
var deferred = new Deferred();
this.app.get_configuration(function(success) {
deferred.resolve(success);
}, function(error) {
deferred.reject(error);
});
}
return deferred.promise;
}));
function init_on_success(data, text_status, xhr) {
$(window).bind('hashchange', window_hashchange);
phases.on('profile', lang.hitch(this, function() {
this.app.choose_profile();
}));
},
var whoami = IPA.whoami;
IPA.whoami_pkey = whoami.uid[0];
$('#loggedinas .login').text(whoami.cn[0]);
$('#loggedinas a').fragment(
{'user-facet': 'details', 'user-pkey': IPA.whoami_pkey}, 2);
$('#logout').click(function() {
IPA.logout();
return false;
}).text(IPA.messages.login.logout);
$('.header-loggedinas').css('visibility','visible');
IPA.update_password_expiration();
IPA.nav = create_navigation();
IPA.nav.create();
IPA.nav.update();
$('#login_header').html(IPA.messages.login.header);
}
function init_on_error(xhr, text_status, error_thrown) {
var container = $('#content').empty();
container.append('<p>Error: '+error_thrown.name+'</p>');
container.append('<p>'+error_thrown.message+'</p>');
}
return {
run: function() {
IPA.init({
on_success: init_on_success,
on_error: init_on_error
});
this.register_phases();
phases.controller.run();
}
};
lang.mixin(exports, app);
return exports;
});

View File

@@ -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

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -0,0 +1,150 @@
/* Authors:
* Petr Vobornik <pvoborni@redhat.com>
*
* Copyright (C) 2013 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/>.
*/
/**
* Navigation tells application to show certain facet.
*
* It's proxy for navigation/Router instace in current running
* application.
*
* Modules just use the interface, they don't have to care about the logic in
* the background.
*/
define([
'dojo/_base/lang',
'./app', // creates circullar dependency
'./ipa',
'exports' // for handling circullar dependency
],
function(lang, app, IPA, exports) {
var get_router = function() {
return app.app.router;
},
/**
* Sets property of params depending on arg type following way:
* for String sets params.facet
* for Facet sets params.facet (based on show function)
* for Object sets params.args
* for Array sets params.pkeys
*
* @param Object params
* @param {Object|Facet|String|Function} arg
*/
set_params = function(params, arg) {
if (lang.isArray(arg)) {
params.pkeys = arg;
} else if (typeof arg === 'object') {
if (typeof arg.show === 'function') params.facet = arg;
else params.args = arg;
} else if (typeof arg === 'string') {
params.facet = arg;
}
},
/**
* Show facet.
*
* Takes 3 arguments:
* * facet(String or Facet)
* * pkeys (Array)
* * args (Object)
*
* Argument order is not defined. They are recognized based on their
* type.
*
* When facet is defined as a string it has to be registered in
* facet register. //FIXME: not yet implemented
*
* When it's an object (Facet) and has an entity set it will be
* dealt as entity facet.
*
*/
show = function(arg1, arg2, arg3) {
var nav = get_router();
var params = {};
set_params(params, arg1);
set_params(params, arg2);
set_params(params, arg3);
var facet = params.facet;
if (typeof facet === 'string') {
// FIXME: doesn't work at the moment
throw 'Not yet supported';
//facet = IPA.get_facet(facet);
}
if (!facet) throw 'Argument exception: missing facet';
if (facet && facet.entity) {
return nav.navigate_to_entity_facet(
facet.entity.name,
facet.name,
params.pkeys,
params.args);
} else {
return nav.navigate_to_facet(facet.name, params.args);
}
},
/**
* Show entity facet.
*
* @param String Enity name
* @param {Object|Facet|String|Function} arg1
* @param {Object|Facet|String|Function} arg2
* @param {Object|Facet|String|Function} arg3
*
* arg1,arg2,arg3 are:
* facet name as String
* pkeys as Array
* args as Object
*/
show_entity = function(entity_name, arg1, arg2, arg3) {
var nav = get_router();
var params = {};
set_params(params, arg1);
set_params(params, arg2);
set_params(params, arg3);
return nav.navigate_to_entity_facet(entity_name, params.facet,
params.pkeys, params.args);
},
show_default = function() {
// TODO: make configurable
return show_entity('user', 'search');
};
// Module export
exports = {
show: show,
show_entity: show_entity,
show_default: show_default
};
return exports;
});

View File

@@ -0,0 +1,193 @@
/* 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/_base/lang',
'dojo/_base/array',
'dojo/dom',
'dojo/dom-construct',
'dojo/dom-prop',
'dojo/dom-class',
'dojo/dom-style',
'dojo/query',
'dojo/on',
'dojo/Evented',
'dojo/Stateful',
'./Menu',
'dojo/NodeList-dom'
],
function(declare, lang, array, dom, construct, prop, dom_class,
dom_style, query, on, Stateful, Evented, Menu) {
/**
* Main application widget
*
* This class serves as top level widget. It creates basic UI: controls
* rendering of header, footer and placeholder for facets.
*
* @name freeipa.widgets.app
* @class
*/
var app = declare([Stateful, Evented], {
//widgets
menu_widget: null,
//nodes:
domNode: null,
container_node: null,
background_node: null,
header_node: null,
password_expires_node: null,
logged_nodes: null,
logged_user_node: null,
logged_user_link_node: null,
logout_link_node: null,
menu_node: null,
content_node: null,
app_id: 'container',
logged: false,
_loggedSetter: function(value) {
this.logged = value;
if (this.logged_nodes) {
this.logged_nodes.style('visibility', value ? 'visible' : 'hidden');
}
},
fullname: '',
_fullnameSetter: function(value) {
this.fullname = value;
if (this.logged_user_node) {
prop.set(this.logged_user_node, 'textContent', value);
}
},
render: function() {
// TODO: this method may be split into several components
this.domNode = construct.create('div', {
id: this.app_id
});
if (this.container_node) {
construct.place(this.domNode, this.container_node);
}
this._render_background();
this._render_header();
this.menu_node = this.menu_widget.render();
construct.place(this.menu_node, this.domNode);
this.content_node = construct.create('div', {
id: 'content'
}, this.domNode);
},
_render_background: function() {
var inner_html = ''+
'<div id="background-header"></div>'+
'<div id="background-navigation"></div>'+
'<div id="background-left"></div>'+
'<div id="background-center"></div>'+
'<div id="background-right"></div>';
this.background_node = construct.create('div', {
id: 'background',
innerHTML: inner_html
}, this.domNode);
},
_render_header: function() {
this.header_node = construct.create('div', {
id: 'header'
}, this.domNode);
// logo
construct.place(''+
'<span class="header-logo">'+
'<a href="#"><img src="images/ipa-logo.png" />'+
'<img src="images/ipa-banner.png" /></a>'+
'</span>', this.header_node);
// right part
construct.place(''+
'<span class="header-right">'+
'<span class="header-passwordexpires"></span>'+
'<span id="loggedinas" class="header-loggedinas" style="visibility:hidden;">'+
'<a href="#"><span id="login_header">Logged in as</span>: <span class="login"></span></a>'+
'</span>'+
'<span class="header-loggedinas" style="visibility:hidden;">'+
' | <a href="#logout" id="logout">Logout</a>'+
'</span>'+
'<span id="header-network-activity-indicator" class="network-activity-indicator">'+
'<img src="images/spinner-header.gif" />'+
'</span>'+
'</span>', this.header_node);
this.password_expires_node = query('.header-passwordexpires', this.header_node)[0];
this.logged_nodes = query('.header-loggedinas', this.header_node);
this.logged_header_node = dom.byId('login_header');// maybe ditch the id?
this.logged_user_node = query('#loggedinas .login', this.header_node)[0];
this.logged_user_link_node = query('#loggedinas a', this.header_node)[0];
this.logout_link_node = dom.byId('logout');
on(this.logout_link_node, 'click', lang.hitch(this,this.on_logout));
on(this.logged_user_link_node, 'click', lang.hitch(this,this.on_profile));
construct.place(this.header_node, this.domNode);
},
on_profile: function(event) {
event.preventDefault();
this.emit('profile-click');
},
on_logout: function(event) {
event.preventDefault();
this.emit('logout-click');
},
constructor: function(spec) {
spec = spec || {};
this.menu_widget = new Menu();
}
});
return app;
});

View File

@@ -0,0 +1,271 @@
/* 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/_base/array',
'dojo/_base/lang',
'dojo/dom',
'dojo/dom-construct',
'dojo/dom-prop',
'dojo/dom-class',
'dojo/dom-style',
'dojo/dom-attr',
'dojo/query',
'dojo/Evented',
'dojo/on',
'../ipa'], function(declare, array, lang, dom, construct, prop, dom_class,
dom_style, attr, query, Evented, on, IPA) {
return declare([Evented], {
/**
* @name freeipa.widget.menu
* @class
*
* Creates UI for freeipa.navigation.menu. Provides an event when
* a menu items is selected.
*
* event: item-select(menu_item)
*/
/**
* Object store of menu items
* @protected
* @type freeipa.navigation.menu
*/
menu: null,
/**
* domNode of this widget. FIXME: move to superclass (none yet)
* @type Node
*/
domNode: null,
/**
* Turns off update on data change
* @type Boolen
*/
ignore_changes: false,
/**
* Css class for nodes containing a submenu of certain level_class
* @type String
*/
level_class: 'menu-level',
/**
* Renders widget's elements
*/
render: function() {
if (this.domNode) {
construct.empty(this.domNode);
} else {
this.domNode = construct.create('div', {
'class': 'navigation'
});
}
if (this.menu) {
this._render_children(null, this.domNode, 1);
}
return this.domNode;
},
/**
* Render children of menu_item
* Top level items are rendered if menu_items is null
*
* @protected
* @param {menu_item|null} menu_item
* @param {Node} node
* @param {Number} level
*/
_render_children: function (menu_item, node, level) {
var self = this;
var name = menu_item ? menu_item.name : null;
var children = this.menu.items.query({ parent: name },
{ sort: [{attribute:'position'}]});
var lvl_class = this._get_lvl_class(level);
if (children.total > 0) {
var menu_node = construct.create('div', {
'class': 'submenu ' + lvl_class
//style: { display: 'none' }
});
if (menu_item) {
attr.set(menu_node, 'data-item', menu_item.name);
}
var ul_node = construct.create('ul', null, menu_node);
array.forEach(children, function(menu_item) {
var click_handler = function(event) {
self.item_clicked(menu_item, event);
event.preventDefault();
};
var li_node = construct.create('li', {
'data-name': menu_item.name,
click: click_handler
}, ul_node);
var a_node = construct.create('a', {
click: click_handler
}, li_node);
this._update_item(menu_item, li_node);
// create submenu
this._render_children(menu_item, menu_node, level + 1);
}, this);
construct.place(menu_node, node);
}
},
_get_lvl_class: function(level) {
return this.level_class + '-' + level;
},
/**
* Updates content of li_node associated with menu_item base on
* menu_item's state.
*
* @protected
* @param {menu_item|string} menu_item
* @param {Node} [li_node]
*/
_update_item: function(menu_item, li_node) {
if (typeof menu_item === 'string') {
menu_item = this.menu.items.get(menu_item);
}
if (!li_node) {
li_node = query('li[data-name=\''+menu_item.name+'\']')[0];
// Quit for non-existing nodes.
// FIXME: maybe change to exception
if (!li_node) return;
}
dom_class.toggle(li_node, 'disabled', !menu_item.disabled);
dom_class.toggle(li_node, 'selected', menu_item.selected);
dom_style.set(li_node, {
display: menu_item.hidden ? 'none': 'default'
});
var a_node = query('a', li_node)[0];
prop.set(a_node, 'href', '#' + menu_item.name);
prop.set(a_node, 'textContent', menu_item.label);
prop.set(a_node, 'title', menu_item.title || menu_item.label);
},
/**
* Displays only supplied menu items.
* @param {menu_item[]} menu_items Items to show
*/
select: function(menu_items) {
// hide all except top level
var exception = this._get_lvl_class(1);
query('div.submenu', this.domNode).forEach(function(submenu_node) {
if (dom_class.contains(submenu_node, exception)) return;
dom_style.set(submenu_node, {
display: 'none'
});
}, this);
// show and update selected
array.forEach(menu_items, function(item) {
this._update_item(item);
// show submenu
var item_div = query('div[data-item=\''+item.name+'\']', this.domNode)[0];
if (item_div) {
dom_style.set(item_div, {
display: 'block'
});
}
}, this);
},
/**
* Handles changes in this.menu object.
*
* @protected
* @param {menu_item} object
* @param {Number} removedFrom
* @param {Number} insertedInto
*/
_items_changed: function(object, removedFrom, insertedInto) {
if (this.ignore_changes) return;
if (removedFrom === -1 && insertedInto === -1) {
this._update_item(object);
} else {
// on add or removal, replace whole menu
this.render();
this.select(this.menu.selected);
}
},
/**
* Sets this.menu and starts to watch its changes
* @param {freeipa.navigation.menu} menu
*/
set_menu: function(menu) {
this.menu = menu;
//get all items
var q = menu.items.query();
q.observe(lang.hitch(this, this._items_changed), true);
on(this.menu, 'selected', lang.hitch(this, function(event) {
this.select(event.new_selection);
}));
},
/**
* Internal handler for clicking on menu item.
* Raises item-select event.
*/
_item_clicked: function(menu_item) {
this.emit('item-select', menu_item);
},
/**
* Handles click on menu item.
*
* Intended for overriding.
*
* @param {menu_item} menu_item
* @param {Event} event
*/
item_clicked: function(menu_item/*, event*/) {
this._item_clicked(menu_item);
}
});
});