Jason Guiditta
2009-Jan-22 22:31 UTC
[Ovirt-devel] [PATCH server] Create VM/Storage integration w/ tree widget enhancements
== Create VM/ Storage Flow = * Can add as many storage volumes as you need to, flow allows you to return to secondary form w/o limits. * Create VM form, along with its state and state of tree are stored in a temporary area and returned after create storage via pub/sub. == Tree enhancements = * Now stores state of selected (open) nodes, so if we have a need to reload or generate a new tree with certain nodes open, we now can. * Has ability to subscribe to messages via the 'channel' option * Added refresh method, which can be triggered by a message or fired manually. * Refresh adds newly returned items to tree, stubs in place for update and delete. * Added lookup object with references to all items in the tree to allow for easy recreation of tree w/o another call to server. * Added ui_parent and ui_object references (generated in model via a convenience method) to simplify finding nodes to update/add/delete. * Changed click behavior to use event delegation, which means we no longer need to use livequery, which scans the dom for changes. Event delegation is much faster and cleaner (can remove livequery entirely once all trees use widget). == Widget tests/files = * Adds the standard_tree (actual widget vs nav) test harness * Cleans up the tree.rhtml to account for some previous restructuring * Moves sample json data into a 'test' subfolder. I am looking into some more proper javascript testing systems, so this will undoubtedly be changing again in the near future. Signed-off-by: Jason Guiditta <jguiditt at redhat.com> --- src/app/controllers/storage_controller.rb | 20 +- src/app/models/lvm_storage_volume.rb | 3 + src/app/models/pool.rb | 1 + src/app/models/storage_pool.rb | 28 +++- src/app/models/storage_volume.rb | 28 +++- src/app/views/hardware/show_storage.rhtml | 12 +- src/app/views/layouts/_tree.rhtml | 4 +- .../views/layouts/components/standard_tree.rhtml | 202 ++++++++++++++++++++ src/app/views/layouts/components/tree.rhtml | 11 +- src/app/views/smart_pools/show_storage.rhtml | 10 +- src/app/views/storage/new_volume.rhtml | 22 ++- src/app/views/vm/_form.rhtml | 24 ++- src/public/javascripts/ovirt.js | 59 ++++++- src/public/javascripts/ovirt.tree.js | 163 ++++++++++++---- src/public/javascripts/smart_nav_test_data.js | 151 --------------- .../javascripts/test/smart_nav_sample_data.js | 151 +++++++++++++++ .../javascripts/test/storage_tree_sample_data.js | 68 +++++++ src/test/fixtures/storage_volumes.yml | 10 + src/test/unit/storage_volume_test.rb | 21 ++- 19 files changed, 752 insertions(+), 236 deletions(-) create mode 100644 src/app/views/layouts/components/standard_tree.rhtml delete mode 100644 src/public/javascripts/smart_nav_test_data.js create mode 100644 src/public/javascripts/test/smart_nav_sample_data.js create mode 100644 src/public/javascripts/test/storage_tree_sample_data.js diff --git a/src/app/controllers/storage_controller.rb b/src/app/controllers/storage_controller.rb index e4b72f1..3c7d98a 100644 --- a/src/app/controllers/storage_controller.rb +++ b/src/app/controllers/storage_controller.rb @@ -1,4 +1,4 @@ -# +# # Copyright (C) 2008 Red Hat, Inc. # Written by Scott Seago <sseago at redhat.com> # @@ -122,7 +122,8 @@ class StorageController < ApplicationController end def new_volume - @return_facebox = params[:return_facebox] + @return_to_workflow = params[:return_to_workflow] + @return_to_workflow ||= false if params[:storage_pool_id] @storage_pool = StoragePool.find(params[:storage_pool_id]) unless @storage_pool.user_subdividable @@ -170,7 +171,8 @@ class StorageController < ApplicationController respond_to do |format| format.json { render :json => { :object => "storage_volume", :success => true, - :alert => "Storage Volume was successfully created." } } + :alert => "Storage Volume was successfully created.", + :new_volume => @storage_volume.storage_tree_element({:filter_unavailable => false, :state => 'new'})} } format.xml { render :xml => @storage_volume, :status => :created, # FIXME: create storage_volume_url method if relevant @@ -240,7 +242,7 @@ class StorageController < ApplicationController end def edit - render :layout => 'popup' + render :layout => 'popup' end def update @@ -249,11 +251,11 @@ class StorageController < ApplicationController @storage_pool.update_attributes!(params[:storage_pool]) insert_refresh_task end - render :json => { :object => "storage_pool", :success => true, + render :json => { :object => "storage_pool", :success => true, :alert => "Storage Pool was successfully modified." } rescue # FIXME: need to distinguish pool vs. task save errors (but should mostly be pool) - render :json => { :object => "storage_pool", :success => false, + render :json => { :object => "storage_pool", :success => false, :errors => @storage_pool.errors.localize_error_messages.to_a } end end @@ -269,7 +271,7 @@ class StorageController < ApplicationController def addstorage add_internal - render :layout => 'popup' + render :layout => 'popup' end def add @@ -301,10 +303,10 @@ class StorageController < ApplicationController storage_pool.destroy end end - render :json => { :object => "storage_pool", :success => true, + render :json => { :object => "storage_pool", :success => true, :alert => "Storage Pools were successfully deleted." } rescue - render :json => { :object => "storage_pool", :success => true, + render :json => { :object => "storage_pool", :success => true, :alert => "Error deleting storage pools." } end end diff --git a/src/app/models/lvm_storage_volume.rb b/src/app/models/lvm_storage_volume.rb index 38949ce..5b6177b 100644 --- a/src/app/models/lvm_storage_volume.rb +++ b/src/app/models/lvm_storage_volume.rb @@ -35,4 +35,7 @@ class LvmStorageVolume < StorageVolume validates_presence_of :lv_group_perms validates_presence_of :lv_mode_perms + def ui_parent + storage_pool.source_volumes[0][:type].to_s + '_' +storage_pool.source_volumes[0].id.to_s + end end diff --git a/src/app/models/pool.rb b/src/app/models/pool.rb index 62cb732..372cf54 100644 --- a/src/app/models/pool.rb +++ b/src/app/models/pool.rb @@ -180,6 +180,7 @@ class Pool < ActiveRecord::Base ["Disk", :storage_in_gb, "(gb)"]] #needed by tree widget for display + #FIXME: this can be removed once all instances of treeview are gone def hasChildren return (rgt - lft) != 1 end diff --git a/src/app/models/storage_pool.rb b/src/app/models/storage_pool.rb index f9abb55..a021a26 100644 --- a/src/app/models/storage_pool.rb +++ b/src/app/models/storage_pool.rb @@ -1,4 +1,4 @@ -# +# # Copyright (C) 2008 Red Hat, Inc. # Written by Scott Seago <sseago at redhat.com> # @@ -97,16 +97,37 @@ class StoragePool < ActiveRecord::Base false end + #-- + #TODO: the following two methods should be moved out somewhere, perhaps an 'acts_as' plugin? + #Though ui_parent will have class specific impl + #++ + #This is a convenience method for use in the ui to simplify creating a unigue id for placement/retrieval + #in/from the DOM. This was added because there is a chance of duplicate ids between different object types, + #and multiple object type will appear concurrently in the ui. The combination of type and id should be unique. + def ui_object + self.class.to_s + '_' + id.to_s + end + + #This is a convenience method for use in the processing and manipulation of json in the ui. + #This serves as a key both for determining where to attached elements in the DOM and quickly + #accessing and updating a cached object on the client. + def ui_parent + nil + end + def storage_tree_element(params = {}) vm_to_include=params.fetch(:vm_to_include, nil) filter_unavailable = params.fetch(:filter_unavailable, true) include_used = params.fetch(:include_used, false) + state = params.fetch(:state,nil) return_hash = { :id => id, :type => self[:type], - :text => display_name, :name => display_name, + :ui_object => ui_object, + :state => state, :available => false, :create_volume => user_subdividable, + :ui_parent => ui_parent, :selected => false, :is_pool => true} conditions = nil @@ -117,7 +138,8 @@ class StoragePool < ActiveRecord::Base end end if filter_unavailable - availability_conditions = "storage_volumes.state = '#{StoragePool::STATE_AVAILABLE}'" + availability_conditions = "(storage_volumes.state = '#{StoragePool::STATE_AVAILABLE}' + or storage_volumes.state = '#{StoragePool::STATE_PENDING_SETUP}')" if conditions.nil? conditions = availability_conditions else diff --git a/src/app/models/storage_volume.rb b/src/app/models/storage_volume.rb index e289a96..59d166e 100644 --- a/src/app/models/storage_volume.rb +++ b/src/app/models/storage_volume.rb @@ -1,4 +1,4 @@ -# +# # Copyright (C) 2008 Red Hat, Inc. # Written by Scott Seago <sseago at redhat.com> # @@ -97,20 +97,41 @@ class StorageVolume < ActiveRecord::Base storage_pool.user_subdividable and vms.empty? and (lvm_storage_pool.nil? or lvm_storage_pool.storage_volumes.empty?) end + #-- + #TODO: the following two methods should be moved out somewhere, perhaps an 'acts_as' plugin? + #Though ui_parent will have class specific impl + #++ + ##This is a convenience method for use in the ui to simplify creating a unigue id for placement/retrieval + #in/from the DOM. This was added because there is a chance of duplicate ids between different object types, + #and multiple object type will appear concurrently in the ui. The combination of type and id should be unique. + def ui_object + self.class.to_s + '_' + id.to_s + end + + #This is a convenience method for use in the processing and manipulation of json in the ui. + #This serves as a key both for determining where to attached elements in the DOM and quickly + #accessing and updating a cached object on the client. + def ui_parent + storage_pool[:type].to_s + '_' + storage_pool_id.to_s + end + def storage_tree_element(params = {}) vm_to_include=params.fetch(:vm_to_include, nil) filter_unavailable = params.fetch(:filter_unavailable, true) include_used = params.fetch(:include_used, false) vm_ids = vms.collect {|vm| vm.id} + state = params.fetch(:state,'new') return_hash = { :id => id, :type => self[:type], - :text => display_name, + :ui_object => ui_object, + :state => state, :name => display_name, :size => size_in_gb, :available => ((vm_ids.empty?) or (vm_to_include and vm_to_include.id and vm_ids.include?(vm_to_include.id))), :create_volume => supports_lvm_subdivision, + :ui_parent => ui_parent, :selected => (!vm_ids.empty? and vm_to_include and vm_to_include.id and (vm_ids.include?(vm_to_include.id))), :is_pool => false} @@ -126,7 +147,8 @@ class StorageVolume < ActiveRecord::Base end end if filter_unavailable - availability_conditions = "storage_volumes.state = '#{StoragePool::STATE_AVAILABLE}'" + availability_conditions = "(storage_volumes.state = '#{StoragePool::STATE_AVAILABLE}' + or storage_volumes.state = '#{StoragePool::STATE_PENDING_SETUP}')" if conditions.nil? conditions = availability_conditions else diff --git a/src/app/views/hardware/show_storage.rhtml b/src/app/views/hardware/show_storage.rhtml index 5643c83..35f0a55 100644 --- a/src/app/views/hardware/show_storage.rhtml +++ b/src/app/views/hardware/show_storage.rhtml @@ -111,11 +111,13 @@ ${htmlList(pools)} } function storage_select(e, elem) { - $('#storage_tree_form ul.ovirt-tree li div').removeClass('current'); - $(elem) - .addClass('current'); - $('#storage_selection').load('<%= url_for :controller => "search", :action => "single_result" %>', - { class_and_id: elem.id.substring(elem.id.indexOf("_")+1)}); + if ($(e.target).is('div') && $(e.target).parent().is('li')){ + $('#storage_tree_form .current').removeClass('current'); + $(e.target) + .addClass('current'); + $('#storage_selection').load('<%= url_for :controller => "search", :action => "single_result" %>', + { class_and_id: e.target.id.substring(e.target.id.indexOf("_")+1)}); + } } </script> diff --git a/src/app/views/layouts/_tree.rhtml b/src/app/views/layouts/_tree.rhtml index fa3effc..8d5ce51 100644 --- a/src/app/views/layouts/_tree.rhtml +++ b/src/app/views/layouts/_tree.rhtml @@ -27,7 +27,7 @@ }); e.preventDefault(); } - }) + }); $('div.nav-networks a').bind('click', function(e){ if(this === e.target){ var myURL = $(this).attr('href'); @@ -41,7 +41,7 @@ }); e.preventDefault(); } - }) + }); $('#nav_tree_form ul.ovirt-tree li').livequery( function(){ $(this) diff --git a/src/app/views/layouts/components/standard_tree.rhtml b/src/app/views/layouts/components/standard_tree.rhtml new file mode 100644 index 0000000..02ea74e --- /dev/null +++ b/src/app/views/layouts/components/standard_tree.rhtml @@ -0,0 +1,202 @@ +<!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> + <%= stylesheet_link_tag 'ovirt-tree/tree' %> + <%= stylesheet_link_tag 'facebox' %> + + <%= javascript_include_tag "jquery-1.2.6.min.js" -%> + <%= javascript_include_tag "jquery.ui-1.5.2/ui/packed/ui.core.packed.js" -%> + <%= javascript_include_tag "test/storage_tree_sample_data.js" -%> + <%= javascript_include_tag "test/smart_nav_sample_data.js" -%> + <%= javascript_include_tag "jquery.form.js" -%> + <%= javascript_include_tag "trimpath-template-1.0.38.js" %> + <%= javascript_include_tag "ovirt.js" %> + <%= javascript_include_tag "ovirt.tree.js" %> + <%= javascript_include_tag "facebox.js" %> + <script type="text/javascript"> + var currentDomTreeElem = 'storage_volumes_tree', treeObject; + $(document).ready(function(){ + //initialize tree widget + $('#nav_tree').tree({ + content: {"pools" : pools3.pools}, + clickHandler: myClickHandler, + cacheContent: false + }); + /* $('#some_tree').tree({ + content: {"pools" : pools3.pools}, + toggle: myToggler + });*/ + $('#storage_volumes_tree').tree({ + content: {"pools" : storage_pools.pools}, + template : 'storage_tree_template', + clickHandler: myClickHandler, + channel: 'STORAGE_VOLUME', + refresh: refreshPostHandler, + cacheContent: true + }); + $('#swap_button').bind('click', function(event){ + var newDomTreeElem; + if (currentDomTreeElem === 'storage_volumes_tree') { + newDomTreeElem = 'temp_storage_tree'; + } else { newDomTreeElem = 'storage_volumes_tree'} + $('#' + currentDomTreeElem).clone().attr({style: 'background:green',id: newDomTreeElem}).appendTo('body').end().remove(); + currentDomTreeElem = newDomTreeElem; + }); + $('#refresh_button').bind('click', function(event){ + $('ul.ovirt-tree').trigger('STORAGE_VOLUME', data_snippet); + }); + $('#empty_button').bind('click', function(event){ + $('#' + currentDomTreeElem).empty(); + }); + $('#populate_button').bind('click', function(event){ + $('#' + currentDomTreeElem).tree({ + content: treeObject.content, + template : 'storage_tree_template', + clickHandler: myClickHandler, + selectedNodes: treeObject.selectedNodes, + channel: 'STORAGE_VOLUME', + refresh: refreshPostHandler, + cacheContent: true + }); + }); + $('#return_button').bind('click', function(event){ + treeObject = $('#' + currentDomTreeElem).data('tree').options; + }); + $('#toggle_button').bind('click', function(e) { + $('#' + currentDomTreeElem).toggle(); + }); + $('#open_button').bind('click', function(event){ + $('#' + currentDomTreeElem).tree('openToSelected'); + }); + $('#show_button').bind('click', function(event){ + $.facebox(''); + $('#' + currentDomTreeElem).clone().appendTo('td.body > div.content').end().remove(); + $('#populate_button').trigger('click'); + }); + }); + + function myToggler(e, elem) { + if ($(e.target).is('span.hitarea')){ + alert('Yay, I am a custom toggler!'); + } + } + + function refreshPostHandler(e, self, data) { + //alert('I am ' + self.element.get(0).id + ', and I have a parent of ' + self.getData('sourceID')); + } + + function myClickHandler(e, elem) { + //VmCreator.test(elem.element.get(0).id); + if ($(e.target).is('div') && $(e.target).parent().is('li')){ + $(e.target) + .toggleClass('current'); + } + if ($(e.target).is('img') && $(e.target).parent().is('a')){ + alert('I got yer link right hea! (' + e.target + ')'); + e.preventDefault(); + } + } + + var data_snippet = [{ + "selected":false, + "name":"LVM: test_lvm:test_lun3_2", + "available":true, + "children":[], + "create_volume":false, + "id":13, + "type":"LvmStorageVolume", + "ui_object": "LvmStorageVolume_13", + "ui_parent": "IscsiStorageVolume_4" + }]; + + </script> + </head> + + <body> + <button id="swap_button">Swap Tree</button> + <button id="refresh_button">refresh</button> + <button id="empty_button">Empty Tree</button> + <button id="populate_button">Populate Storage Tree</button> + <button id="return_button">Store Tree Info</button> + <button id="toggle_button">Toggle Visibility</button> + <button id="open_button">Open To Selected</button> + <button id="show_button">Show tree in facebox</button> + <form id="nav_tree_form"> + <ul id="nav_tree" class="ovirt-tree"></ul> + </form> + <br/><br/><br/><br/> + <form id="some_tree_form"> + <ul id="some_tree" class="ovirt-tree"></ul> + </form> + <textarea id="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> + + <div id="tree_placeholder"> + <form id="storage_tree_form"> + My Name: <input type="text" name="my_name"/> + <div> + <ul id="storage_volumes_tree" class="ovirt-tree"></ul> + </div> + </form> + </div> + <textarea id="storage_tree_template" style="display:none;"> + {macro htmlList(list, id, isFullList)} + {if isFullList} + <ul style="display:none;"> + {/if} + {for item in list} + <li> + <span class="hitarea {if item.children.length > 0} expandable{/if}"> </span> + <div id="${id}-${item.ui_object}" class="{if !item.available} unclickable{/if}"> + <input type="checkbox" name="vm[storage_volume_ids][]" value="${item.id}" + {if !item.available}disabled="disabled" style="display:none"{/if} + {if item.selected}checked="checked"{/if}/> ${item.name} {if item.size}(${item.size} GB){/if} + {if item.create_volume} + <input type="hidden" name="return_item" value="true"/><%=image_tag("icon_addstorage.png")%> + <a href="<%= url_for :controller => 'storage', :action => 'new_volume'%>?source_volume_id=${item.id}" + class="selection_facebox"></a> + {/if} + </div> + {if item.children.length > 0} + ${htmlList(item.children, id, true)} + {/if} + </li> + {/for} + {if isFullList} + </ul> + {/if} + {/macro} + + ${htmlList(pools, id)} + </textarea> + </body> +</html> diff --git a/src/app/views/layouts/components/tree.rhtml b/src/app/views/layouts/components/tree.rhtml index 063a6df..402c6e3 100644 --- a/src/app/views/layouts/components/tree.rhtml +++ b/src/app/views/layouts/components/tree.rhtml @@ -6,10 +6,15 @@ <head> <meta http-equiv="content-type" content="text/html;charset=UTF-8" /> <title><%= yield :title -%></title> + <%= stylesheet_link_tag 'ovirt-tree/tree' %> + <%= 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.ui-1.5.2/ui/packed/ui.core.packed.js" -%> + <%= javascript_include_tag "test/smart_nav_sample_data.js" -%> <%= javascript_include_tag "jquery.form.js" -%> + <%= javascript_include_tag "trimpath-template-1.0.38.js" %> + <%= javascript_include_tag "ovirt.tree.js" %> <%= javascript_include_tag "ovirt.js" -%> <script type="text/javascript"> @@ -43,12 +48,8 @@ <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"> diff --git a/src/app/views/smart_pools/show_storage.rhtml b/src/app/views/smart_pools/show_storage.rhtml index 68f1b48..2549e7d 100644 --- a/src/app/views/smart_pools/show_storage.rhtml +++ b/src/app/views/smart_pools/show_storage.rhtml @@ -63,11 +63,13 @@ ${htmlList(pools)} } function smart_storage_select(e, elem) { - $('#smart_storage_tree_form ul.ovirt-tree li div').removeClass('current'); - $(elem) + if ($(e.target).is('div') && $(e.target).parent().is('li')){ + $('#smart_storage_tree_form .current').removeClass('current'); + $(e.target) .addClass('current'); - $('#smart_storage_selection').load('<%= url_for :controller => "search", :action => "single_result" %>', - { class_and_id: elem.id.substring(elem.id.indexOf("_")+1)}); + $('#smart_storage_selection').load('<%= url_for :controller => "search", :action => "single_result" %>', + { class_and_id: e.target.id.substring(e.target.id.indexOf("_")+1)}); + } } </script> diff --git a/src/app/views/storage/new_volume.rhtml b/src/app/views/storage/new_volume.rhtml index 958c463..2e49d16 100644 --- a/src/app/views/storage/new_volume.rhtml +++ b/src/app/views/storage/new_volume.rhtml @@ -18,20 +18,26 @@ </div> </div> <!-- FIXME: need to pop up the details dialog again --> - <%= popup_footer("$('#storage_volume_form').submit()", "New Storage Volume") %> + <% if @return_to_workflow %> + <%# TODO: update this method in application_helper to take an array, so we can include + a callback or trigger to to go previous step in flow. %> + <%= popup_footer("$('#storage_volume_form').submit()", "New Storage Volume") %> + <% else %> + <%= popup_footer("$('#storage_volume_form').submit()", "New Storage Volume") %> + <% end %> </form> </div> <script type="text/javascript"> function afterStorageVolume(response, status){ ajax_validation(response, status); if (response.success) { - <% if @return_facebox %> - $('#window').fadeOut('fast'); - $("#window").empty().load("<%= @return_facebox %>") - $('#window').fadeIn('fast'); - <% else %> - $(document).trigger('close.facebox'); - <% end %> + //this is where I want to publish to... + //$(document).trigger('STORAGE_VOLUME', [response.new_volume]); + //but it only picks up correctly right now if I push it here, so this needs to change later + $('ul.ovirt-tree').trigger('STORAGE_VOLUME', [response.new_volume]); + <% unless @return_to_workflow -%> + $(document).trigger('close.facebox'); + <% end -%> } } $(function() { diff --git a/src/app/views/vm/_form.rhtml b/src/app/views/vm/_form.rhtml index 523e81e..fb3d137 100644 --- a/src/app/views/vm/_form.rhtml +++ b/src/app/views/vm/_form.rhtml @@ -37,7 +37,7 @@ <div class="clear_row" style="height:15px;"></div> <div class="clear_row"></div> <div class="clear_row"></div> - + <div class="form_heading">Network</div> <div class="clear_row"></div> <div class="clear_row"></div> @@ -56,21 +56,27 @@ <!--[eoform:vm]--> -<textarea id="tree_template" style="display:none;"> -{macro htmlList(list, isFullList)} +<textarea id="storage_volumes_template" style="display:none;"> +{macro htmlList(list, id, isFullList)} {if isFullList} <ul style="display:none;"> {/if} {for item in list} <li> <span class="hitarea {if item.children.length > 0} expandable{/if}"> </span> - <div id="move-${item.id}" class="{if !item.available} unclickable{/if}"> + <div id="${id}-${item.ui_object}" class="{if !item.available} unclickable{/if}"> <input type="checkbox" name="vm[storage_volume_ids][]" value="${item.id}" {if !item.available}disabled="disabled" style="display:none"{/if} {if item.selected}checked="checked"{/if}/> ${item.name} {if item.size}(${item.size} GB){/if} + {if item.create_volume} + <input type="hidden" name="return_item" value="true"/> + <%=image_tag("icon_addstorage.png")%> + <a href="<%= url_for :controller => 'storage', :action => 'new_volume'%>?source_volume_id=${item.id}&return_to_workflow=true" + rel="facebox[.bolder]" class="selection_facebox"></a> + {/if} </div> {if item.children.length > 0} - ${htmlList(item.children, true)} + ${htmlList(item.children, id, true)} {/if} </li> {/for} @@ -79,12 +85,16 @@ {/if} {/macro} -${htmlList(pools)} +${htmlList(pools, id)} </textarea> <script type="text/javascript"> $(document).ready(function(){ $('#storage_volumes_tree').tree({ - content: {"pools" : <%= @storage_tree%>} + content: {"pools" : <%= @storage_tree%>}, + template: "storage_volumes_template", + clickHandler: VmCreator.goToCreateStorageHandler, + channel: 'STORAGE_VOLUME', + refresh: VmCreator.returnToVmForm }); }); </script> diff --git a/src/public/javascripts/ovirt.js b/src/public/javascripts/ovirt.js index a43f004..09d8bdc 100644 --- a/src/public/javascripts/ovirt.js +++ b/src/public/javascripts/ovirt.js @@ -173,7 +173,7 @@ function afterHwPool(response, status){ $tabs.tabs("load",$tabs.data('selected.tabs')); } } - + //FIXME: point all these refs at a widget so we dont need the functions in here processTree(); @@ -322,4 +322,61 @@ function afterNetwork(response, status){ function handleTabsAndContent(data) { $('#side-toolbar').html($(data).find('div.toolbar')); $('#tabs-and-content-container').html($(data).not('div#side-toolbar')); +} + +var VmCreator = { + checkedBoxesFromTree : [], + buildCheckboxList: function(id) { + var rawList = $('#'+ id + ' :checkbox:checked').parent('div'); + if (rawList.length >0) { + rawList.each(function(i) { + VmCreator.checkedBoxesFromTree.push(rawList.get(i).id); + }); + } else { + VmCreator.checkedBoxesFromTree.splice(0); + } + }, + clickCheckboxes: function() { + $.each(VmCreator.checkedBoxesFromTree, function(n, curBox){ + $('#' + curBox).children(':checkbox').click(); + }); + VmCreator.checkedBoxesFromTree = []; + }, + recreateTree: function(o){ + $('#storage_volumes_tree').tree({ + content: o.content, + template: "storage_volumes_template", + selectedNodes: o.selectedNodes, + clickHandler: VmCreator.goToCreateStorageHandler, + channel: 'STORAGE_VOLUME', + refresh: VmCreator.returnToVmForm + }); + }, + goToCreateStorageHandler: function goToCreateStorageHandler(e,elem){ + if ($(e.target).is('img') && $(e.target).parent().is('div')){ + //remove the temp form in case there is one hanging around for some reason + $('temp_create_vm_form').remove(); + VmCreator.buildCheckboxList(elem.element.get(0).id); + var storedOptions = $('#storage_volumes_tree').data('tree').options; + // copy/rename form + $('#window').clone(true).attr({style: 'display:none', id: 'temp_window'}).appendTo('body'); + $('#temp_window #vm_form').attr({id: 'temp_create_vm_form'}); + // continue standard calls to go to next step (create storage) + $(e.target).siblings('a').click(); + // empty tree + $('#temp_create_vm_form #storage_volumes_tree').empty(); + // reinitialize tree so it has data and is subscribed + VmCreator.recreateTree(storedOptions); + } + }, + returnToVmForm: function returnToVmForm(e,elem) { + //The item has now been added to the tree, now copy it into a facebox + var storedOptions = $('#storage_volumes_tree').data('tree').options; + $('#window').remove(); + $('#temp_window').clone(true).attr({style: 'display:block', id: 'window'}) + .appendTo('td.body > div.content').end().remove(); + $('#window #temp_create_vm_form').attr({id: 'vm_form'}); + VmCreator.recreateTree(storedOptions); + VmCreator.clickCheckboxes(); + } } \ No newline at end of file diff --git a/src/public/javascripts/ovirt.tree.js b/src/public/javascripts/ovirt.tree.js index 77d6c55..48f529c 100644 --- a/src/public/javascripts/ovirt.tree.js +++ b/src/public/javascripts/ovirt.tree.js @@ -75,60 +75,149 @@ function processChildren(list, templateObj){ this.setData('template', TrimPath.parseDOMTemplate(this.getData('template'))); }, init: function() { + var self = this, o = this.options; this.setTemplate(this.getTemplate()); - this.element.html(this.getTemplate().process(this.getData('content'))); - var self = this; + this.populate(); this.element - .find('li:has(ul)') - .children('span.hitarea') - .click(function(event){ - if (this == event.target) { - if($(this).siblings('ul').size() >0) { - if(self.getData('toggle') === 'toggle') { - self.toggle(event, this); //we need 'this' so we have the right element to toggle - } else { - self.element.triggerHandler('toggle',[event,this],self.getData('toggle')); - } - } - } + .bind('click', function(event){ + self.clickHandler(event, self); + if(self.getData('toggle') === 'toggle') { + self.toggle(event, this); + } else { + self.element.triggerHandler('toggle',[event,this],self.getData('toggle')); + } }); - this.element - .find('li > div') - .filter(':not(.unclickable)') - .bind('click', function(event) { - if (this == event.target) { - if(self.getData('clickHandler') === 'clickHandler') { - self.clickHandler(event, this); //we need 'this' so we have the right element to add click behavior to - } else { - self.element.triggerHandler('clickHandler',[event,this],self.getData('clickHandler')); - } - } + o.selectedNodes !== undefined? this.openToSelected() :o.selectedNodes=[]; + o.channel !== undefined? this.subscribe(o.channel): o.channel = ''; + if (o.cacheContent === true) this.buildLookup(); + }, + populate: function() { + var contentWithId = this.getData('content'); + contentWithId.id = this.element.get(0).id; + this.element.html(this.getTemplate().process(contentWithId)); + }, + buildLookup: function() { + this.setData('lookupList', this.walkTree(this.getData('content').pools, [], this)); + }, + walkTree: function(list, lookup, self) { + $.each(list, function(n,obj){ + lookup.push(obj); + if (obj.children.length > 0) self.walkTree(obj.children, lookup, self); }); - this.openToSelected(self); + return lookup; + }, + subscribe: function subscribe(channel) { + var self = this; + this.element.bind(channel, function(e,data){self.refresh(e,data);}); }, toggle: function(e, elem) { - $(elem) + if ($(e.target).is('span.hitarea')){ + $(e.target) .toggleClass('expanded') .toggleClass('expandable') .siblings('ul').slideToggle("normal"); + if ($(e.target).hasClass('expanded')) { + this.setSelectedNode(this.chop(e.target), true); + } else { + this.setSelectedNode(this.chop(e.target), false); + } + } + }, + chop: function(elem) { + var id = $(elem).siblings('div').get(0).id; + return id.substring(id.indexOf('-') +1); + }, + clickHandler: function(e,elem) { //TODO: make this a default impl if needed. + this.options.clickHandler !== undefined? this.element.triggerHandler('clickHandler',[e,this],this.getData('clickHandler')): null; + if ($(e.target).is('div') && $(e.target).parent().is('li')){} + }, + setSelectedNode: function(id, isOpen) { + if (isOpen) { + if($.inArray(id,this.getData('selectedNodes')) == -1){ + this.setData(this.getData('selectedNodes').push(id)); + } + } else { + if($.inArray(id,this.getData('selectedNodes')) != -1){ + this.setData(this.getData('selectedNodes').splice(this.getData('selectedNodes').indexOf(id),1)); + } + } + }, + openToSelected: function() { + for (var i = 0; i < this.getData('selectedNodes').length; i++){ + this.toggle($.event.fix({type: 'toggle', + target: this.element.find('#' +this.element.get(0).id + '-' + this.getData('selectedNodes')[i]).siblings('span').get(0)}) + , this); + } + }, + refresh: function(e, list) { + //NOTE: The widget expects the convention used elsewhere of {blah}-{ui_object} + //(where {blah} is the id of the container element, see above for an example soon), + //since there may be 2 items with the same db id. + var self = this; + list = $.makeArray(list); + $.each(list, function(n,data){ + switch(data.state) { + case 'deleted': { + self._delete(data); + break; + } + case 'changed': { + self._update(data); + break; + } + default: { + self._add(data); + break; + } + } + }); + self.options.refresh !== undefined? self.element.triggerHandler('refresh',[e,list],self.getData('refresh')): null; }, - clickHandler: function(e,elem) { - // make this a default impl if needed. + //methods meant to be called internally by widget + _add: function(data){ + var myLookupList = this.getData('lookupList'); + if (data.ui_parent !==null) { + var matchedItems = $.grep(myLookupList,function(value) {return value.ui_object == data.ui_parent;}); + var self = this; + $.each(matchedItems, function(n,obj){ + var existingObj = []; + if(obj.children.length >0) { + existingObj = $.grep(obj.children,function(value) {return value.ui_object == data.ui_object;}); + } + if (existingObj.length === 0){ + obj.children.push(data); + myLookupList.push(data); + self._addDomElem(data); + } else {} + }); + } else {myLookupList.push(data);} }, - openToSelected: function(self) { - //find 'selected' items and open tree accordingly. This may need to have a - //marker of some sort passed in since different trees may have different needs. + _delete: function(data){}, //TODO: implement + _update: function(data) {}, //TODO: implement + _addDomElem: function(data) { + var dataToInsert = this.getTemplate().process({"pools":[data], "id":this.element.get(0).id}); + if (data.ui_parent) { + var searchString = '#' + this.element.get(0).id + '-' + data.ui_parent; + var parentElem = this.element.find(searchString).siblings('ul'); + if (parentElem.size() === 0) { + this.element.find(searchString).parent().append('<ul>' + dataToInsert + '</ul>'); + this.element.find(searchString).siblings('span').addClass('expanded'); + } else { + parentElem.append(dataToInsert); + } + } else { + this.element.append(dataToInsert); + } }, - off: function() { - this.element.css({background: 'none'}); - this.destroy(); // use the predefined function - } + _deleteDomElem: function(data) {}, //TODO: implement + _updateDomElem: function(data) {} //TODO: implement }; $.yi = $.yi || {}; // create the namespace $.widget("yi.tree", Tree); $.yi.tree.defaults = { template: 'tree_template', toggle: 'toggle', - clickHandler: 'clickHandler' + clickHandler: 'clickHandler', + cacheContent: true }; })(jQuery); \ 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 deleted file mode 100644 index 43e7dbc..0000000 --- a/src/public/javascripts/smart_nav_test_data.js +++ /dev/null @@ -1,151 +0,0 @@ -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/test/smart_nav_sample_data.js b/src/public/javascripts/test/smart_nav_sample_data.js new file mode 100644 index 0000000..43e7dbc --- /dev/null +++ b/src/public/javascripts/test/smart_nav_sample_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/test/storage_tree_sample_data.js b/src/public/javascripts/test/storage_tree_sample_data.js new file mode 100644 index 0000000..f798a45 --- /dev/null +++ b/src/public/javascripts/test/storage_tree_sample_data.js @@ -0,0 +1,68 @@ +var storage_pools = {"pools": +[ + { + "selected":false, + "name":"iSCSI: 192.168.50.2:ovirtpriv:storage", + "available":false, + "children": + [ + { + "selected":false, + "name":"iSCSI: 192.168.50.2:ovirtpriv:storage:lun-2", + "available":true, + "children":[], + "create_volume":true, + "text":"iSCSI: 192.168.50.2:ovirtpriv:storage:lun-2", + "id":5, + "type":"IscsiStorageVolume", + "ui_object": "IscsiStorageVolume_5", + "ui_parent": "IscsiStoragePool_2" + }, + + { + "selected":false, + "name":"iSCSI: 192.168.50.2:ovirtpriv:storage:lun-3", + "available":true, + "children":[], + "create_volume":true, + "text":"iSCSI: 192.168.50.2:ovirtpriv:storage:lun-3", + "id":4, + "type":"IscsiStorageVolume", + "ui_object": "IscsiStorageVolume_4", + "ui_parent": "IscsiStoragePool_2" + } + ], + "create_volume":false, + "text":"iSCSI: 192.168.50.2:ovirtpriv:storage", + "id":2, + "type":"IscsiStoragePool", + "ui_object": "IscsiStoragePool_2", + "ui_parent": null + }, + + { + "selected":false, + "name":"iSCSI: 192.68.60.2:/fred", + "available":false, + "children":[], + "create_volume":false, + "text":"iSCSI: 192.68.60.2:/fred", + "id":7, + "type":"IscsiStoragePool", + "ui_object": "IscsiStoragePool_7", + "ui_parent": null + }, + + { + "selected":false, + "name":"iSCSI: 192.168.60.4:/mo", + "available":false, + "children":[], + "create_volume":false, + "text":"iSCSI: 192.168.60.4:/mo", + "id":6, + "type":"IscsiStoragePool", + "ui_object": "IscsiStoragePool_6", + "ui_parent": null + } +]} \ No newline at end of file diff --git a/src/test/fixtures/storage_volumes.yml b/src/test/fixtures/storage_volumes.yml index a3711bf..8e5b60a 100644 --- a/src/test/fixtures/storage_volumes.yml +++ b/src/test/fixtures/storage_volumes.yml @@ -19,6 +19,16 @@ ovirtpriv_storage_lun_3: storage_pool: corp_com_ovirtpriv_storage type: IscsiStorageVolume state: available + lvm_pool_id: corp_com_dev_lvm_ovirtlvm +ovirtpriv_lvm_volume_1: + size: 1048576 + path: /dev/disk/by-id/scsi-S_beaf321013 + storage_pool: corp_com_dev_lvm_ovirtlvm + type: LvmStorageVolume + state: available + lv_owner_perms: 0744 + lv_group_perms: 0744 + lv_mode_perms: 0744 ovirt_nfs_disk_1: size: 3145728 path: /mnt/a3q49Sj4Sfmae1bV/disk1.dsk diff --git a/src/test/unit/storage_volume_test.rb b/src/test/unit/storage_volume_test.rb index 3978685..16be0fb 100644 --- a/src/test/unit/storage_volume_test.rb +++ b/src/test/unit/storage_volume_test.rb @@ -1,4 +1,4 @@ -# +# # Copyright (C) 2008 Red Hat, Inc. # Written by Scott Seago <sseago at redhat.com> # @@ -112,4 +112,23 @@ class StorageVolumeTest < Test::Unit::TestCase assert_equal @storage_volume.movable?, false, "Storage volume w/ vms should not be movable" end + def test_create_valid_lvm_volume +# FIXME: Write this test, using similer steps as in storage_controller#new_volume. +# Also add validation to model to make sure the lvm volume's lvm pool has a source volume + end + def test_return_correct_lvm_ui_parent + #test lvm volume values + assert_equal storage_volumes(:ovirtpriv_storage_lun_3).type.to_s + '_' +storage_volumes(:ovirtpriv_storage_lun_3).id.to_s, + storage_volumes(:ovirtpriv_lvm_volume_1).ui_parent, + 'Incorrect ui parent returned' + #test isci volume values + assert_equal storage_volumes(:corp_com_ovirtpriv_storage).type.to_s + '_' +storage_volumes(:corp_com_ovirtpriv_storage).id.to_s, + storage_volumes(:ovirtpriv_storage_lun_3).ui_parent, + 'Incorrect ui parent returned' + #test nfs volume values + assert_equal storage_volumes(:corp_com_nfs_ovirtnfs).type.to_s + '_' +storage_volumes(:corp_com_nfs_ovirtnfs).id.to_s, + storage_volumes(:ovirt_nfs_disk_3).ui_parent, + 'Incorrect ui parent returned' + end + end -- 1.5.6.6
Scott Seago
2009-Jan-23 21:37 UTC
[Ovirt-devel] [PATCH server] Create VM/Storage integration w/ tree widget enhancements
Jason Guiditta wrote:> == Create VM/ Storage Flow => * Can add as many storage volumes as you need to, flow allows you to > return to secondary form w/o limits. > * Create VM form, along with its state and state of tree are stored in > a temporary area and returned after create storage via pub/sub. > > diff --git a/src/app/views/vm/_form.rhtml b/src/app/views/vm/_form.rhtml > index 523e81e..fb3d137 100644 > --- a/src/app/views/vm/_form.rhtml > +++ b/src/app/views/vm/_form.rhtml > @@ -56,21 +56,27 @@ > > <!--[eoform:vm]--> > > -<textarea id="tree_template" style="display:none;"> > -{macro htmlList(list, isFullList)} > +<textarea id="storage_volumes_template" style="display:none;"> > +{macro htmlList(list, id, isFullList)} > {if isFullList} > <ul style="display:none;"> > {/if} > {for item in list} > <li> > <span class="hitarea {if item.children.length > 0} expandable{/if}"> </span> > - <div id="move-${item.id}" class="{if !item.available} unclickable{/if}"> > + <div id="${id}-${item.ui_object}" class="{if !item.available} unclickable{/if}"> > <input type="checkbox" name="vm[storage_volume_ids][]" value="${item.id}" > {if !item.available}disabled="disabled" style="display:none"{/if} > {if item.selected}checked="checked"{/if}/> ${item.name} {if item.size}(${item.size} GB){/if} > + {if item.create_volume} > + <input type="hidden" name="return_item" value="true"/> > + <%=image_tag("icon_addstorage.png")%> > + <a href="<%= url_for :controller => 'storage', :action => 'new_volume'%>?source_volume_id=${item.id}&return_to_workflow=true" > + rel="facebox[.bolder]" class="selection_facebox"></a> >This needs the fix you sent me earlier for NFS. Other than that the new stuff is working basically well. Two things to work through before pushing though: 1) the 'move' dialog (for hosts and storage) hasn't been updated to use the new tree 2) the facebox overlay component is being added again for each new Volume form without being removed. Scott