diff options
author | Nuo Yan <nuo@opscode.com> | 2011-02-04 15:33:06 -0800 |
---|---|---|
committer | Nuo Yan <nuo@opscode.com> | 2011-02-04 15:33:06 -0800 |
commit | cec3fcd58261a7544dac38344b08c17bcfb3d1c4 (patch) | |
tree | 36f098e2dc822e5e548f023cd2de72491192de54 /chef-server-webui | |
parent | 4f00cfd3bf59460ea1f8cb270e61cd423e329fe9 (diff) | |
download | chef-cec3fcd58261a7544dac38344b08c17bcfb3d1c4.tar.gz |
CHEF-1999 Environment LCRUD WebUI
Diffstat (limited to 'chef-server-webui')
15 files changed, 652 insertions, 112 deletions
diff --git a/chef-server-webui/app/controllers/application.rb b/chef-server-webui/app/controllers/application.rb index 5c3ad0a266..34e1d75236 100644 --- a/chef-server-webui/app/controllers/application.rb +++ b/chef-server-webui/app/controllers/application.rb @@ -175,9 +175,9 @@ class Application < Merb::Controller def append_tree(name, html, node, count, parent) to_do = node #to_do = node.kind_of?(Chef::Node) ? node.attribute : node - Chef::Log.error("I have #{to_do.inspect}") + Chef::Log.debug("I have #{to_do.inspect}") to_do.sort{ |a,b| a[0] <=> b[0] }.each do |key, value| - Chef::Log.error("I am #{key.inspect} #{value.inspect}") + Chef::Log.debug("I am #{key.inspect} #{value.inspect}") to_send = Array.new count += 1 is_parent = false @@ -267,4 +267,27 @@ class Application < Merb::Controller string.to_s.gsub(/\n/, '<br />') unless string.nil? end + def format_exception(exception) + require 'pp' + pretty_params = StringIO.new + PP.pp({:request_params => params}, pretty_params) + "#{exception.class.name}: #{exception.message}\n#{pretty_params.string}\n#{exception.backtrace.join("\n")}" + end + + def conflict?(exception) + exception.kind_of?(Net::HTTPServerException) && exception.message =~ /409/ + end + + def forbidden?(exception) + exception.kind_of?(Net::HTTPServerException) && exception.message =~ /403/ + end + + def not_found?(exception) + exception.kind_of?(Net::HTTPServerException) && exception.message =~ /404/ + end + + def bad_request?(exception) + exception.kind_of?(Net::HTTPServerException) && exception.message =~ /400/ + end + end diff --git a/chef-server-webui/app/controllers/cookbooks.rb b/chef-server-webui/app/controllers/cookbooks.rb index 66dd1e35d9..4c6065f6dc 100644 --- a/chef-server-webui/app/controllers/cookbooks.rb +++ b/chef-server-webui/app/controllers/cookbooks.rb @@ -8,9 +8,9 @@ # 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. @@ -22,17 +22,18 @@ require 'chef/cookbook_loader' require 'chef/cookbook_version' class Cookbooks < Application - + provides :html before :login_required before :params_helper - + attr_reader :cookbook_id def params_helper @cookbook_id = params[:id] || params[:cookbook_id] end - + def index + provides :html, :json # Because the Environment web form needs to retrieve the list of cookbooks, this method needs to provide both html and json results. @cl = begin if session[:environment] result = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("environments/#{session[:environment]}/cookbooks") @@ -48,23 +49,23 @@ class Cookbooks < Application Chef::Log.error("#{e}\n#{e.backtrace.join("\n")}") @_message = {:error => $!} {} - end - render + end + display @cl end def show begin # array of versions, sorted from large to small e.g. ["0.20.0", "0.1.0"] - versions = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("cookbooks/#{cookbook_id}")[cookbook_id].sort!{|x,y| y <=> x } + versions = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("cookbooks/#{cookbook_id}")[cookbook_id].sort!{|x,y| y <=> x } # if version is not specified in the url, get the most recent version, otherwise get the specified version version = if params[:cb_version].nil? || params[:cb_version].empty? versions.first else params[:cb_version] end - + @cookbook = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("cookbooks/#{cookbook_id}/#{version}") - + # by default always show the largest version number (assuming largest means most recent) @other_versions = versions - [version] raise NotFound unless @cookbook @@ -76,14 +77,22 @@ class Cookbooks < Application @_message = {:error => $!} @cl = {} render :index - end + end + end + + # GET /cookbooks/cookbook_id +``# provides :json, for the javascript on the environments web form. + def cb_versions + provides :json + @versions = {cookbook_id => get_versions} + display @versions end - + def recipe_files - # node = params.has_key?('node') ? params[:node] : nil + # node = params.has_key?('node') ? params[:node] : nil # @recipe_files = load_all_files(:recipes, node) r = Chef::REST.new(Chef::Config[:chef_server_url]) - @recipe_files = r.get_rest("cookbooks/#{params[:id]}/recipes") + @recipe_files = r.get_rest("cookbooks/#{params[:id]}/recipes") display @recipe_files end @@ -92,17 +101,23 @@ class Cookbooks < Application @recipe_files = r.get_rest("cookbooks/#{params[:id]}/attributes") display @attribute_files end - + def definition_files r = Chef::REST.new(Chef::Config[:chef_server_url]) @recipe_files = r.get_rest("cookbooks/#{params[:id]}/definitions") display @definition_files end - + def library_files r = Chef::REST.new(Chef::Config[:chef_server_url]) @recipe_files = r.get_rest("cookbooks/#{params[:id]}/libraries") display @lib_files end - + + private + + def get_versions + Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("cookbooks/#{cookbook_id}")[cookbook_id].sort!{|x,y| y <=> x } + end + end diff --git a/chef-server-webui/app/controllers/environments.rb b/chef-server-webui/app/controllers/environments.rb index 6c8c801e01..aacc6ff018 100644 --- a/chef-server-webui/app/controllers/environments.rb +++ b/chef-server-webui/app/controllers/environments.rb @@ -45,37 +45,64 @@ class Environments < Application # GET /environemnts/new def new @environment = Chef::Environment.new + load_cookbooks render :new end # POST /environments def create @environment = Chef::Environment.new - if @environment.update_from_params(params) - @environment.save - render :show + if @environment.update_from_params(processed_params=process_params) + begin + @environment.create + redirect(url(:environments), :message => { :notice => "Created Environment #{@environment.name}" }) + rescue Net::HTTPServerException => e + if conflict?(e) + Chef::Log.debug("Got 409 conflict creating environment #{params[:name]}\n#{format_exception(e)}") + redirect(url(:new_environment), :message => { :error => "An environment with that name already exists"}) + elsif forbidden?(e) + # Currently it's not possible to get 403 here. I leave the code here for completeness and may be useful in the future.[nuo] + Chef::Log.debug("Got 403 forbidden creating environment #{params[:name]}\n#{format_exception(e)}") + redirect(url(:new_environment), :message => { :error => "Permission Denied. You do not have permission to create an environment."}) + else + Chef::Log.error("Error communicating with the API server\n#{format_exception(e)}") + raise + end + end else - raise "TODO" + load_cookbooks + # By rendering :new, the view shows errors from @environment.invalid_fields render :new - # redirect to new, tell them what they did wrong end end # GET /environments/:id/edit def edit load_environment + load_cookbooks render end # PUT /environments/:id def update load_environment - @environment.update_from_params(params) - if @environment.invalid_fields.empty? #success - @environment.save - render :show + if @environment.update_from_params(process_params(params[:id])) + begin + @environment.save + redirect(url(:environment, @environment.name), :message => { :notice => "Updated Environment #{@environment.name}" }) + rescue Net::HTTPServerException => e + if forbidden?(e) + # Currently it's not possible to get 403 here. I leave the code here for completeness and may be useful in the future.[nuo] + Chef::Log.debug("Got 403 forbidden updating environment #{params[:name]}\n#{format_exception(e)}") + redirect(url(:edit_environment), :message => { :error => "Permission Denied. You do not have permission to update an environment."}) + else + Chef::Log.error("Error communicating with the API server\n#{format_exception(e)}") + raise + end + end else - @environment.update_from_params(params) + load_cookbooks + # By rendering :new, the view shows errors from @environment.invalid_fields render :edit end end @@ -160,4 +187,33 @@ class Environments < Application end end + def load_cookbooks + begin + # @cookbooks is a hash, keys are cookbook names, values are their URIs. + @cookbooks = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("cookbooks").keys + rescue Net::HTTPServerException => e + Chef::Log.error(format_exception(e)) + redirect(url(:new_environment), :message => { :error => "Could not load the list of available cookbooks."}) + end + end + + def process_params(name=params[:name]) + {:name => name, :description => params[:description], :attributes => params[:attributes], :cookbook_version => search_params_for_cookbook_version_constraints} + end + + def search_params_for_cookbook_version_constraints + cookbook_version_constraints = {} + index = 0 + params.each do |k,v| + cookbook_name_box_id = k[/cookbook_name_(\d+)/, 1] + unless cookbook_name_box_id.nil? || v.nil? || v.empty? + cookbook_version_constraints[index] = v + " " + params["operator_#{cookbook_name_box_id}"] + " " + params["cookbook_version_#{cookbook_name_box_id}"].strip # e.g. {"0" => "foo > 0.3.0"} + index = index + 1 + end + end + Chef::Log.debug("cookbook version constraints are: #{cookbook_version_constraints.inspect}") + cookbook_version_constraints + end + + end
\ No newline at end of file diff --git a/chef-server-webui/app/views/environments/_form.html.erb b/chef-server-webui/app/views/environments/_form.html.erb index bef255decf..0fd55e3f6c 100644 --- a/chef-server-webui/app/views/environments/_form.html.erb +++ b/chef-server-webui/app/views/environments/_form.html.erb @@ -1,34 +1,71 @@ -<div id="#block-tables" class="block"> - <div class="content"> - <% if new_object %> - <h3>New Environment</h3> - <% else %> - <h3><%= @environment.name -%></h3> - <% end %> - <%= form(:action => url(*route), :method => method) do -%> - <div id="environment_name_control_container" class="block"> - <span class="label"><label for="name">name</label></span> - <span><%= text_field(:name => "name", :value => @environment.name, :disabled => !new_object)%></span> - </div> - <div id="environment_description_control_container" class="block"> - <span class="label"><label for="description">description:</label></span> - <span><%= text_area(@environment.description, :name => "description")%></span> - </div> - <div id="attributes_control_container"> - <span class="label"><label for="attributes">attributes:</label></span> - <span><%= text_area(@environment.attributes.to_json, :name => "attributes")%></span> +<div class="content environmentFormContainer"> + <%= form(:action => form_url, :method => (method || :post), :id => form_id, :class => 'form') do -%> + <% unless form_for == 'edit' %> + <div class="group form"> + <label class="label" for="environment_name">Name</label> + <% if !@environment.invalid_fields[:name].nil? && @environment.invalid_fields[:name].size != 0 %> + <span class="inline_error_message">The environment name contains illegal characters. The following characters are allowed: a-z, A-Z, 0-9, _, and -.</span> + <% end %> + + <%= text_field :id => "environment_name", :name => "name", :class => "text_field", :value => params.has_key?(:name) ? h(params[:name]) : @environment.name %> + <span class="description">The name of the Environment</span> </div> - <% cookbook_version_index = 0 -%> - <% @environment.cookbook_versions.each do |cookbook_name, constraint| -%> - <%= partial "environments/version_selector",:cookbook_version_index => cookbook_version_index, - :cookbook_name => cookbook_name, - :constraint => constraint %> - <% cookbook_version_index += 1 -%> + <% end %> + + <div class="group form"> + <label class="label" for="environment_description">Description</label> + <%= text_area(params.has_key?(:description) ? h(params[:description]) : @environment.description, :name => "description", :class => "text_area", :id => "environment_description") %> + <span class="description">A description of this Environment</span> + </div> + + <div class="group form cbVerPicker" id=<%=form_for%>> + <label class="label" for="cookbook_version_constraints">Cookbook Version Constraints</label> + <% if !@environment.invalid_fields[:cookbook_version].nil? && !@environment.invalid_fields[:cookbook_version].empty? %> + <span class="inline_error_message" id='error'>There was a problem in one of your constraints. Please enter version constraints in acceptable formats (e.g. 0.0.0). </span> <% end %> - <%= partial "environments/version_selector",:cookbook_version_index => cookbook_version_index, - :cookbook_name => nil, - :constraint => nil %> - <%= submit("#{new_object ? 'create' : 'update'}")%> - <% end =%> - </div> + <table class="table" id="cbVerPickerTable"> + <tbody> + <tr> + <th class = "first" id="CookbookVersionConstraints">Name</th> + <th>Operator</th> + <th>Version</th> + <th class = "last">Foobar</th> + </tr> + </tbody> + </table> + <a href='javascript:void(0)' onclick='addTableRow(null, ">=", "0.0.0")'>Add Cookbook Version Constraint</a> + </div> + + <div class="group form attrEditor"> + <label class="label">Environment Attributes</label> + <%= partial 'layout/jsonedit', :json => @environment.attributes.to_json%> + <span class="description"> + A JSON hash for attributes of this environment. These attributes will be applied according to the attribute precedence rules. + </span> + </div> + + <div class="group"> + <div class="actions-bar"> + <%= submit(submit_name, :id => submit_id, :class => 'button') -%> + </div> + </div> + <% end =%> </div> + + +<script type='text/javascript'> +var cookbook_names = <%= @cookbooks.to_json %>; + +isEdit = document.getElementById('edit'); +if (isEdit != null){ + <%unless @environment.cookbook_versions.nil? || @environment.cookbook_versions.empty?%> + var existing_env_cookbook_version_constraints = <%= @environment.cookbook_versions.to_json %>; + for (var cookbook in existing_env_cookbook_version_constraints){ + var operator = existing_env_cookbook_version_constraints[cookbook].split(" ")[0]; + var version = existing_env_cookbook_version_constraints[cookbook].split(" ")[1]; + addTableRow(cookbook, operator, version); + } + <% end %> +} +</script> +<script type='text/javascript' charset="utf-8" src="/javascripts/cookbook_constraint_ctrl.js"></script> diff --git a/chef-server-webui/app/views/environments/edit.html.erb b/chef-server-webui/app/views/environments/edit.html.erb index 44bc9de233..c1485cc54a 100644 --- a/chef-server-webui/app/views/environments/edit.html.erb +++ b/chef-server-webui/app/views/environments/edit.html.erb @@ -1,6 +1,17 @@ -<%= partial "environments/navigation", :environment => @environment, :active => 'edit'%> - -<%= partial 'environments/form', :environment => @environment, - :route => [:environment, @environment.name], - :method => :put, - :new_object => false %>
\ No newline at end of file +<div id="block-tables" class="block"> + <div class="content"> + <h2 class="title">Environment <%= params[:id] %></h2> + <div class="inner"> + <%= partial "environments/navigation", :environment => @environment, :active => 'edit'%> + </div> + <%= partial 'environments/form', :environment => @environment, + :route => :environments, + :method => :put, + :form_url => url(:environment, @environment.name), + :form_id => 'edit_environment', + :form_for => 'edit', + :submit_name => "Update Environment", + :submit_id => "edit_environment_button", + :new_object => true %> + </div> +</div>
\ No newline at end of file diff --git a/chef-server-webui/app/views/environments/new.html.erb b/chef-server-webui/app/views/environments/new.html.erb index 1eef0fa1df..b24f2c65cf 100644 --- a/chef-server-webui/app/views/environments/new.html.erb +++ b/chef-server-webui/app/views/environments/new.html.erb @@ -1,6 +1,17 @@ -<%= partial "environments/navigation", :environment => @environment, :active => 'create'%> - -<%= partial 'environments/form', :environment => @environment, - :route => :environments, - :method => :post, - :new_object => true %> +<div id="block-tables" class="block"> + <div class="content"> + <h2 class="title">Environment <%= params[:id] %></h2> + <div class="inner"> + <%= partial "environments/navigation", :environment => @environment, :active => 'create'%> + </div> + <%= partial 'environments/form', :environment => @environment, + :route => :environments, + :method => :post, + :form_url => url(:environments), + :form_id => 'create_environment', + :form_for => 'create', + :submit_name => "Create Environment", + :submit_id => "create_environment_button", + :new_object => true %> + </div> +</div>
\ No newline at end of file diff --git a/chef-server-webui/app/views/environments/show.html.haml b/chef-server-webui/app/views/environments/show.html.haml index dc5898258b..bf4f3cffe7 100644 --- a/chef-server-webui/app/views/environments/show.html.haml +++ b/chef-server-webui/app/views/environments/show.html.haml @@ -12,14 +12,14 @@ %h3 Description = @environment.description .left - %h3 Cookbook Versions + %h3 Cookbook Version Constraints %table.table %tr %th.first Name - %th.last Version(s) + %th.last Version Constraints - if @environment.cookbook_versions.empty? %tr - %td{:colspan => 2} This environment does not include any cookbook version restrictions + %td{:colspan => 2} This environment does not include any cookbook version constraints. - else - @environment.cookbook_versions.each do |name, versions| %tr diff --git a/chef-server-webui/app/views/layout/application.html.haml b/chef-server-webui/app/views/layout/application.html.haml index 703b676316..de1d492942 100644 --- a/chef-server-webui/app/views/layout/application.html.haml +++ b/chef-server-webui/app/views/layout/application.html.haml @@ -3,13 +3,14 @@ %head %meta{ "http-equiv" => "content-type", :content => "text/html; charset=utf-8" } %title Chef Server - = css_include_tag "base", "themes/djime-cerulean/style", "chef", "/facebox/facebox.css", "jquery-ui-1.7.1.custom", "jquery.treeTable", "jsonedit_main.css" + = css_include_tag "base", "themes/djime-cerulean/style", "chef", "/facebox/facebox.css", "jquery-ui-1.7.1.custom", "jquery.treeTable", "jsonedit_main.css", "jquery.suggest.css" = js_include_tag "jquery-1.4.4.min", "jquery.jeditable.mini", "jquery.livequery", "jquery.localscroll", "jquery.scrollTo", "jquery.tools.min" = js_include_tag "/facebox/facebox.js" = js_include_tag "jquery-ui-1.7.1.custom.min" = js_include_tag "jquery.treeTable.min" = js_include_tag "chef" = js_include_tag "drop_down_menu" + = js_include_tag "jquery.suggest" = catch_content :header_content diff --git a/chef-server-webui/config/router.rb b/chef-server-webui/config/router.rb index e02e771dde..0f2340115c 100644 --- a/chef-server-webui/config/router.rb +++ b/chef-server-webui/config/router.rb @@ -56,9 +56,10 @@ Merb::Router.prepare do match("/cookbooks/:cookbook_id/attributes", :cookbook_id => /[\w\.]+/).to(:controller => "cookbook_attributes", :action => "index") match("/cookbooks/:cookbook_id/files", :cookbook_id => /[\w\.]+/).to(:controller => "cookbook_files", :action => "index") + match("/cookbooks/:cookbook_id", :cookbook_id => /[\w\.]+/, :method => 'get').to(:controller => "cookbooks", :action => "cb_versions") match("/cookbooks/:cookbook_id/:cb_version", :cb_version => /[\w\.]+/, :method => 'get').to(:controller => "cookbooks", :action => "show").name(:show_specific_version_cookbook) resources :cookbooks - + resources :clients match("/databags/:databag_id/databag_items", :method => 'get').to(:controller => "databags", :action => "show", :id=>":databag_id") diff --git a/chef-server-webui/public/javascripts/chef.js b/chef-server-webui/public/javascripts/chef.js index 2959b24ab7..07070dab7c 100644 --- a/chef-server-webui/public/javascripts/chef.js +++ b/chef-server-webui/public/javascripts/chef.js @@ -7,9 +7,9 @@ // 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. @@ -42,7 +42,13 @@ $(document).ready(function(){ buildHiddenFormFromDragDrop($(this), $('ul.runListItemList')); buildHiddenFormFromJSONEditor($(this)); }); - + + $('form#edit_environment, form#create_environment').submit(function(event) { + var form = $(this); + form.append('<input type="hidden" id="attributes" name="attributes"/>'); + $('input#attributes').attr('value', BCJTEP.save('json')) + }); + $('form#edit_node, form#create_node').submit(function(event) { var form = $(this); var to_node = $('ul#for_node').sortable('toArray'); @@ -66,7 +72,7 @@ $(document).ready(function(){ form.append($('input#json_data').attr('value', BCJTEP.save())); }); - $('form#edit_databag, form#create_databag').submit(function(event) { + $('form#edit_databag, form#create_databag').submit(function(event) { var form = $(this); if (form.attr('id') == 'edit_databag') { form.append('<input type="hidden" name="_method" value="put">'); @@ -91,30 +97,30 @@ $(document).ready(function(){ var form = $(this); if (form.attr('id') == 'edit_user') { form.append('<input type="hidden" name="_method" value="put">'); - form.append($('input#user_new_password')).css('display', 'none'); - form.append($('input#user_admin')).css('display', 'none'); - form.append($('input#user_confirm_new_password')).css('display', 'none'); - form.append($('input#openid')).css('display', 'none'); - } - if (form.attr('id') == 'login') { - form.append($('input#user_name')).css('display', 'none'); - form.append($('input#password')).css('display', 'none'); - } + form.append($('input#user_new_password')).css('display', 'none'); + form.append($('input#user_admin')).css('display', 'none'); + form.append($('input#user_confirm_new_password')).css('display', 'none'); + form.append($('input#openid')).css('display', 'none'); + } + if (form.attr('id') == 'login') { + form.append($('input#user_name')).css('display', 'none'); + form.append($('input#password')).css('display', 'none'); + } }); // livequery hidden form for link_to ajax magic $('a[method]').livequery(function(){ var message = $(this).attr('confirm'); var method = $(this).attr('method'); - + if (!method && !message) return; - + $(this).click(function(event){ if (message && !confirm(message)) { event.preventDefault(); return; } - + if (method === 'post' || method === 'put' || method === 'delete') { event.preventDefault(); var form = $("<form/>").attr('method', 'post').attr('action', this.href).attr('style', 'display: none'); @@ -125,16 +131,16 @@ $(document).ready(function(){ } }); }); - + // accordion for the cookbooks show view - $('.accordion .head').click(function() { - $(this).next().toggle('slow'); - return false; - }).next().hide(); - - // global facebox callback - $('a[rel*=facebox]').facebox(); - + $('.accordion .head').click(function() { + $(this).next().toggle('slow'); + return false; + }).next().hide(); + + // global facebox callback + $('a[rel*=facebox]').facebox(); + var enableDragDropBehavior = function() { $('.connectedSortable').sortable({ @@ -152,23 +158,23 @@ $(document).ready(function(){ // The table tree! $('table.tree').treeTable({ expandable: true }); $('span.expander').click(function() { $('tr#' + $(this).attr('toggle')).toggleBranch(); }); - + // Tooltips - $("div.tooltip").tooltip({ + $("div.tooltip").tooltip({ position: ['center', 'right'], offset: [-5, 10], effect: 'toggle', - opacity: 0.7 + opacity: 0.7 }); - + // Show the sidebars if they have text in them! var sidebar_block_notice_children = $("#sidebar_block_notice").children().length; var sidebar_block_children = $("#sidebar_block").children().length; - + if (sidebar_block_notice_children > 0) { $("#sidebar_block_notice").fadeIn(); } - + if (sidebar_block_children > 0) { $("#sidebar_block").fadeIn(); } @@ -205,7 +211,7 @@ $(document).ready(function(){ var role = allRoles[i]; $('ul#availableRoles').append('<li id="role[' + role + ']" class="ui-state-highlight runListItem">' + role + '</li>'); } - + }; var clearRunListFor = function(environment) { diff --git a/chef-server-webui/public/javascripts/cookbook_constraint_ctrl.js b/chef-server-webui/public/javascripts/cookbook_constraint_ctrl.js new file mode 100644 index 0000000000..3ef7b1647d --- /dev/null +++ b/chef-server-webui/public/javascripts/cookbook_constraint_ctrl.js @@ -0,0 +1,74 @@ +function jQuerySuggest(timestamp){ + var cb_name = retrieveCbName(timestamp); + populateVersionBoxContent(timestamp, cb_name); + document.getElementById("cookbook_version_" + timestamp).value = "0.0.0"; +} + +function populateVersionBoxContent(timestamp, cb_name){ + $.getJSON('/cookbooks/'+cb_name, function(result){ + jQuery('#cookbook_version_'+timestamp).suggest(result[cb_name]); + }); +} + +function clearVersionBox(box, timestamp){ + populateVersionBoxContent(timestamp, retrieveCbName(timestamp)); + //if (box.value == "0.0.0"){box.value = "";} + error_message = document.getElementById('inline_error_message_' + timestamp); + if (error_message != null) + $(error_message).remove(); +} + +function validateVersionBoxValue(box, timestamp){ + if (box.value.match(/\d+\.\d+\.\d+/) == null){ + if (box.value.length != 0 && document.getElementById('inline_error_message_' + timestamp) == null) + $(box).parent().append('<span class="inline_error_message" id="inline_error_message_' + timestamp + '" >Invalid version format. The version should be in the format of 0.0.0.</span>'); + if (box.value.length==0) + box.value = "0.0.0"; + } +} + +function buildCookbookList(cookbook_names, default_cookbook){ + if (default_cookbook != null && $.inArray(default_cookbook, cookbook_names) < 0){ + cookbook_names.push(default_cookbook); + } + var result = '<option value=""></option>'; + for(i=0; i<cookbook_names.length; i++){ + result += '<option value=' + '"' + cookbook_names[i] + '" '; + if (cookbook_names[i] == default_cookbook) + result += 'selected=true>' + cookbook_names[i] + '</option>'; + else + result += '>' + cookbook_names[i] + '</option>'; + } + return result; +} + +function buildOperatorList(default_operator){ + return '<option value=">="' + (default_operator == ">=" ? "selected=true" : "") + '>>=</option>' + + '<option value=">"' + (default_operator == ">" ? "selected=true" : "" )+ '>></option>' + + '<option value="="' + (default_operator == "=" ? "selected=true" : "") +'>=</option>' + + '<option value="<"' + (default_operator == "<" ? "selected=true" : "") + '><</option>' + + '<option value="<="' + (default_operator == "<=" ? "selected=true" : "") + '><=</option>' + + '<option value="~>"' + (default_operator == "~>" ? "selected=true" : "") + '>~></option>'; +} + +function retrieveCbName(timestamp){ + var select_box = document.getElementById("cookbook_name_" + timestamp); + return select_box.options[select_box.selectedIndex].value; +} + +function addTableRow(default_cookbook, default_operator, default_version){ + var timestamp = new Date().getTime(); + var row = '<tr id=' + '"' + timestamp + '"><td>' + '<select size="1" name="cookbook_name_' + timestamp + '" ' + 'id="cookbook_name_' + timestamp + '" class="cookbook_version_constraints_cb_name" onchange="jQuerySuggest(' + timestamp + ')"'+'>' + + buildCookbookList(cookbook_names, default_cookbook) + '</select>' + + '</td>' + + '<td><select name="operator_' + timestamp + '">' + buildOperatorList(default_operator) + '</select></td>' + + '<td><input class="text" name="cookbook_version_' + timestamp +'" ' + 'id="cookbook_version_' + timestamp + '" ' + 'type="text" onfocus="clearVersionBox(this,' + timestamp + ')" onblur="validateVersionBoxValue(this,' + timestamp + ')" value="' + default_version + '"></td>' + + '<td><a href="javascript::void(0)" onclick="removeTableRow($(this).parent().parent())">Remove</a></td>' + + '</tr>'; + $("#cbVerPickerTable tbody").append(row); + validateVersionBoxValue(document.getElementById("cookbook_version_" + timestamp)); +} + +function removeTableRow(row){ + row.remove(); +}
\ No newline at end of file diff --git a/chef-server-webui/public/javascripts/jquery.suggest.js b/chef-server-webui/public/javascripts/jquery.suggest.js new file mode 100644 index 0000000000..aaee89cebd --- /dev/null +++ b/chef-server-webui/public/javascripts/jquery.suggest.js @@ -0,0 +1,250 @@ + +/* + * jquery.suggest 1.1 - 2007-08-06 + * + * Uses code and techniques from following libraries: + * 1. http://www.dyve.net/jquery/?autocomplete + * 2. http://dev.jquery.com/browser/trunk/plugins/interface/iautocompleter.js + * + * All the new stuff written by Peter Vulgaris (www.vulgarisoip.com) + * Feel free to do whatever you want with this file + * + */ + +(function($) { + + $.suggest = function(input, options) { + var $input = $(input).attr("autocomplete", "off"); + var $results = $(document.createElement("ul")); + + var timeout = false; // hold timeout ID for suggestion results to appear + var prevLength = 0; // last recorded length of $input.val() + var cache = []; // cache MRU list + var cacheSize = 0; // size of cache in chars (bytes?) + + $results.addClass(options.resultsClass).appendTo('body'); + + resetPosition(); + $(window) + .load(resetPosition) // just in case user is changing size of page while loading + .resize(resetPosition); + + //Show suggestion drop down list when on focus. + $input.focus(function() { + suggest(); + }); + + $input.blur(function() { + setTimeout(function() { $results.hide() }, 200); + }); + + // help IE users if possible + try { + $results.bgiframe(); + } catch(e) { } + + // I really hate browser detection, but I don't see any other way + if ($.browser.mozilla) + $input.keypress(processKey); // onkeypress repeats arrow keys in Mozilla/Opera + else + $input.keydown(processKey); // onkeydown repeats arrow keys in IE/Safari + + function resetPosition() { + // requires jquery.dimension plugin + var offset = $input.offset(); + $results.css({ + top: (offset.top + input.offsetHeight) + 'px', + left: offset.left + 'px' + }); + } + + function processKey(e) { + // handling up/down/escape requires results to be visible + // handling enter/tab requires that AND a result to be selected + if ((/27$|38$|40$/.test(e.keyCode) && $results.is(':visible')) || + (/^13$|^9$/.test(e.keyCode) && getCurrentResult())) { + + if (e.preventDefault) + e.preventDefault(); + if (e.stopPropagation) + e.stopPropagation(); + + e.cancelBubble = true; + e.returnValue = false; + + switch(e.keyCode) { + case 38: // up + prevResult(); + break; + case 40: // down + nextResult(); + break; + case 9: // tab + case 13: // return + selectCurrentResult(); + break; + case 27: // escape + $results.hide(); + break; + } + } else { + if (timeout) + clearTimeout(timeout); + timeout = setTimeout(suggest, options.delay); + prevLength = $input.val().length; + } + } + + function suggest() { + var q = $.trim($input.val()).replace(/\s/g, ''); + if (q.length >= options.minchars) { + var filter = new RegExp(q); + var filtered_list = []; + if (q != '0.0.0'){ + for (item in options.list) { + var item_string = options.list[item]; + if (filter.test(item_string.replace(/\s/g,''))){ + filtered_list.push(item_string); + } + } + } else { + filtered_list = options.list; + } + displayItems(filtered_list); + } else { + $results.hide(); + } + } + + function checkCache(q) { + for (var i = 0; i < cache.length; i++) + if (cache[i]['q'] == q) { + cache.unshift(cache.splice(i, 1)[0]); + return cache[0]; + } + return false; + } + + function addToCache(q, items, size) { + while (cache.length && (cacheSize + size > options.maxCacheSize)) { + var cached = cache.pop(); + cacheSize -= cached['size']; + } + cache.push({ + q: q, + size: size, + items: items + }); + cacheSize += size; + } + + function displayItems(items) { + if (!items) + return; + + if (!items.length) { + $results.hide(); + return; + } + + var html = ''; + for (var i = 0; i < items.length; i++) + html += '<li>' + items[i] + '</li>'; + $results.html(html).show(); + + $results + .children('li') + .mouseover(function() { + $results.children('li').removeClass(options.selectClass); + $(this).addClass(options.selectClass); + }) + .click(function(e) { + e.preventDefault(); + e.stopPropagation(); + selectCurrentResult(); + }); + } + + function parseTxt(txt, q) { + var items = []; + var tokens = txt.split(options.delimiter); + + // parse returned data for non-empty items + for (var i = 0; i < tokens.length; i++) { + var token = $.trim(tokens[i]); + if (token) { + token = token.replace( + new RegExp(q, 'ig'), + function(q) { return '<span class="' + options.matchClass + '">' + q + '</span>' } + ); + items[items.length] = token; + } + } + return items; + } + + function getCurrentResult() { + if (!$results.is(':visible')) + return false; + var $currentResult = $results.children('li.' + options.selectClass); + if (!$currentResult.length) + $currentResult = false; + return $currentResult; + } + + function selectCurrentResult() { + $currentResult = getCurrentResult(); + if ($currentResult) { + $input.val($currentResult.text()); + $results.hide(); + if (options.onSelect) + options.onSelect.apply($input[0]); + } + } + + function nextResult() { + $currentResult = getCurrentResult(); + if ($currentResult) + $currentResult + .removeClass(options.selectClass) + .next() + .addClass(options.selectClass); + else + $results.children('li:first-child').addClass(options.selectClass); + } + + function prevResult() { + $currentResult = getCurrentResult(); + if ($currentResult) + $currentResult + .removeClass(options.selectClass) + .prev() + .addClass(options.selectClass); + else + $results.children('li:last-child').addClass(options.selectClass); + } + } + + $.fn.suggest = function(list, options) { + //if (!source) + // return; + options = options || {}; + //options.source = source; + options.delay = options.delay || 100; + options.resultsClass = options.resultsClass || 'ac_results'; + options.selectClass = options.selectClass || 'ac_over'; + options.matchClass = options.matchClass || 'ac_match'; + options.minchars = options.minchars || 0; + options.delimiter = options.delimiter || '\n'; + options.onSelect = options.onSelect || false; + options.maxCacheSize = options.maxCacheSize || 65536; + options.list = list; + + this.each(function() { + new $.suggest(this, options); + }); + + return this; + }; + +})(jQuery);
\ No newline at end of file diff --git a/chef-server-webui/public/stylesheets/chef.css b/chef-server-webui/public/stylesheets/chef.css index 2f145d146a..094b1565c7 100644 --- a/chef-server-webui/public/stylesheets/chef.css +++ b/chef-server-webui/public/stylesheets/chef.css @@ -52,7 +52,7 @@ dl dt { font-weight: bold; } padding-top: 2px; } -div.roleFormContainer, div.nodeFormContainer{ +div.roleFormContainer, div.nodeFormContainer, div.environmentFormContainer{ padding: 0 15px 15px; min-width:1050px; } @@ -70,6 +70,15 @@ div#current_run_list_display_container{ margin-left:20px; } +span#cookbook_constraints_picker{ + float:left; + margin-left:10px; +} + +span#cookbook_constraints_picker select{ + size: 15; + margin-right:50px; +} span#environment_run_list_selection_control{ float:right; @@ -108,7 +117,7 @@ div.runListItemListContainer{ margin-top:5px; margin-right: 10px; box-shadow:2px 2px 2px #dfdfdf; - /* Safari 5 has a weird problem where these divs increase in width by 15px after updating contents with ajax. + /* Safari 5 has a weird problem where these divs increase in width by 15px after updating contents with ajax. Problem doesn't occur in webkit nightlies. setting max-width works around.*/ max-width:360px; } @@ -300,4 +309,8 @@ h1 label { font-size: 12px; color: #F7F7F8; padding-left: 10px; +} + +.cookbook_version_constraints_cb_name{ + width:80%; }
\ No newline at end of file diff --git a/chef-server-webui/public/stylesheets/jquery.suggest.css b/chef-server-webui/public/stylesheets/jquery.suggest.css new file mode 100644 index 0000000000..8ddee6c0d4 --- /dev/null +++ b/chef-server-webui/public/stylesheets/jquery.suggest.css @@ -0,0 +1,28 @@ +.ac_results { + width: 152px; + border: 1px solid gray; + background-color: white; + padding: 0; + margin: 0; + list-style: none; + position: absolute; + z-index: 10000; + display: none; +} + +.ac_results li { + padding: 2px 5px; + white-space: nowrap; + color: #101010; + text-align: left; +} + +.ac_over { + cursor: pointer; + background-color: #F0F0B8; +} + +.ac_match { + text-decoration: underline; + color: black; +}
\ No newline at end of file diff --git a/chef-server-webui/public/stylesheets/themes/djime-cerulean/style.css b/chef-server-webui/public/stylesheets/themes/djime-cerulean/style.css index 5c5538a6f3..6acc13406a 100644 --- a/chef-server-webui/public/stylesheets/themes/djime-cerulean/style.css +++ b/chef-server-webui/public/stylesheets/themes/djime-cerulean/style.css @@ -211,6 +211,12 @@ hr { font-size: .9em; } +.form .inline_error_message { + color: #FF0000; + font-weight: bold; + padding-left:5px; +} + /* @group Flash messages */ .flash .message { @@ -267,7 +273,7 @@ ul.list li .item .avatar { /* rounded borders */ -#main, #main-navigation, #main-navigation li, #main-navigation li a, .secondary-navigation, #main .block, #sidebar .block, #sidebar h3, ul.list li, +#main, #main-navigation, #main-navigation li, #main-navigation li a, .secondary-navigation, #main .block, #sidebar .block, #sidebar h3, ul.list li, #footer .block, .form input.button, #box .block, #box .block h2 { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; @@ -280,6 +286,14 @@ ul.list li .item .avatar { -webkit-border-top-left-radius: 4px; } +.table th.first#CookbookVersionConstraints{ + width:30%; +} + +.table th.last#CookbookVersionConstraints{ + width:50%; +} + .table th.last { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; |