From 3b9280c97477ae1f5fc0e856c9e21c31e2ad070e Mon Sep 17 00:00:00 2001 From: Petr Vobornik Date: Thu, 7 Nov 2013 18:03:38 +0100 Subject: RCUE Header https://fedorahosted.org/freeipa/ticket/3902 --- install/ui/ipa.css | 51 +----- install/ui/less/brand.less | 32 ++++ install/ui/less/rcue.less | 1 + install/ui/src/freeipa/widgets/App.js | 155 +++++++++++------ install/ui/src/freeipa/widgets/DropdownWidget.js | 211 +++++++++++++++++++++++ ipatests/test_webui/ui_driver.py | 23 ++- 6 files changed, 364 insertions(+), 109 deletions(-) create mode 100644 install/ui/less/brand.less create mode 100644 install/ui/src/freeipa/widgets/DropdownWidget.js diff --git a/install/ui/ipa.css b/install/ui/ipa.css index 126c51ed7..f2605acd6 100644 --- a/install/ui/ipa.css +++ b/install/ui/ipa.css @@ -120,38 +120,6 @@ textarea[readonly] { padding: 0.2em; } -/* ---- Header ---- */ -.header { - position: absolute; - top: 0; - left: 6px; - right: 6px; - height: 34px; - background: transparent; -} - -.header a { - text-decoration: none; -} - -.header a:link { - text-decoration: none; - color: white; -} - -.header a:visited { - text-decoration: none; - color: white; -} - -.header span.header-logo { - padding-left: 2em; -} - -.header span.header-logo a img { - border: 0; -} - /* ---- Password expiration */ .header-passwordexpires { @@ -164,20 +132,6 @@ textarea[readonly] { font-weight: bold; } -/* ---- Logged-in As ---- */ -.header-right { - float: right; -} - -.header-loggedinas { - line-height: 34px; - color: #fff; -} - -.header-loggedinas .login { - font-weight: bold; -} - /* ---- Notification area ---- */ .notification-area { @@ -231,7 +185,7 @@ textarea[readonly] { .facet { position: absolute; - top: 5px; + top: 110px; left: 10px; right: 10px; bottom: 0; @@ -253,7 +207,7 @@ textarea[readonly] { .facet-title { position: absolute; - top: 15px; + top: 10px; left: 0; color: gray; display: block; @@ -261,6 +215,7 @@ textarea[readonly] { .facet-title h3 { margin: 0; + line-height: 1.8em; } .facet-title span { diff --git a/install/ui/less/brand.less b/install/ui/less/brand.less new file mode 100644 index 000000000..72270a254 --- /dev/null +++ b/install/ui/less/brand.less @@ -0,0 +1,32 @@ +/* Authors: + * Petr Vobornik + * + * 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 . +*/ + +// This file contains overrides of reference RCUE implementation to comply +// with IPA design + +.header.rcue { + // Use blue instead of red + border-top: 3px solid #1d85d9; + .brand { + // Lower vertical padding by 5px + // FreeIPA uses logo with height: 20px instead of 10px. + padding: 2px 0; + } +} \ No newline at end of file diff --git a/install/ui/less/rcue.less b/install/ui/less/rcue.less index 9553e37d7..f27583698 100644 --- a/install/ui/less/rcue.less +++ b/install/ui/less/rcue.less @@ -4,3 +4,4 @@ @import "rcue/navbar"; @import "rcue/buttons"; @import "rcue/forms"; +@import "brand"; diff --git a/install/ui/src/freeipa/widgets/App.js b/install/ui/src/freeipa/widgets/App.js index e89d4cc61..42705649f 100644 --- a/install/ui/src/freeipa/widgets/App.js +++ b/install/ui/src/freeipa/widgets/App.js @@ -31,10 +31,11 @@ define(['dojo/_base/declare', 'dojo/Evented', 'dojo/Stateful', './Menu', + './DropdownWidget', 'dojo/NodeList-dom' ], function(declare, lang, array, dom, construct, prop, dom_class, - dom_style, query, on, Stateful, Evented, Menu) { + dom_style, query, on, Stateful, Evented, Menu, DropdownWidget) { /** * Main application widget @@ -59,14 +60,8 @@ define(['dojo/_base/declare', 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, @@ -77,9 +72,7 @@ define(['dojo/_base/declare', _loggedSetter: function(value) { this.logged = value; - if (this.logged_nodes) { - this.logged_nodes.style('visibility', value ? 'visible' : 'hidden'); - } + //TODO show/hide menu }, fullname: '', @@ -92,8 +85,6 @@ define(['dojo/_base/declare', }, render: function() { - // TODO: this method may be split into several components - this.domNode = construct.create('div', { id: this.app_id, @@ -106,9 +97,6 @@ define(['dojo/_base/declare', this._render_header(); - this.menu_node = this.menu_widget.render(); - construct.place(this.menu_node, this.header_node); - this.content_node = construct.create('div', { 'class': 'content' }, this.domNode); @@ -117,57 +105,116 @@ define(['dojo/_base/declare', _render_header: function() { this.header_node = construct.create('div', { 'class': 'header rcue' - }, this.domNode); + }); - // logo - construct.place(''+ - '', this.header_node); - - // right part - construct.place(''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - '', 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 = query('.login_header')[0]; - 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 = query('.logout')[0]; - - 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)); + this._render_nav_util(); + construct.place(this.nav_util_node, this.header_node); + + this.menu_node = this.menu_widget.render(); + construct.place(this.menu_node, this.header_node); construct.place(this.header_node, this.domNode); }, - on_profile: function(event) { - event.preventDefault(); - this.emit('profile-click'); + _render_nav_util: function() { + this.nav_util_node = construct.create('div', { + 'class': 'navbar utility' + }); + + this.nav_util_inner_node = construct.create('div', { + 'class': 'navbar-inner' + }, this.nav_util_node); + + this._render_brand(); + construct.place(this.brand_node, this.nav_util_inner_node); + + this.nav_util_tool_node = construct.create('ul', { + 'class': 'nav pull-right' + }, this.nav_util_inner_node); + + this.password_expires_node = construct.create('li', { + 'class': 'header-passwordexpires' + }, this.nav_util_tool_node); + + var network_activity = construct.create('li', { + 'class': 'header-network-activity-indicator network-activity-indicator' + }, this.nav_util_tool_node); + + construct.create('img', { + 'src': 'images/spinner-header.gif' + }, network_activity); + + var user_toggle = this._render_user_toggle_nodes(); + this.user_menu.set('toggle_content', user_toggle); + construct.place(this.user_menu.render(), this.nav_util_tool_node); + + return this.nav_util_node; + }, + + _render_brand: function() { + this.brand_node = construct.create('a', { + 'class': 'brand', + href: '#' + }); + + construct.create('img', { + src: 'images/header-logo.png', + alt: 'FreeIPA' // TODO: replace with configuration value + }, this.brand_node); + + return this.brand_node; }, - on_logout: function(event) { - event.preventDefault(); - this.emit('logout-click'); + _render_user_toggle_nodes: function() { + + var nodes = []; + + nodes.push(construct.create('span', { + 'class': 'icon-user icon-white' + })); + + this.logged_user_node = construct.create('span', { + 'class': 'loggedinas' + }); + nodes.push(this.logged_user_node); + + nodes.push(construct.create('b', { + 'class': 'caret' + })); + + return nodes; + }, + + on_user_menu_click: function(item) { + + if (item.name === 'profile') { + this.emit('profile-click'); + } else if (item.name === 'logout') { + this.emit('logout-click'); + } }, constructor: function(spec) { spec = spec || {}; this.menu_widget = new Menu(); + this.user_menu = new DropdownWidget({ + el_type: 'li', + name: 'profile-menu', + items: [ + { + name: 'profile', + label: 'Profile' + }, + { + 'class': 'divider' + }, + { + name: 'logout', + label: 'Logout' + } + ] + }); + on(this.user_menu, 'item-click', lang.hitch(this, this.on_user_menu_click)); } }); diff --git a/install/ui/src/freeipa/widgets/DropdownWidget.js b/install/ui/src/freeipa/widgets/DropdownWidget.js new file mode 100644 index 000000000..992bcf378 --- /dev/null +++ b/install/ui/src/freeipa/widgets/DropdownWidget.js @@ -0,0 +1,211 @@ +/* Authors: + * Petr Vobornik + * + * 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 . +*/ +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/Stateful', + 'dojo/on', + '../jquery', + '../ipa'], function(declare, array, lang, dom, construct, prop, dom_class, + dom_style, attr, query, Evented, Stateful, on, $, IPA) { + + return declare([Stateful, Evented], { + /** + * Represents and creates a dropdown widget. It can contain multiple + * levels. + * + * @class widgets.DropdownWidget + */ + + /** + * Raised when menu item is clicked + * @event item-click + */ + + /** + * Dropdown name + * @property {string} + */ + name: '', + + /** + * Element type + * @property {string} + */ + el_type: 'div', + + /** + * Element class + * @property {string} + */ + 'class': 'dropdown', + + /** + * Submenu class + */ + submenu_class: 'dropdown-submenu', + + /** + * Toggle button text + * @property {string} + */ + toggle_text: '', + + /** + * Toggle button content. Replaces toggle button text if set. Can be + * use for more complex toggle buttons. + * @property {HTMLElement|HTMLElement[]} + */ + toggle_content: null, + + /** + * Array of dropdown items to display. Item can have `items` field + * with an array of child items. + * @property {Array} + */ + items: [], + + /** + * domNode of this widget + * @property {HTMLElement} + */ + dom_node: null, + + render: function() { + if (this.dom_node) { + construct.empty(this.dom_node); + + } else { + this.dom_node = construct.create(this.el_type, { + name: this.name || '', + 'class': this['class'] + }); + } + + this._render_toggle(this.dom_node); + this._render_items(this.items, this.dom_node); + + return this.dom_node; + }, + + _render_toggle: function(container) { + + this.toggle_node = construct.create('a', { + 'class': 'dropdown-toggle', + 'data-toggle': 'dropdown', + href: '#' + }); + + this._update_toggle(); + if (container) { + construct.place(this.toggle_node, container); + } + return this.toggle_node; + }, + + _update_toggle: function() { + if (!this.toggle_node) return; + if (this.toggle_content) { + if (lang.isArray(this.toggle_content)) { + array.forEach(this.toggle_content, function(item) { + construct.place(item, this.toggle_node); + }, this); + } else { + construct.place(this.toggle_content, this.toggle_node); + } + } else { + prop.set(this.toggle_node, 'textContent', this.toggle_text); + } + }, + + _toggle_textSetter: function(value) { + this.toggle_text = value; + this._update_toggle(); + }, + + _toggle_contentSetter: function(value) { + this.toggle_content = value; + this._update_toggle(); + }, + + _render_items: function(items, container) { + var ul = construct.create('ul', { + 'class': 'dropdown-menu' + }); + + array.forEach(items, function(item) { + this._render_item(item, ul); + }, this); + + if (container) { + construct.place(ul, container); + } + return ul; + }, + + _render_item: function(item, container) { + + var li = construct.create('li', { + 'data-name': item.name || '' + }); + var a = construct.create('a', { + 'href': '#' + item.name || '', + innerHTML: item.label || '' + }, li); + + if (item['class']) { + dom_class.add(li, item['class']); + } + + if (item.items && item.items.length > 0) { + dom_class.add(li, 'dropdown-submenu'); + this._render_items(item.items, li); + } else { + on(a, 'click', lang.hitch(this, function(event) { + this.on_item_click(event, item); + event.preventDefault(); + })); + } + + if (container) { + construct.place(li, container); + } + return li; + }, + + on_item_click: function(event, item) { + + if (item.click) item.click(); + this.emit('item-click', item); + }, + + constructor: function(spec) { + declare.safeMixin(this, spec); + } + }); +}); diff --git a/ipatests/test_webui/ui_driver.py b/ipatests/test_webui/ui_driver.py index cf95a8cdf..dabe4a7db 100644 --- a/ipatests/test_webui/ui_driver.py +++ b/ipatests/test_webui/ui_driver.py @@ -234,7 +234,7 @@ class UI_driver(object): """ Test if dependencies were loaded. (Checks if UI has been rendered) """ - indicator = self.find("span.network-activity-indicator", By.CSS_SELECTOR) + indicator = self.find(".network-activity-indicator", By.CSS_SELECTOR) return indicator is not None def has_ca(self): @@ -259,7 +259,7 @@ class UI_driver(object): """ Check if there is running AJAX request """ - indicator = self.find("span.network-activity-indicator", By.CSS_SELECTOR) + indicator = self.find(".network-activity-indicator", By.CSS_SELECTOR) displayed = indicator and indicator.is_displayed() return displayed @@ -343,14 +343,13 @@ class UI_driver(object): """ Check if user is logged in """ - login_as = self.find('header-loggedinas', 'class name') - visible_name = login_as and login_as.is_displayed() + login_as = self.find('loggedinas', 'class name') + visible_name = len(login_as.text) > 0 logged_in = not self.auth_dialog_opened() and visible_name return logged_in def logout(self): - btn = self.find('logout', 'class name') - btn.click() + self.profile_menu_action('logout') def get_auth_dialog(self): """ @@ -380,7 +379,7 @@ class UI_driver(object): parent = parts[0:-1] self.navigate_by_menu('/'.join(parent), complete) - s = ".navigation a[href='#%s']" % item + s = ".navbar a[href='#%s']" % item link = self.find(s, By.CSS_SELECTOR, strict=True) assert link.is_displayed(), 'Navigation link is not displayed' link.click() @@ -595,6 +594,16 @@ class UI_driver(object): btn.click() self.wait_for_request() + def profile_menu_action (self, name): + """ + Execute action from profile menu + """ + menu_toggle = self.find('[name=profile-menu] > a', By.CSS_SELECTOR) + menu_toggle.click() + s = "[name=profile-menu] a[href='#%s']" % name + btn = self.find(s, By.CSS_SELECTOR, strict=True) + btn.click() + def get_form(self): """ Get last dialog or visible facet -- cgit