summaryrefslogtreecommitdiffstats
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
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
-rw-r--r--install/ui/index.html41
-rw-r--r--install/ui/ipa.css136
-rw-r--r--install/ui/jsl.conf2
-rw-r--r--install/ui/src/freeipa/Application_controller.js323
-rw-r--r--install/ui/src/freeipa/app.js133
-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
-rw-r--r--install/ui/src/freeipa/navigation2.js150
-rw-r--r--install/ui/src/freeipa/widgets/App.js193
-rw-r--r--install/ui/src/freeipa/widgets/Menu.js271
11 files changed, 1742 insertions, 196 deletions
diff --git a/install/ui/index.html b/install/ui/index.html
index 5dc70db56..0523df4d1 100644
--- a/install/ui/index.html
+++ b/install/ui/index.html
@@ -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>
-
-</html>
+<body></body>
+</html> \ No newline at end of file
diff --git a/install/ui/ipa.css b/install/ui/ipa.css
index 3e443d54e..11b9aa7f3 100644
--- a/install/ui/ipa.css
+++ b/install/ui/ipa.css
@@ -280,7 +280,7 @@ body {
}
/* ---- Navigation ---- */
-#navigation {
+.navigation {
position: absolute;
top: 34px;
left: 6px;
@@ -288,64 +288,61 @@ body {
height: 102px;
}
-#navigation.tabs-3 {
- height: 150px;
-}
-
-div.tabs {
- width: 100%;
- min-height: 4em;
- background: transparent;
-}
-
-.tabs.ui-tabs, .tabs .ui-tabs {
- padding: 0;
+.navigation ul {
+ list-style-type: none;
}
-/* ---- Tabs level 1 ---- */
-
-.tabs.ui-widget {
- border: none;
+.navigation .submenu li {
+ float: left;
+ position: relative;
+ list-style: none;
+ white-space:nowrap;
}
+/*
+.navigation.tabs-3 {
+ height: 150px;
+}*/
-.tabs1 > .ui-tabs-nav {
- background: transparent;
+.submenu {
+ width: 100%;
+/* min-height: 4em;
+ background: transparent;*/
}
-.tabs1 > .ui-tabs-nav > .ui-state-hover {
- background: url(images/hover-tab.png);
-}
+/* ---- Navigation level 1 ---- */
-.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;
+.menu-level-1 > ul > li.ui-state-hover,
+.menu-level-1 > ul > li:hover {
+ background: url(images/hover-tab.png);
+}
+
+.menu-level-1 > ul > li.selected {
+ padding-bottom: 1px;
background-image: url(images/mainnav-tab-on.png);
- text-align: center;
}
-.tabs1 > .ui-tabs-nav > li > a {
- -moz-border-radius: 0 !important;
- -webkit-border-radius: 0 !important;
+.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;
}
diff --git a/install/ui/jsl.conf b/install/ui/jsl.conf
index 4d9f9ef13..e9e3ecc5e 100644
--- a/install/ui/jsl.conf
+++ b/install/ui/jsl.conf
@@ -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 \ No newline at end of file
diff --git a/install/ui/src/freeipa/Application_controller.js b/install/ui/src/freeipa/Application_controller.js
new file mode 100644
index 000000000..6406e8bc4
--- /dev/null
+++ b/install/ui/src/freeipa/Application_controller.js
@@ -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;
+}); \ No newline at end of file
diff --git a/install/ui/src/freeipa/app.js b/install/ui/src/freeipa/app.js
index 3dcb10f49..88c8c2bab 100644
--- a/install/ui/src/freeipa/app.js
+++ b/install/ui/src/freeipa/app.js
@@ -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, $) {
-
- /* main loop (hashchange event handler) */
- function window_hashchange(evt){
- IPA.nav.update();
- }
-
- function create_navigation() {
- var whoami = IPA.whoami;
- var factory;
-
-
- 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;
- }
-
- return factory({
- container: $('#navigation'),
- content: $('#content')
- });
- }
-
-
- function init_on_success(data, text_status, xhr) {
- $(window).bind('hashchange', window_hashchange);
-
- 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(lang, Deferred, phases, Application_controller, exports) {
+
+ var app = {
+
+ /**
+ * Application instance
+ */
+ app: null,
+
+ /**
+ * Application class
+ */
+ App_class: Application_controller,
+
+ /**
+ * Phases registration
+ */
+ register_phases: function() {
+
+ 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;
+ }));
+
+ phases.on('profile', lang.hitch(this, function() {
+ this.app.choose_profile();
+ }));
+ },
+
+ run: function() {
+ this.register_phases();
+ phases.controller.run();
+ }
+ };
- 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>');
- }
+ lang.mixin(exports, app);
- return {
- run: function() {
- IPA.init({
- on_success: init_on_success,
- on_error: init_on_error
- });
- }
- };
+ return exports;
}); \ No newline at end of file
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
diff --git a/install/ui/src/freeipa/navigation2.js b/install/ui/src/freeipa/navigation2.js
new file mode 100644
index 000000000..4278d0457
--- /dev/null
+++ b/install/ui/src/freeipa/navigation2.js
@@ -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;
+});
diff --git a/install/ui/src/freeipa/widgets/App.js b/install/ui/src/freeipa/widgets/App.js
new file mode 100644
index 000000000..662d0ee0b
--- /dev/null
+++ b/install/ui/src/freeipa/widgets/App.js
@@ -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;
+}); \ No newline at end of file
diff --git a/install/ui/src/freeipa/widgets/Menu.js b/install/ui/src/freeipa/widgets/Menu.js
new file mode 100644
index 000000000..0f69efa9d
--- /dev/null
+++ b/install/ui/src/freeipa/widgets/Menu.js
@@ -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);
+ }
+ });
+}); \ No newline at end of file