Jason Guiditta
2008-Oct-08 17:06 UTC
[Ovirt-devel] [PATCH server] Version 1 of Revamped Tree Navigation. Take 3
The new javascript tree widget contains the following features/changes from previous implementation: * The html for the list is dynamically generated using a javascript template system. This will allow us to plug in different layouts per tree as the widget matures. * Updates to the tree are now incremental, rather than a full rip and replace as earlier. We have 2 states we currently look for - 'new' and 'changed'. The first generates new html and appends it to the DOM, the second just does a replacement of the content of existing nodes. * Vastly simplified the markup and css. * Added calls where appropriate to refresh the tree before next planned call (for instance, if you add a new hardware pool). * Added slide effect when opening and closing a node of the tree. * Clicking the plus/minus opens/close the node only, does not load main content area. * Clicking anywhere to the right of that on a given node will load content area. * Added interim icons for 'smartpool' and 'add smartpool' Note that aside from the nav area, this should not impact the existing trees which have not been converted yet (all popups that have one), as this is a completely separate codebase with it's own js and css files. Related, but not technically part of the tree, I added a choose_layout method to allow testing of javascript components as we are building them to help eliminate possible side effects from other code. When not in a production environment, you can pass in ?component_layout=[name] where [name] is the name of a shell rhtml file you have put in views/layouts/components. As our UI is growing increasingly complex, I think this will be a very useful way to facilitate building components. Signed-off-by: Jason Guiditta <jguiditt at redhat.com> --- src/app/controllers/application.rb | 19 +- src/app/controllers/tree_controller.rb | 73 ++++- src/app/models/pool.rb | 18 +- src/app/views/layouts/_side_toolbar.rhtml | 2 +- src/app/views/layouts/_tree.rhtml | 148 ++++++-- src/app/views/layouts/components/tree.rhtml | 61 +++ src/app/views/layouts/redux.rhtml | 40 +-- src/public/images/icon_add_smartpool.png | Bin 0 -> 1341 bytes src/public/images/icon_smartpool.png | Bin 0 -> 641 bytes src/public/javascripts/ovirt.js | 28 +- src/public/javascripts/ovirt.tree.js | 85 +++++ src/public/javascripts/smart_nav_test_data.js | 151 ++++++++ src/public/javascripts/trimpath-template-1.0.38.js | 397 ++++++++++++++++++++ src/public/stylesheets/ovirt-tree/tree.css | 83 ++++ 14 files changed, 1011 insertions(+), 94 deletions(-) create mode 100644 src/app/views/layouts/components/tree.rhtml create mode 100644 src/public/images/icon_add_smartpool.png create mode 100644 src/public/images/icon_smartpool.png create mode 100644 src/public/javascripts/ovirt.tree.js create mode 100644 src/public/javascripts/smart_nav_test_data.js create mode 100644 src/public/javascripts/trimpath-template-1.0.38.js create mode 100644 src/public/stylesheets/ovirt-tree/tree.css diff --git a/src/app/controllers/application.rb b/src/app/controllers/application.rb index 6dcf6f8..a751768 100644 --- a/src/app/controllers/application.rb +++ b/src/app/controllers/application.rb @@ -1,4 +1,4 @@ -# +# # Copyright (C) 2008 Red Hat, Inc. # Written by Scott Seago <sseago at redhat.com> # @@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base # Pick a unique cookie name to distinguish our session data from others' session :session_key => '_ovirt_session_id' init_gettext "ovirt" - layout 'redux' + layout :choose_layout before_filter :pre_new, :only => [:new] before_filter :pre_create, :only => [:create] @@ -33,6 +33,13 @@ class ApplicationController < ActionController::Base before_filter :authorize_admin, :only => [:new, :create, :edit, :update, :destroy] before_filter :is_logged_in + def choose_layout + if(params[:component_layout]) + return (ENV["RAILS_ENV"] != "production")?'components/' << params[:component_layout]:'redux' + end + return 'redux' + end + def is_logged_in redirect_to(:controller => "login", :action => "login") unless get_login_user end @@ -40,7 +47,7 @@ class ApplicationController < ActionController::Base def get_login_user (ENV["RAILS_ENV"] == "production") ? session[:user] : "ovirtadmin" end - + def set_perms(hwpool) @user = get_login_user @can_view = hwpool.can_view(@user) @@ -91,8 +98,8 @@ class ApplicationController < ActionController::Base # don't define find_opts for array inputs def json_hash(full_items, attributes, arg_list=[], find_opts={}, id_method=:id) page = params[:page].to_i - paginate_opts = {:page => page, - :order => "#{params[:sortname]} #{params[:sortorder]}", + paginate_opts = {:page => page, + :order => "#{params[:sortname]} #{params[:sortorder]}", :per_page => params[:rp]} arg_list << find_opts.merge(paginate_opts) item_list = full_items.paginate(*arg_list) @@ -102,7 +109,7 @@ class ApplicationController < ActionController::Base json_hash[:rows] = item_list.collect do |item| item_hash = {} item_hash[:id] = item.send(id_method) - item_hash[:cell] = attributes.collect do |attr| + item_hash[:cell] = attributes.collect do |attr| if attr.is_a? Array value = item attr.each { |attr_item| value = value.send(attr_item)} diff --git a/src/app/controllers/tree_controller.rb b/src/app/controllers/tree_controller.rb index 1aed544..07d46f7 100644 --- a/src/app/controllers/tree_controller.rb +++ b/src/app/controllers/tree_controller.rb @@ -1,5 +1,4 @@ class TreeController < ApplicationController - def get_pools # TODO: split these into separate hash elements for HW and smart pools pools = HardwarePool.get_default_pool.full_set_nested(:method => :json_hash_element, @@ -8,11 +7,81 @@ class TreeController < ApplicationController :privilege => Permission::PRIV_VIEW, :user => get_login_user, :smart_pool_set => true) end + def fetch_nav @pools = get_pools end - + def fetch_json render :json => get_pools.to_json end + + def return_filtered_list + @ids = Array.new + @clientHash = {} + if (params[:item]) + params[:item].each { |item| + tempItem = item.split("-") + itemHash = { + :id => tempItem[0].to_s, + :name =>tempItem[1] + } + @clientHash[tempItem[0]] = itemHash + } + end + @serverHash = {:pools => build_json(HardwarePool.get_default_pool.full_set_nested(:method => :json_hash_element, + :privilege => Permission::PRIV_VIEW, :user => get_login_user)) + } + @serverHash[:smart_pools] = adjust_smart_pool_list(build_json(DirectoryPool.get_smart_root.full_set_nested(:method => :json_hash_element, + :privilege => Permission::PRIV_VIEW, :user => get_login_user, + :smart_pool_set => true))) + @ids.each { |item| + if @clientHash.has_key?(item.to_s) + @clientHash.delete(item.to_s) + end + } + @serverHash[:deleted] = @clientHash + render :json => @serverHash.to_json + end + + private + def build_json(list) + list.each {|listItem| + process_list_item(listItem) + if listItem.has_key?(:children) + build_json(listItem[:children]) + end + } + list + end + + def process_list_item(item) + if @clientHash.has_key?(item[:id].to_s) + unless @clientHash[item[:id].to_s][:name] == item[:name] + item[:state] = "changed" + else + item[:state] = "unchanged" + end + else + item[:state] = "new" + end + @ids = @ids.push(item[:id]) + end + + def adjust_smart_pool_list(list) + mySmartPools = Array.new + otherSmartPools = Array.new + list.each {|listItem| + if (listItem[:name] == get_login_user) + if listItem.has_key?(:children) + listItem[:children].each {|item| + mySmartPools.push(item) + } + end + else + otherSmartPools.push(listItem) + end + } + mySmartPools + otherSmartPools + end end diff --git a/src/app/models/pool.rb b/src/app/models/pool.rb index eb71be8..d189649 100644 --- a/src/app/models/pool.rb +++ b/src/app/models/pool.rb @@ -1,4 +1,4 @@ -# +# # Copyright (C) 2008 Red Hat, Inc. # Written by Scott Seago <sseago at redhat.com> # @@ -45,7 +45,7 @@ class Pool < ActiveRecord::Base has_many :smart_pool_tags, :as => :tagged, :dependent => :destroy has_many :smart_pools, :through => :smart_pool_tags - # used to allow parent traversal before obj is saved to the db + # used to allow parent traversal before obj is saved to the db # (needed for view code 'create' form) attr_accessor :tmp_parent @@ -168,10 +168,10 @@ class Pool < ActiveRecord::Base end end - RESOURCE_LABELS = [["CPUs", :cpus, ""], - ["Memory", :memory_in_mb, "(mb)"], - ["NICs", :nics, ""], - ["VMs", :vms, ""], + RESOURCE_LABELS = [["CPUs", :cpus, ""], + ["Memory", :memory_in_mb, "(mb)"], + ["NICs", :nics, ""], + ["VMs", :vms, ""], ["Disk", :storage_in_gb, "(gb)"]] #needed by tree widget for display @@ -195,7 +195,7 @@ class Pool < ActiveRecord::Base found = false open_list.each do |open_pool| if pool.id == open_pool.id - new_open_list = open_list[(open_list.index(open_pool)+1)..-1] + new_open_list = open_list[(open_list.index(open_pool)+1)..-1] unless new_open_list.empty? pool_children = pool.children unless pool_children hash[:children] = pool_hash(pool_children, new_open_list, filter_vm_pools) @@ -210,7 +210,7 @@ class Pool < ActiveRecord::Base end def json_hash_element - { :id => id, :type => self[:type], :text => name, :name => name} + { :id => id, :type => self[:type], :text => name, :name => name, :parent_id => parent_id} end def hash_element @@ -272,7 +272,7 @@ class Pool < ActiveRecord::Base obj = args.shift method = args.shift obj.send(method, *args) - end + end def display_name name diff --git a/src/app/views/layouts/_side_toolbar.rhtml b/src/app/views/layouts/_side_toolbar.rhtml index e1958f1..4b92bcf 100644 --- a/src/app/views/layouts/_side_toolbar.rhtml +++ b/src/app/views/layouts/_side_toolbar.rhtml @@ -25,7 +25,7 @@ <% end -%> <div class="toolbar" style="float:left;"> <a href="<%= url_for :controller => :smart_pools, :action => 'new' %>" rel="facebox[.bolder]"> - <%=image_tag "icon_add_hardwarepool.png", :title=>"Add Smart Pool" %> + <%=image_tag "icon_add_smartpool.png", :title=>"Add Smart Pool" %> </a> </div> <%if pool -%> diff --git a/src/app/views/layouts/_tree.rhtml b/src/app/views/layouts/_tree.rhtml index 0e6e138..a6bde14 100644 --- a/src/app/views/layouts/_tree.rhtml +++ b/src/app/views/layouts/_tree.rhtml @@ -1,32 +1,124 @@ -<div style=" float:left; padding:0 0 0 5px;"><%= image_tag("icon_dashboard.gif")%></div> - <% selected = "selected" if controller.controller_name == "dashboard" && params[:action] == "index" %> -<div style=" float:left; padding:5px 0 0 5px;"> - <%= link_to "Dashboard", { :controller => "dashboard" }, { :id => "dashboard", :class => "#{selected}" } %> -</div> -<div style="clear:both"></div> -<%= javascript_include_tag "jquery.ovirt.treeview.js" -%> -<script type="text/javascript"> - $(document).ready(function(){ - $("#test-tree").ovirt_treeview({ - collapsed: true, - //animated: "normal", - url: "<%= url_for :controller =>'/tree', :action => 'fetch_json' %>", - hardware_url: "<%= url_for :controller =>'/hardware', :action => 'show' %>", - resource_url: "<%= url_for :controller =>'/resources', :action => 'show' %>", - smart_url: "<%= url_for :controller =>'/smart_pools', :action => 'show' %>" - }); - var tree_reload = { - url: "<%= url_for :controller =>'/tree', :action => 'fetch_json' %>", - hardware_url: "<%= url_for :controller =>'/hardware', :action => 'show' %>", - resource_url: "<%= url_for :controller =>'/resources', :action => 'show' %>", - smart_url: "<%= url_for :controller =>'/smart_pools', :action => 'show' %>" +<%= javascript_include_tag "trimpath-template-1.0.38.js" %> +<%= javascript_include_tag "ovirt.tree.js" %> +<%= stylesheet_link_tag 'ovirt-tree/tree' %> +<script type="text/javascript"> + var treeTimer, urlObj; + var processRecursive = true; + var recursiveTreeTempl, treeItemTempl, tree_url; + $(document).ready(function(){ + recursiveTreeTempl = TrimPath.parseDOMTemplate("nav_tree_template"); + treeItemTempl = TrimPath.parseDOMTemplate("nav_tree_updater_template"); + urlObj = { + "HardwarePool" : "<%= url_for :controller =>'/hardware', :action => 'show' %>", + "VmResourcePool" : "<%= url_for :controller =>'/resources', :action => 'show' %>", + "SmartPool" : "<%= url_for :controller =>'/smart_pools', :action => 'show' %>", + "DirectoryPool" : null + } + tree_url = "<%= url_for :controller =>"/tree", :action => "return_filtered_list" %>"; + processTree(); + treeTimer = setInterval(processTree,15000); + $('ul.ovirt-tree li').livequery( + function(){ + $(this) + .children('div') + .bind('click',function(){ + $('ul.ovirt-tree li div').removeClass('current'); + var thisHref = (urlObj[$(this).attr('class')] !=null) ? urlObj[$(this).attr('class')] + '/' + this.id :null; + $(this).toggleClass('current'); + currentNode = this.id; + if ($tabs != null) { + var tabType = $tabs.data("pool_type.tabs"); + ($(this).attr('class').toLowerCase().indexOf(tabType) == -1) ?selected_tab = 0 :selected_tab = $tabs.data("selected.tabs"); + } + if (thisHref != null) { + $.ajax({ + url: thisHref, + type: 'GET', + data: {ajax:true,tab:selected_tab}, + dataType: 'html', + success: function(data) { + $('#side-toolbar').html($(data).find('div.toolbar')); + $('#tabs-and-content-container').html($(data).not('div#side-toolbar')); + }, + error: function(xhr) {$.jGrowl(xhr.status + ' ' + xhr.statusText);} + }); + } + }); + },function(){ + $(this) + .children('div') + .unbind('click'); } - $('#test-tree').everyTime(15000,function(){ - load(tree_reload, {}, this, this); - }) + ); + $('ul.ovirt-tree li:has(ul)').livequery( + function(){ + $(this) + .children('span.hitarea') + .click(function(event){ + if (this == event.target) { + if($(this).siblings('ul').size() >0) { + $(this) + .toggleClass('expanded') + .toggleClass('expandable') + .siblings('ul').slideToggle("normal"); + } + } + }); + },function(){ + $(this).children('span.hitarea') + .removeClass('expanded expandable') + .unbind('click'); + } + ); }); </script> -<ul id="test-tree" class="filetree treeview-famfamfam treeview"> -</ul> -<!--<ul id="tree" class="filetree treeview-famfamfam treeview"></ul>--> +<!-- Output elements --> +<div class="nav-dashboard"> + <% selected = "selected" if controller.controller_name == "dashboard" && params[:action] == "index" %> + <%= link_to "Dashboard", { :controller => "dashboard" }, { :id => "dashboard", :class => "#{selected}" } %> +</div> +<form id="nav_tree_form"> + <div class="nav-tree"> + <ul id="nav_tree" class="ovirt-tree"></ul> + </div> + <div class="nav-smart-pool"> + <span class="nav-smart-pool-header">Smart Pools</span> <!--FIXME: replace with i18n text --> + <ul id="smart_nav_tree" class="ovirt-tree"></ul> + </div> +</form> + +<!-- Template content --> +<!-- TODO: possibly move these templates into external files--> +<textarea id="nav_tree_updater_template" style="display:none;"> + <li> + <input type="checkbox" name="item[]" value="${id}-${name}" style="display:none" checked="checked"/> + <span class="hitarea {if defined('children')} expandable{/if}"> </span><div id="${id}" class="${type}">${name}</div> + </li> +</textarea> +<textarea id="nav_tree_template" style="display:none;"> + {macro htmlList(list, optionalListType)} + {var listType = optionalListType != null ? optionalListType : "ul"} + <${listType} style="display:none;"> + {for item in list} + <li> + <input type="checkbox" name="item[]" value="${item.id}-${item.name}" style="display:none" checked="checked"/> + <span class="hitarea {if item.children} expandable{/if}"> </span><div id="${item.id}" class="${item.type}">${item.name}</div> + {if item.children} + ${htmlList(item.children)} + {/if} + </li> + {/for} + </${listType}> + {/macro} + + {for item in pools} + <li> + <input type="checkbox" name="item[]" value="${item.id}-${item.name}" style="display:none" checked="checked"/> + <span class="hitarea {if item.children} expandable{/if}"> </span><div id="${item.id}" class="${item.type}">${item.name}</div> + {if item.children} + ${htmlList(item.children)} + {/if} + </li> + {/for} +</textarea> diff --git a/src/app/views/layouts/components/tree.rhtml b/src/app/views/layouts/components/tree.rhtml new file mode 100644 index 0000000..063a6df --- /dev/null +++ b/src/app/views/layouts/components/tree.rhtml @@ -0,0 +1,61 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + +<head> + <meta http-equiv="content-type" content="text/html;charset=UTF-8" /> + <title><%= yield :title -%></title> + <%= javascript_include_tag "jquery-1.2.6.min.js" -%> + <%= javascript_include_tag "jquery.livequery.min.js" -%> + <%= javascript_include_tag "smart_nav_test_data.js" -%> + <%= javascript_include_tag "jquery.form.js" -%> + <%= javascript_include_tag "ovirt.js" -%> + + <script type="text/javascript"> + var $tabs, selected_tab; + function delete_vm_pool(id, parent) { + $(document).trigger('close.facebox'); + $.post('<%= url_for :controller => "resources", :action => "destroy" %>', + {id: id}, + function(data,status){ + $("#vmpools_grid").flexReload(); + processTree(); + if (data.alert) { + $.jGrowl(data.alert); + } + }, 'json'); + } + function delete_hw_pool(id, parent) { + $(document).trigger('close.facebox'); + $.post('<%= url_for :controller => "hardware", :action => "destroy" %>', + {id: id}, + function(data,status){ + processTree(); + if (data.alert) { + $.jGrowl(data.alert); + } + }, 'json'); + } + </script> + <%= yield :scripts -%> + </head> + + <body> + <div id="side"> + <button id="stop">Stop!</button> + <%= render :partial => '/layouts/tree' %> + </div> + <div id="side-toolbar" class="header_menu_wrapper"> + <%= render :partial => '/layouts/side_toolbar' %> + </div> + + <div id="tabs-and-content-container"> + <div id="main"> + <div id="content_area"> + + </div> + </div> + </div> + </body> +</html> diff --git a/src/app/views/layouts/redux.rhtml b/src/app/views/layouts/redux.rhtml index 01540d4..a985cdd 100644 --- a/src/app/views/layouts/redux.rhtml +++ b/src/app/views/layouts/redux.rhtml @@ -16,20 +16,18 @@ <%= stylesheet_link_tag 'flexigrid/flexigrid.css' %> <%= stylesheet_link_tag 'facebox' %> <%= stylesheet_link_tag 'jquery.jgrowl.css' %> - <!--%= stylesheet_link_tag 'jquery.ui-1.5b4/themes/flora/flora.tabs.css' %--> + <%= javascript_include_tag "jquery-1.2.6.min.js" -%> <%= javascript_include_tag "jquery-treeview/jquery.treeview.js" -%> <%= javascript_include_tag "jquery-treeview/jquery.treeview.async.js" -%> <%= javascript_include_tag "flexigrid.js" -%> <%= javascript_include_tag "facebox.js" -%> - <%= javascript_include_tag "jquery.timers.js" -%> + <%#= javascript_include_tag "jquery.timers.js" -%> <%= javascript_include_tag "jquery-svg/jquery.svg.pack.js" -%> <!--%= javascript_include_tag "jquery-svg/jquery.svgfilter.js" -%--> <%= javascript_include_tag "jquery-svg/jquery.svggraph.js" -%> - <!--%= javascript_include_tag "jquery.ui-1.5b4/ui.core.js" -%> - < %= javascript_include_tag "jquery.ui-1.5b4/ui.tabs.js" -%--> <%= javascript_include_tag "jquery.cookie.js" -%> - <%= javascript_include_tag "jquery.livequery.pack.js" -%> + <%= javascript_include_tag "jquery.livequery.min.js" -%> <%= javascript_include_tag "jquery.form.js" -%> <%= javascript_include_tag "jquery.jgrowl.js" -%> @@ -40,37 +38,9 @@ <%= javascript_include_tag "ovirt.js" -%> <script type="text/javascript"> var $tabs, selected_tab; - $(document).ready(function(){ + $(document).ready(function(){ + $.ajaxSetup({error: function(xhr) {$.jGrowl(xhr.status + ' ' + xhr.statusText);}}); $('a[rel*=facebox]').livequery(function(){$(this).facebox();},function(){}); - $('#side a').livequery(function(){ - $(this).bind('click', function(){ - currentNode = $(this).parent().parent()[0].id; - if ($tabs != null) { - var tabType = $tabs.data("pool_type.tabs"); - ($(this).attr('href').indexOf(tabType) == -1) ?selected_tab = 0 :selected_tab = $tabs.data("selected.tabs"); - } - $('#side span').each(function(){ - var nodeType = $(this).attr('class'); - if (nodeType.indexOf('_') != -1){ - nodeType = nodeType.substr(nodeType.indexOf('_') +1); - $(this).attr('class', nodeType); - } - }); - var nodeType = $(this).parent().attr('class'); - $(this).parent().attr('class', 'current_' + nodeType); - $.ajax({ - url: $(this).attr('href'), - type: 'GET', - data: {ajax:true,tab:selected_tab}, - //data: {nolayout:true}, - dataType: 'html', - success: function(data) { - $('#side-toolbar').html($(data).find('div.toolbar')); - $('#tabs-and-content-container').html($(data).not('div#side-toolbar')); - }, - error: function(xhr) {$.jGrowl(xhr.status + ' ' + xhr.statusText);} - }); - return false;})},function(){}); $('.dialog_tab_nav a').livequery(function(){ $(this).bind('click', function(){ $('.dialog_tab_nav li').removeClass('current'); diff --git a/src/public/images/icon_add_smartpool.png b/src/public/images/icon_add_smartpool.png new file mode 100644 index 0000000000000000000000000000000000000000..d7cb7316596b1eafbcad54890971f42737d45a62 GIT binary patch literal 1341 zcmV-D1;YA?P)<h;3K|Lk000e1NJLTq0018V0018d1^@s7XsITz00001b5ch_0Itp) z=>Px#32;bRa{vGe at Bjb`@Bu=sG?)MY1ma0VK~zY`?Uj8 at m1P*lf6sdk2N4b*32Gpj ziIr#}C<=4BR93E5nbW3j`bTK1wQ`NkwdGP<I&+%SGMr_8v}Lp5DrT%42!5m_a$+JN znu#8ik0Ws4aK7L7?hk|@QFOLy&Fin{eeUPFuIGNA`?(+J4=cVoEq2>x0Wbs1Je}!` ze{g(M at wQp<036Moz3Z2+-<aJm9l3!%lg6=BasDv&Ptg;VL-E*;a7R$xgZa57vMJ at H zB*xoR#W4eq*F)yg{GK?48&Y at 4{sb}h#Y_46DB&MQck=e~O;WZ$r?PiU1312IIsgfzWyB>6O)lU4OwT6G0K?PY9_K6=d)8_VBT?0;+Pa3m3Q1GuR$Z)UMA at 5B^-Wl!$L#L2 z0HDR?qfDNgR at y!hzxx{hBGw%|BO)TTW#2C@*_bScR>%Eba->ELc-SH$ukJnBC;0L6 zZ79H&w~>rxh3d8f0Ebq`O4h38F2U+~C|@(8081QVNu;ASDCIxqtC=h7RKEfMMVm7} z3v*X5&6qeG(FLUxm=q_=u3;KEzjV|~JF?=!mFuQs=!U2HoH@=C#8GouPLyBg_^Ae( zt1ih<i%wH at UUhHsef*u5aoxc)0&uoCSAJSKSc*SNpM0zJ=H5hrHn+cLJaYVK`8ju4 zX|uyMEG{;p>6TTuY|n{4ZD+ at l44{Vd1s`zrLVeLwZxzq_E4im&7CTq4V at U>WZa=SU z+o1gbT*zk3)VWw=5@*`>z96S}&XRt~uB&AL?0;=KsZ&CcOS$0EFzP-fZL)<4GbUnE z{{C}*GP`u#;q}RRR?F4LCro-6P1CxPl1_BFj;%BgmuYmzEpHmY;SJ*?^`TB8t)X2( zrP0>z=1lz%MlZ<EN=Qg3yveyI4<m^3ctHFJ3Z*nOw+D at AdK3F4<7p(=VQ{{|OT9ma z1ha!7krC(}b=>lA_aG{X at +KW$+S)}&-*2=&_O7meZ444Hh2OtL|N9ov8{PA at r@ocK zwWB0|IdU+^RMlPVC0nMcyU|wznXme;0)RCyrl6EkMIWXt3<;0?${K4WWKbwUpH6#o zd%)*%F35VvcHo}OMMReGJ9S&qmWAoK8V?aMEE8AbNlx#{7V9H()WrE))jgZ{{$8^4 zH~@RIr}kavSc9SkF&@AjOrfga6YS-MttJf{3PTG)vqZ)-BzYc_Ufw$Eww=6ZH%h7e z93(t?7&BHj(B=;Ga&&{Y5}Lk5dC!CHLTTX)9 at I?w!gZv-@<a^o-)0(tc6{x2TI<TF zJ+|-e&fIojh3MW^JXenq9c^ORh-it8)f7fMV(6ekQD~qvKoE2=d?;lCQxpz|LBo|6 z91TrgLqb2#de8pm9h=+FS6EFU5&GqV9NlrH=nIRPMKO_P9!-kH8Wo1<Mpvu9L01Tv zG&-CfYA-lwX>mzJ(i}B3ZE2b*G}1F_Y(~u;{dRy6V^Vq^jicM2^($Ws;;uzpouC4s z3n+Ety-*+;Xa;(R2MGv~56w^zvOQ_S)3)0coOz&%>};$eO!E_A31abj!0-|9>)4$E z+yR5ZAsUHM7D6l<gM0=NrXWq%EAdL`pn<dpG&h2sVA4ShE;jjb1~i<yMJ$68I+Ugb zb+5~OuGMLcH2YMP#UR9_A^uK+0nhy|{olkN%sNK%{HL at T00000NkvXXu0mjfQ2usz literal 0 HcmV?d00001 diff --git a/src/public/images/icon_smartpool.png b/src/public/images/icon_smartpool.png new file mode 100644 index 0000000000000000000000000000000000000000..4ed0af0d2f2da5b34d13491ebe06f64a9450b5ec GIT binary patch literal 641 zcmV-{0)G98P)<h;3K|Lk000e1NJLTq000gE000gM1^@s7XiptS00001b5ch_0Itp) z=>Px#32;bRa{vGe>i_@>>j8p`4O##I0wzgBK~yNuZIDYylwlNxpYI#zqK at T2S=JyC z*&>E6I%yZEa3L*)iWaScHm%y#Sj12vM9^iELAzSCYGDjC>WH_Ll*&tLv#6;-Yf=*( zLq~sS=07cl{z>v(edpsn?|FFPUrEF#`N at p<B=)fr2A^FZ9B_&;-y<D&ygAEwlUu?8 zXWrl1=*kUkH{2rM&r55dNVKW#5S_qN>OgCO?qQ8bXY*%b6TKMABl7yX+X_>W+dJjd z*#@*RiZ;R2Aa*aK{hv_)UHA6OiHbRtXp~Y6$A425SX5T*=MN#m*9wJ=6mE2hu{<L2 z at jmNMwFkFJ)<{x!wGGEoGp)_9P`ZGv)t8kTdg_;h$1A~@vNp1=1N6KM<$wn7mdfF3 zFQ5UFj=o>i-<nYc0CrI at H=iq6OkAGbzzhWd#X3#<*edoCFI&t6sZ=xvv=Mpt{VFNj zTZvLOW at A&Fyz*v4-#T?Ys*z~RIWboUM2ta+ww#gP$94a8qr#0YscHzYx#B2xS2=pq z9{AY=O9LQ+xQ`&^H<9HpWKszhC*Lw0dVu;i65(s;k~nQi{Ks}-x7+fmN+FUvmPrfm z`s1WBPU?fx%3|+(QZJ6=d;I%6>lQ(il?+3llCb?WH5XqfAlDo~YI=;a;xx=Hu(FnA ze0B{0UzvkKhmBHOn)qa=6{`O}igp}Qv4t#=AB9Z%9;({limDB+sD%Hy>P!?7TS%jw bB^KcyJ~ZgpH4FC800000NkvXXu0mjf6L=r` literal 0 HcmV?d00001 diff --git a/src/public/javascripts/ovirt.js b/src/public/javascripts/ovirt.js index 28585fd..4579c80 100644 --- a/src/public/javascripts/ovirt.js +++ b/src/public/javascripts/ovirt.js @@ -1,6 +1,5 @@ // ovirt-specific javascript functions are defined here - // helper functions for dialogs and action links @@ -41,7 +40,7 @@ function add_hosts(url) if (validate_selected(hosts, "host")) { $.post(url, { resource_ids: hosts.toString() }, - function(data,status){ + function(data,status){ $(document).trigger('close.facebox'); grid = $("#hosts_grid"); if (grid.size()>0 && grid != null) { @@ -142,11 +141,11 @@ function ajax_validation(response, status) $(".fieldWithErrors").removeClass("fieldWithErrors"); $("div.errorExplanation").remove(); if (!response.success && response.errors ) { - for(i=0; i<response.errors.length; i++) { + for(i=0; i<response.errors.length; i++) { var element = $("div.form_field:has(#"+response.object + "_" + response.errors[i][0]+")"); if (element) { element.addClass("fieldWithErrors"); - for(j=0; j<response.errors[i][1].length; j++) { + for(j=0; j<response.errors[i][1].length; j++) { element.append('<div class="errorExplanation">'+response.errors[i][1][j]+'</div>'); } } @@ -163,9 +162,7 @@ function afterHwPool(response, status){ ajax_validation(response, status); if (response.success) { $(document).trigger('close.facebox'); - // FIXME do we need to reload the tree here - - // this is for reloading the host/storage grid when + // this is for reloading the host/storage grid when // adding hosts/storage to a new HW pool if (response.resource_type) { grid = $('#' + response.resource_type + '_grid'); @@ -176,9 +173,12 @@ function afterHwPool(response, status){ } } + //FIXME: point all these refs at a widget so we dont need the functions in here + processTree(); + if ((response.resource_type == 'hosts' ? get_selected_hosts() : get_selected_storage()).indexOf($('#'+response.resource_type+'_selection_id').html()) != -1){ empty_summary(response.resource_type +'_selection', (response.resource_type == 'hosts' ? 'Host' : 'Storage Pool')); - } + } // do we have HW pools grid? //$("#vmpools_grid").flexReload() } @@ -193,12 +193,14 @@ function afterVmPool(response, status){ } else { $tabs.tabs("load",$tabs.data('selected.tabs')); } + processTree(); } } function afterSmartPool(response, status){ ajax_validation(response, status); if (response.success) { $(document).trigger('close.facebox'); + processTree(); } } function afterStoragePool(response, status){ @@ -208,7 +210,7 @@ function afterStoragePool(response, status){ grid = $("#storage_grid"); if (grid.size()>0 && grid != null) { grid.flexReload(); - } else {; + } else { $tabs.tabs("load",$tabs.data('selected.tabs')); } } @@ -238,7 +240,7 @@ function afterVm(response, status){ } } -//selection detail refresh +//selection detail refresh function refresh_summary(element_id, url, obj_id){ $('#'+element_id+'').load(url, { id: obj_id}) } @@ -289,10 +291,10 @@ function delete_pool(delete_url, id) $.post(delete_url, {id: id}, function(data,status){ + //no more flex reload? + processTree(); if (data.alert) { $.jGrowl(data.alert); } }, 'json'); -} - - +} \ No newline at end of file diff --git a/src/public/javascripts/ovirt.tree.js b/src/public/javascripts/ovirt.tree.js new file mode 100644 index 0000000..1d8e26f --- /dev/null +++ b/src/public/javascripts/ovirt.tree.js @@ -0,0 +1,85 @@ +function processTree (){ + $("#nav_tree_form").ajaxSubmit({ + url: tree_url, + type: "POST", + dataType: "json", + success: function(response){ + // First, remove any deleted items from the tree + $.each(response.deleted, function(name, value){ + //FIXME: special case for other peoples smart pools + //come up with better way or split out somewhere. + if($('#' + value.id).hasClass('SmartPool')) { + if($("#smart_nav_tree > li > div.SmartPool").size() > 1) { + $("#smart_nav_tree > li:first div").click(); + } else { + $('#nav_tree > li:first > div').click(); + } + } else { + //check if the li is the only one. If so, remove its container as well + if ($('#' + value.id).parent("li").siblings().size() === 0 ) { + if($('#' + value.id).is(':visible')) { + $('#' + value.id).parent("li").parent("ul").siblings("div").click(); + } + $('#' + value.id).parent("li").parent("ul").remove(); + } else { + if($('#' + value.id).is(':visible')) { + $('#' + value.id).parent() + .siblings('li:first') + .children('div') + .click(); + } + } + } + $('#' + value.id).parent().remove(); + }); + + if(processRecursive) { + $("#nav_tree").html(recursiveTreeTempl.process({"pools" : response.pools})); + $("#smart_nav_tree").html(recursiveTreeTempl.process({"pools" : response.smart_pools})); + processRecursive = false; + } else { + // Loop through the items and decide if we need updated/new html for each item. + processChildren(response.pools, treeItemTempl); + processChildren(response.smart_pools, treeItemTempl); + } + } + }); +} + +function processChildren(list, templateObj){ +/* TODO: In future, we may need an additional state here of 'moved' which deletes + * the item where it was in the tree and adds it to its new parent. +*/ + $.each(list, function(n,data){ + var updatedNode; + if(data.state === 'changed'){ + $('input[value^=' + data.id + '-]').attr('value', data.id + '-' + data.name); + $('#' + data.id).html(data.name); + } else if(data.state === 'new') { + /* If the elem with id matching the parent id has a sibling that is a ul, + * we should append the result of processing the template to the existing + * sublist. Otherwise, we need to add a new sublist and add it there. + */ + var result = templateObj.process(data); + if ($('#' + data.parent_id).siblings('ul').size() > 0) { + $('#' + data.parent_id).siblings('ul').append(result); + } else { + if (data.type === "SmartPool"){ //handle current user smart pools + if($('#smart_nav_tree > li:has(ul)').size() > 0) { + $(result).insertBefore('#smart_nav_tree > li:has(ul):first'); + } else { + $('#smart_nav_tree').append(result); + } + } else { + $('#' + data.parent_id).parent().append('<ul>' + result + '</ul>'); + $('#' + data.parent_id).siblings('span').addClass('expanded'); + } + } + } + else { + if (data.children) { + processChildren(data.children, templateObj); + } + } + }); +} \ No newline at end of file diff --git a/src/public/javascripts/smart_nav_test_data.js b/src/public/javascripts/smart_nav_test_data.js new file mode 100644 index 0000000..43e7dbc --- /dev/null +++ b/src/public/javascripts/smart_nav_test_data.js @@ -0,0 +1,151 @@ +var pools3 = { + "deleted" : {}, + "pools" :[ + { "name": "default", + "text": "default", + "children": + [{ "name": "Engineering", + "text": "Engineering", + "children": + [{ "name": "Development", + "text": "Development", + "children": + [{ "name": "Project X", + "text": "Project X", + "id": 19, + "type": "VmResourcePool"}, + { "name": "Project Y", + "text": "Project Y", + "id": 20, + "type": "VmResourcePool"}], + "id": 9, + "type": "HardwarePool"}, + { "name": "QA", + "text": "QA", + "children": + [{ "name": "Bob's Team", + "text": "Bob's Team", + "children": + [{ "name": "Bob's VMs", + "text": "Bob's VMs", + "id": 21, + "type": "VmResourcePool"}], + "id": 17, + "type": "HardwarePool"}, + { "name": "Jim's Team", + "text": "Jim's Team", + "children": + [{ "name": "Jim's VMs", + "text": "Jim's VMs", + "id": 22, + "type": "VmResourcePool"}], + "id": 18, + "type": "HardwarePool"}, + { "name": "Sally's Team", + "text": "Sally's Team", + "children": + [{ "name": "Sally's VMs", + "text": "Sally's VMs", + "id": 33, + "type": "VmResourcePool"}], + "id": 32, + "type": "HardwarePool"}], + "id": 10, + "type": "HardwarePool"}, + { "name": "Stage", + "text": "Stage", + "children": + [{ "name": "stage1", + "text": "stage1", + "id": 45, + "type": "HardwarePool"}, + { "name": "stage2", + "text": "stage2", + "id": 46, + "type": "HardwarePool"}], + "id": 44, + "type": "HardwarePool"}], + "id": 5, + "type": "HardwarePool"}, + { "name": "Finance", + "text": "Finance", + "children": + [{ "name": "Payroll", + "text": "Payroll", + "children": + [{ "name": "Payroll VMs", + "text": "Payroll VMs", + "id": 23, + "type": "VmResourcePool"}], + "id": 11, + "type": "HardwarePool"}, + { "name": "Accts. Receivable", + "text": "Accts. Receivable", + "children": + [{ "name": "our VMs", + "text": "our VMs", + "id": 24, + "type": "VmResourcePool"}], + "id": 12, + "type": "HardwarePool"}], + "id": 6, + "type": "HardwarePool"}, + { "name": "HR", + "text": "HR", + "children": + [{ "name": "Hiring Team", + "text": "Hiring Team", + "id": 13, + "type": "HardwarePool"}, + { "name": "Benefits", + "text": "Benefits", + "id": 14, + "type": "HardwarePool"}], + "id": 7, + "type": "HardwarePool"}, + { "name": "External (DMZ)", + "text": "External (DMZ)", + "children": + [{ "name": "VMs", + "text": "VMs", + "id": 25, + "type": "VmResourcePool"}, + { "name": "DB Cluster", + "text": "DB Cluster", + "children": + [{ "name": "VMs", + "text": "VMs", + "id": 27, + "type": "VmResourcePool"}], + "id": 26, + "type": "HardwarePool"}], + "id": 8, + "type": "HardwarePool"}], + "id": 1, + "type": "HardwarePool"}], +"smart_pools":[{ "name": "ovirtadmin", + "text": "ovirtadmin", + "children": + [{ "name": "not so smart", + "text": "not so smart", + "id": 39, + "type": "SmartPool"}, + { "name": "a little smarter", + "text": "a little smarter", + "id": 40, + "type": "SmartPool"}, + { "name": "arrrrr", + "text": "arrrrr", + "id": 41, + "type": "SmartPool"}, + { "name": "huh?", + "text": "huh?", + "id": 42, + "type": "SmartPool"}, + { "name": "booya", + "text": "booya", + "id": 43, + "type": "SmartPool"}], + "id": 37, + "type": "DirectoryPool"}] +} \ No newline at end of file diff --git a/src/public/javascripts/trimpath-template-1.0.38.js b/src/public/javascripts/trimpath-template-1.0.38.js new file mode 100644 index 0000000..fd0898d --- /dev/null +++ b/src/public/javascripts/trimpath-template-1.0.38.js @@ -0,0 +1,397 @@ +/** + * TrimPath Template. Release 1.0.38. + * Copyright (C) 2004, 2005 Metaha. + * + * TrimPath Template is licensed under the GNU General Public License + * and the Apache License, Version 2.0, as follows: + * + * 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 2 + * of the License, or (at your option) any later version. + * + * This program is distributed 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var TrimPath; + +// TODO: Debugging mode vs stop-on-error mode - runtime flag. +// TODO: Handle || (or) characters and backslashes. +// TODO: Add more modifiers. + +(function() { // Using a closure to keep global namespace clean. + if (TrimPath == null) + TrimPath = new Object(); + if (TrimPath.evalEx == null) + TrimPath.evalEx = function(src) { return eval(src); }; + + var UNDEFINED; + if (Array.prototype.pop == null) // IE 5.x fix from Igor Poteryaev. + Array.prototype.pop = function() { + if (this.length === 0) {return UNDEFINED;} + return this[--this.length]; + }; + if (Array.prototype.push == null) // IE 5.x fix from Igor Poteryaev. + Array.prototype.push = function() { + for (var i = 0; i < arguments.length; ++i) {this[this.length] = arguments[i];} + return this.length; + }; + + TrimPath.parseTemplate = function(tmplContent, optTmplName, optEtc) { + if (optEtc == null) + optEtc = TrimPath.parseTemplate_etc; + var funcSrc = parse(tmplContent, optTmplName, optEtc); + var func = TrimPath.evalEx(funcSrc, optTmplName, 1); + if (func != null) + return new optEtc.Template(optTmplName, tmplContent, funcSrc, func, optEtc); + return null; + } + + try { + String.prototype.process = function(context, optFlags) { + var template = TrimPath.parseTemplate(this, null); + if (template != null) + return template.process(context, optFlags); + return this; + } + } catch (e) { // Swallow exception, such as when String.prototype is sealed. + } + + TrimPath.parseTemplate_etc = {}; // Exposed for extensibility. + TrimPath.parseTemplate_etc.statementTag = "forelse|for|if|elseif|else|var|macro"; + TrimPath.parseTemplate_etc.statementDef = { // Lookup table for statement tags. + "if" : { delta: 1, prefix: "if (", suffix: ") {", paramMin: 1 }, + "else" : { delta: 0, prefix: "} else {" }, + "elseif" : { delta: 0, prefix: "} else if (", suffix: ") {", paramDefault: "true" }, + "/if" : { delta: -1, prefix: "}" }, + "for" : { delta: 1, paramMin: 3, + prefixFunc : function(stmtParts, state, tmplName, etc) { + if (stmtParts[2] != "in") + throw new etc.ParseError(tmplName, state.line, "bad for loop statement: " + stmtParts.join(' ')); + var iterVar = stmtParts[1]; + var listVar = "__LIST__" + iterVar; + return [ "var ", listVar, " = ", stmtParts[3], ";", + // Fix from Ross Shaull for hash looping, make sure that we have an array of loop lengths to treat like a stack. + "var __LENGTH_STACK__;", + "if (typeof(__LENGTH_STACK__) == 'undefined' || !__LENGTH_STACK__.length) __LENGTH_STACK__ = new Array();", + "__LENGTH_STACK__[__LENGTH_STACK__.length] = 0;", // Push a new for-loop onto the stack of loop lengths. + "if ((", listVar, ") != null) { ", + "var ", iterVar, "_ct = 0;", // iterVar_ct variable, added by B. Bittman + "for (var ", iterVar, "_index in ", listVar, ") { ", + iterVar, "_ct++;", + "if (typeof(", listVar, "[", iterVar, "_index]) == 'function') {continue;}", // IE 5.x fix from Igor Poteryaev. + "__LENGTH_STACK__[__LENGTH_STACK__.length - 1]++;", + "var ", iterVar, " = ", listVar, "[", iterVar, "_index];" ].join(""); + } }, + "forelse" : { delta: 0, prefix: "} } if (__LENGTH_STACK__[__LENGTH_STACK__.length - 1] == 0) { if (", suffix: ") {", paramDefault: "true" }, + "/for" : { delta: -1, prefix: "} }; delete __LENGTH_STACK__[__LENGTH_STACK__.length - 1];" }, // Remove the just-finished for-loop from the stack of loop lengths. + "var" : { delta: 0, prefix: "var ", suffix: ";" }, + "macro" : { delta: 1, + prefixFunc : function(stmtParts, state, tmplName, etc) { + var macroName = stmtParts[1].split('(')[0]; + return [ "var ", macroName, " = function", + stmtParts.slice(1).join(' ').substring(macroName.length), + "{ var _OUT_arr = []; var _OUT = { write: function(m) { if (m) _OUT_arr.push(m); } }; " ].join(''); + } }, + "/macro" : { delta: -1, prefix: " return _OUT_arr.join(''); };" } + } + TrimPath.parseTemplate_etc.modifierDef = { + "eat" : function(v) { return ""; }, + "escape" : function(s) { return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); }, + "capitalize" : function(s) { return String(s).toUpperCase(); }, + "default" : function(s, d) { return s != null ? s : d; } + } + TrimPath.parseTemplate_etc.modifierDef.h = TrimPath.parseTemplate_etc.modifierDef.escape; + + TrimPath.parseTemplate_etc.Template = function(tmplName, tmplContent, funcSrc, func, etc) { + this.process = function(context, flags) { + if (context == null) + context = {}; + if (context._MODIFIERS == null) + context._MODIFIERS = {}; + if (context.defined == null) + context.defined = function(str) { return (context[str] != undefined); }; + for (var k in etc.modifierDef) { + if (context._MODIFIERS[k] == null) + context._MODIFIERS[k] = etc.modifierDef[k]; + } + if (flags == null) + flags = {}; + var resultArr = []; + var resultOut = { write: function(m) { resultArr.push(m); } }; + try { + func(resultOut, context, flags); + } catch (e) { + if (flags.throwExceptions == true) + throw e; + var result = new String(resultArr.join("") + "[ERROR: " + e.toString() + (e.message ? '; ' + e.message : '') + "]"); + result["exception"] = e; + return result; + } + return resultArr.join(""); + } + this.name = tmplName; + this.source = tmplContent; + this.sourceFunc = funcSrc; + this.toString = function() { return "TrimPath.Template [" + tmplName + "]"; } + } + TrimPath.parseTemplate_etc.ParseError = function(name, line, message) { + this.name = name; + this.line = line; + this.message = message; + } + TrimPath.parseTemplate_etc.ParseError.prototype.toString = function() { + return ("TrimPath template ParseError in " + this.name + ": line " + this.line + ", " + this.message); + } + + var parse = function(body, tmplName, etc) { + body = cleanWhiteSpace(body); + var funcText = [ "var TrimPath_Template_TEMP = function(_OUT, _CONTEXT, _FLAGS) { with (_CONTEXT) {" ]; + var state = { stack: [], line: 1 }; // TODO: Fix line number counting. + var endStmtPrev = -1; + while (endStmtPrev + 1 < body.length) { + var begStmt = endStmtPrev; + // Scan until we find some statement markup. + begStmt = body.indexOf("{", begStmt + 1); + while (begStmt >= 0) { + var endStmt = body.indexOf('}', begStmt + 1); + var stmt = body.substring(begStmt, endStmt); + var blockrx = stmt.match(/^\{(cdata|minify|eval)/); // From B. Bittman, minify/eval/cdata implementation. + if (blockrx) { + var blockType = blockrx[1]; + var blockMarkerBeg = begStmt + blockType.length + 1; + var blockMarkerEnd = body.indexOf('}', blockMarkerBeg); + if (blockMarkerEnd >= 0) { + var blockMarker; + if( blockMarkerEnd - blockMarkerBeg <= 0 ) { + blockMarker = "{/" + blockType + "}"; + } else { + blockMarker = body.substring(blockMarkerBeg + 1, blockMarkerEnd); + } + + var blockEnd = body.indexOf(blockMarker, blockMarkerEnd + 1); + if (blockEnd >= 0) { + emitSectionText(body.substring(endStmtPrev + 1, begStmt), funcText); + + var blockText = body.substring(blockMarkerEnd + 1, blockEnd); + if (blockType == 'cdata') { + emitText(blockText, funcText); + } else if (blockType == 'minify') { + emitText(scrubWhiteSpace(blockText), funcText); + } else if (blockType == 'eval') { + if (blockText != null && blockText.length > 0) // From B. Bittman, eval should not execute until process(). + funcText.push('_OUT.write( (function() { ' + blockText + ' })() );'); + } + begStmt = endStmtPrev = blockEnd + blockMarker.length - 1; + } + } + } else if (body.charAt(begStmt - 1) != '$' && // Not an expression or backslashed, + body.charAt(begStmt - 1) != '\\') { // so check if it is a statement tag. + var offset = (body.charAt(begStmt + 1) == '/' ? 2 : 1); // Close tags offset of 2 skips '/'. + // 10 is larger than maximum statement tag length. + if (body.substring(begStmt + offset, begStmt + 10 + offset).search(TrimPath.parseTemplate_etc.statementTag) == 0) + break; // Found a match. + } + begStmt = body.indexOf("{", begStmt + 1); + } + if (begStmt < 0) // In "a{for}c", begStmt will be 1. + break; + var endStmt = body.indexOf("}", begStmt + 1); // In "a{for}c", endStmt will be 5. + if (endStmt < 0) + break; + emitSectionText(body.substring(endStmtPrev + 1, begStmt), funcText); + emitStatement(body.substring(begStmt, endStmt + 1), state, funcText, tmplName, etc); + endStmtPrev = endStmt; + } + emitSectionText(body.substring(endStmtPrev + 1), funcText); + if (state.stack.length != 0) + throw new etc.ParseError(tmplName, state.line, "unclosed, unmatched statement(s): " + state.stack.join(",")); + funcText.push("}}; TrimPath_Template_TEMP"); + return funcText.join(""); + } + + var emitStatement = function(stmtStr, state, funcText, tmplName, etc) { + var parts = stmtStr.slice(1, -1).split(' '); + var stmt = etc.statementDef[parts[0]]; // Here, parts[0] == for/if/else/... + if (stmt == null) { // Not a real statement. + emitSectionText(stmtStr, funcText); + return; + } + if (stmt.delta < 0) { + if (state.stack.length <= 0) + throw new etc.ParseError(tmplName, state.line, "close tag does not match any previous statement: " + stmtStr); + state.stack.pop(); + } + if (stmt.delta > 0) + state.stack.push(stmtStr); + + if (stmt.paramMin != null && + stmt.paramMin >= parts.length) + throw new etc.ParseError(tmplName, state.line, "statement needs more parameters: " + stmtStr); + if (stmt.prefixFunc != null) + funcText.push(stmt.prefixFunc(parts, state, tmplName, etc)); + else + funcText.push(stmt.prefix); + if (stmt.suffix != null) { + if (parts.length <= 1) { + if (stmt.paramDefault != null) + funcText.push(stmt.paramDefault); + } else { + for (var i = 1; i < parts.length; i++) { + if (i > 1) + funcText.push(' '); + funcText.push(parts[i]); + } + } + funcText.push(stmt.suffix); + } + } + + var emitSectionText = function(text, funcText) { + if (text.length <= 0) + return; + var nlPrefix = 0; // Index to first non-newline in prefix. + var nlSuffix = text.length - 1; // Index to first non-space/tab in suffix. + while (nlPrefix < text.length && (text.charAt(nlPrefix) == '\n')) + nlPrefix++; + while (nlSuffix >= 0 && (text.charAt(nlSuffix) == ' ' || text.charAt(nlSuffix) == '\t')) + nlSuffix--; + if (nlSuffix < nlPrefix) + nlSuffix = nlPrefix; + if (nlPrefix > 0) { + funcText.push('if (_FLAGS.keepWhitespace == true) _OUT.write("'); + var s = text.substring(0, nlPrefix).replace('\n', '\\n'); // A macro IE fix from BJessen. + if (s.charAt(s.length - 1) == '\n') + s = s.substring(0, s.length - 1); + funcText.push(s); + funcText.push('");'); + } + var lines = text.substring(nlPrefix, nlSuffix + 1).split('\n'); + for (var i = 0; i < lines.length; i++) { + emitSectionTextLine(lines[i], funcText); + if (i < lines.length - 1) + funcText.push('_OUT.write("\\n");\n'); + } + if (nlSuffix + 1 < text.length) { + funcText.push('if (_FLAGS.keepWhitespace == true) _OUT.write("'); + var s = text.substring(nlSuffix + 1).replace('\n', '\\n'); + if (s.charAt(s.length - 1) == '\n') + s = s.substring(0, s.length - 1); + funcText.push(s); + funcText.push('");'); + } + } + + var emitSectionTextLine = function(line, funcText) { + var endMarkPrev = '}'; + var endExprPrev = -1; + while (endExprPrev + endMarkPrev.length < line.length) { + var begMark = "${", endMark = "}"; + var begExpr = line.indexOf(begMark, endExprPrev + endMarkPrev.length); // In "a${b}c", begExpr == 1 + if (begExpr < 0) + break; + if (line.charAt(begExpr + 2) == '%') { + begMark = "${%"; + endMark = "%}"; + } + var endExpr = line.indexOf(endMark, begExpr + begMark.length); // In "a${b}c", endExpr == 4; + if (endExpr < 0) + break; + emitText(line.substring(endExprPrev + endMarkPrev.length, begExpr), funcText); + // Example: exprs == 'firstName|default:"John Doe"|capitalize'.split('|') + var exprArr = line.substring(begExpr + begMark.length, endExpr).replace(/\|\|/g, "#@@#").split('|'); + for (var k in exprArr) { + if (exprArr[k].replace) // IE 5.x fix from Igor Poteryaev. + exprArr[k] = exprArr[k].replace(/#@@#/g, '||'); + } + funcText.push('_OUT.write('); + emitExpression(exprArr, exprArr.length - 1, funcText); + funcText.push(');'); + endExprPrev = endExpr; + endMarkPrev = endMark; + } + emitText(line.substring(endExprPrev + endMarkPrev.length), funcText); + } + + var emitText = function(text, funcText) { + if (text == null || + text.length <= 0) + return; + text = text.replace(/\\/g, '\\\\'); + text = text.replace(/\n/g, '\\n'); + text = text.replace(/"/g, '\\"'); + funcText.push('_OUT.write("'); + funcText.push(text); + funcText.push('");'); + } + + var emitExpression = function(exprArr, index, funcText) { + // Ex: foo|a:x|b:y1,y2|c:z1,z2 is emitted as c(b(a(foo,x),y1,y2),z1,z2) + var expr = exprArr[index]; // Ex: exprArr == [firstName,capitalize,default:"John Doe"] + if (index <= 0) { // Ex: expr == 'default:"John Doe"' + funcText.push(expr); + return; + } + var parts = expr.split(':'); + funcText.push('_MODIFIERS["'); + funcText.push(parts[0]); // The parts[0] is a modifier function name, like capitalize. + funcText.push('"]('); + emitExpression(exprArr, index - 1, funcText); + if (parts.length > 1) { + funcText.push(','); + funcText.push(parts[1]); + } + funcText.push(')'); + } + + var cleanWhiteSpace = function(result) { + result = result.replace(/\t/g, " "); + result = result.replace(/\r\n/g, "\n"); + result = result.replace(/\r/g, "\n"); + result = result.replace(/^(\s*\S*(\s+\S+)*)\s*$/, '$1'); // Right trim by Igor Poteryaev. + return result; + } + + var scrubWhiteSpace = function(result) { + result = result.replace(/^\s+/g, ""); + result = result.replace(/\s+$/g, ""); + result = result.replace(/\s+/g, " "); + result = result.replace(/^(\s*\S*(\s+\S+)*)\s*$/, '$1'); // Right trim by Igor Poteryaev. + return result; + } + + // The DOM helper functions depend on DOM/DHTML, so they only work in a browser. + // However, these are not considered core to the engine. + // + TrimPath.parseDOMTemplate = function(elementId, optDocument, optEtc) { + if (optDocument == null) + optDocument = document; + var element = optDocument.getElementById(elementId); + var content = element.value; // Like textarea.value. + if (content == null) + content = element.innerHTML; // Like textarea.innerHTML. + content = content.replace(/</g, "<").replace(/>/g, ">"); + return TrimPath.parseTemplate(content, elementId, optEtc); + } + + TrimPath.processDOMTemplate = function(elementId, context, optFlags, optDocument, optEtc) { + return TrimPath.parseDOMTemplate(elementId, optDocument, optEtc).process(context, optFlags); + } +}) (); diff --git a/src/public/stylesheets/ovirt-tree/tree.css b/src/public/stylesheets/ovirt-tree/tree.css new file mode 100644 index 0000000..5fefb90 --- /dev/null +++ b/src/public/stylesheets/ovirt-tree/tree.css @@ -0,0 +1,83 @@ +#nav_tree { + padding: 20px; +} + +.nav-tree { + width: 222px; + position: absolute; + overflow: auto; + top: 25px; + bottom: 140px; +} + +.ovirt-tree, .ovirt-tree ul { + list-style: none; + margin:0; + padding:5px 0 5px 16px; + margin-right: 8px; +} + +.ovirt-tree div { + background-repeat: no-repeat; + background-position: left; + padding: 4px 0 4px 28px; + cursor: pointer; +} + +.HardwarePool { + background-image: url('../../images/icon_hdwarepool.png'); +} + +.VmResourcePool { + background-image: url('../../images/icon_vmpool.png'); +} + +.SmartPool { + background-image: url('../../images/icon_smartpool.png'); +} + +.hitarea { + width: 16px; + margin-left: -16px; + float: left; + cursor: pointer; +} + +.expandable { + background: url('../../images/plus.gif') no-repeat left; +} + +.expanded { + background: url('../../images/minus.gif') no-repeat left; +} + +ul.ovirt-tree .current { + background-color: #698FA6; + color:#000000; + width: 100%; +} + +.nav-smart-pool { + position: absolute; + bottom: 0; + left: 0; + height: 130px; + background: #CCCCCC; + width: 218px; + padding: 5px 5px 5px 5px; + overflow: auto; +} + +.nav-smart-pool-header { + color: #666666; +} + +.nav-dashboard { + background-image: url('../../images/icon_dashboard.gif'); + background-repeat: no-repeat; + background-position: left; + padding: 0px 0 0px 28px; + position: absolute; + height: 25px; + top: 0; +} \ No newline at end of file -- 1.5.5.2
Scott Seago
2008-Oct-08 17:21 UTC
[Ovirt-devel] [PATCH server] Version 1 of Revamped Tree Navigation. Take 3
Jason Guiditta wrote:> The new javascript tree widget contains the following features/changes from previous implementation: > > * The html for the list is dynamically generated using a javascript template system. This will allow us to plug in different layouts per tree as the widget matures. > * Updates to the tree are now incremental, rather than a full rip and replace as earlier. We have 2 states we currently look for - 'new' and 'changed'. The first generates new html and appends it to the DOM, the second just does a replacement of the content of existing nodes. > * Vastly simplified the markup and css. > * Added calls where appropriate to refresh the tree before next planned call (for instance, if you add a new hardware pool). > * Added slide effect when opening and closing a node of the tree. > * Clicking the plus/minus opens/close the node only, does not load main content area. > * Clicking anywhere to the right of that on a given node will load content area. > * Added interim icons for 'smartpool' and 'add smartpool' > > Note that aside from the nav area, this should not impact the existing trees which have not been converted yet (all popups that have one), as this is a completely separate codebase with it's own js and css files. > > Related, but not technically part of the tree, I added a choose_layout method to allow testing of javascript components as we are building them to help eliminate possible side effects from other code. When not in a production environment, you can pass in ?component_layout=[name] where [name] is the name of a shell rhtml file you have put in views/layouts/components. As our UI is growing increasingly complex, I think this will be a very useful way to facilitate building components. > >Works for me ACK, take 3