[PATCH Conductor] BZ 785216 - Fix priority comparison for provider accounts
by Greg Blomquist
From: Greg Blomquist <gblomqui(a)redhat.com>
https://bugzilla.redhat.com/show_bug.cgi?id=785216
Launching a deployment should not fail if any of the provider accounts' priority
value is nil.
---
src/app/models/deployment.rb | 12 +++++++++++-
src/spec/models/deployment_spec.rb | 18 ++++++++++++++++++
2 files changed, 29 insertions(+), 1 deletions(-)
diff --git a/src/app/models/deployment.rb b/src/app/models/deployment.rb
index 68b5a57..8f6fac1 100644
--- a/src/app/models/deployment.rb
+++ b/src/app/models/deployment.rb
@@ -236,7 +236,17 @@ class Deployment < ActiveRecord::Base
return status
end
# grab the account with the best priority that can launch this deployment
- account = account_matches.keys.sort{|a,b| a.priority <=> b.priority}.first
+ account = account_matches.keys.sort do |a,b|
+ if a.priority.nil? and b.priority.nil?
+ 0
+ elsif a.priority.nil?
+ 1
+ elsif b.priority.nil?
+ -1
+ else
+ a.priority <=> b.priority
+ end
+ end.first
instances_matches = account_matches[account]
if deployable_xml.requires_config_server?
diff --git a/src/spec/models/deployment_spec.rb b/src/spec/models/deployment_spec.rb
index 38b2b19..b8806e7 100644
--- a/src/spec/models/deployment_spec.rb
+++ b/src/spec/models/deployment_spec.rb
@@ -224,6 +224,24 @@ describe Deployment do
deployment2.instances[1].provider_account.should == @provider_account2
end
+ it "should not fail to launch if a provider account has a nil priority" do
+ @deployment.save!
+ @deployment.instances.should be_empty
+
+ @provider_account1.priority = nil
+ @provider_account1.save!
+
+ Instance.any_instance.stub(:provider_images_for_match).and_return([@provider_image])
+ Taskomatic.stub!(:create_dcloud_instance).and_return(true)
+ Taskomatic.stub!(:handle_dcloud_error).and_return(true)
+ Taskomatic.stub!(:handle_instance_state).and_return(true)
+ @deployment.launch(@user_for_launch)[:errors].should be_empty
+ @deployment.reload
+ @deployment.instances.count.should == 2
+ @deployment.instances[0].provider_account.should == @provider_account2
+ @deployment.instances[1].provider_account.should == @provider_account2
+ end
+
it "should not launch instances if user has no access to hardware profile" do
@deployment.save!
@deployment.instances.should be_empty
--
1.7.6.5
12 years, 1 month
[PATCH conductor] BZ773034: Autoupdate for images#show (rev. 3)
by Imre Farkas
From: Imre Farkas <ifarkas(a)redhat.com>
Rebased it once again.
Note: it depends on my previous patchset "BZ781482: feedback for image build/push failures".
https://bugzilla.redhat.com/show_bug.cgi?id=773034
---
src/app/controllers/images_controller.rb | 53 ++++++++++++++
src/app/models/provider_account.rb | 6 ++
src/app/views/images/show.html.haml | 105 ++++++++++++++++++++++++++++
src/public/javascripts/application.js | 32 +++++++++
src/public/javascripts/backbone/models.js | 8 ++
src/public/javascripts/backbone/routers.js | 19 +++++
src/public/javascripts/backbone/views.js | 45 ++++++------
7 files changed, 245 insertions(+), 23 deletions(-)
diff --git a/src/app/controllers/images_controller.rb b/src/app/controllers/images_controller.rb
index 41c48a5..997ab6a 100644
--- a/src/app/controllers/images_controller.rb
+++ b/src/app/controllers/images_controller.rb
@@ -60,6 +60,59 @@ class ImagesController < ApplicationController
load_builds
load_target_images(@build)
flash[:error] = t("images.flash.error.no_provider_accounts") if @account_groups.size == 0
+
+ respond_to do |format|
+ format.html
+ format.json do
+ active_builds = @account_groups.keys.inject({}) do |result, driver|
+ result[driver] = @builder.find_active_build((a)build.id, driver) if @build
+ result
+ end
+
+ active_pushes = @account_groups.inject({}) do |result, (driver, group)|
+ timg = @target_images_by_target[driver]
+ group[:accounts].each do |account|
+ result[account.id] = @builder.find_active_push(timg.id, account.provider.name, account.credentials_hash['username'])
+ end if timg.present?
+
+ result
+ end
+
+ provider_images = @account_groups.inject({}) do |result, (driver, group)|
+ timg = @target_images_by_target[driver]
+ group[:accounts].each do |account|
+ result[account.id] = timg.find_provider_image_by_provider_and_account(account.provider.name, account.credentials_hash['username']).first
+ end if timg.present?
+
+ result
+ end
+
+ failed_build_counts = @account_groups.keys.inject({}) do |result, driver|
+ result[driver] = @builder.failed_build_count((a)build.id, driver) if @build
+ result
+ end
+
+ failed_push_counts = @account_groups.inject({}) do |result, (driver, group)|
+ timg = @target_images_by_target[driver]
+ group[:accounts].each do |account|
+ result[account.id] = @builder.failed_push_count(timg.id, account.provider.name, account.credentials_hash['username'])
+ end if timg.present?
+
+ result
+ end
+
+ render :json => { :image => @image,
+ :build => @build,
+ :account_groups => @account_groups,
+ :provider_images => provider_images,
+ :target_images_by_target => @target_images_by_target,
+ :active_builds => active_builds,
+ :active_pushes => active_pushes,
+ :failed_build_counts => failed_build_counts,
+ :failed_push_counts => failed_push_counts,
+ :latest_build_id => @latest_build }
+ end
+ end
end
def rebuild_all
diff --git a/src/app/models/provider_account.rb b/src/app/models/provider_account.rb
index e772f52..153a910 100644
--- a/src/app/models/provider_account.rb
+++ b/src/app/models/provider_account.rb
@@ -150,6 +150,12 @@ class ProviderAccount < ActiveRecord::Base
label.blank? ? credentials_hash['username'] : label
end
+ def as_json(options={})
+ super(options).merge({
+ :provider_name => provider.name
+ })
+ end
+
def populate_profiles_and_validate
begin
populate_hardware_profiles
diff --git a/src/app/views/images/show.html.haml b/src/app/views/images/show.html.haml
index 0667aa9..3f47973 100644
--- a/src/app/views/images/show.html.haml
+++ b/src/app/views/images/show.html.haml
@@ -91,3 +91,108 @@
$("#seletect_build_button").click();
});
});
+
+%script#imageBuildsTemplate{ :type => 'text/x-jquery-tmpl' }
+ :plain
+ {{each(driver, groups) account_groups}}
+ <li>
+ <dl>
+ <dt>
+ <h3>${groups['type']['name']}</h3>
+ {{if build && active_builds[driver]}}
+ <label>${active_builds[driver]['status']}</label>
+ {{else target_images_by_target[driver]}}
+ #{path = image_target_image_path('replace_image_id', 'replace_target_image_id')
+ path = path.sub('replace_image_id', '${image[\'uuid\']}')
+ path = path.sub('replace_target_image_id', '${target_images_by_target[driver][\'uuid\']}')
+ button_to(t('images.show.delete'), path, :method => :delete)}
+ {{else build && build['uuid'] == latest_build_id}}
+ #{path = image_target_images_path('replace_image_id', :target => 'replace_driver', :build_id => 'replace_build_id')
+ path = path.sub('replace_image_id', '${image[\'uuid\']}')
+ path = path.sub('replace_driver', '${driver}')
+ path = path.sub('replace_build_id', '${build[\'uuid\']}')
+ button_to(t('images.show.build'), path, :method => :post)}
+ {{if failed_build_counts[driver] > 1}}
+ <div class="light">
+ #{t('images.show.failed_build_attemts', :count => '${failed_build_counts[driver]}' )}
+ </div>
+ {{else failed_build_counts[driver] == 1}}
+ <div class="light">
+ #{t('images.show.failed_build_attemts', :count => 1 )}
+ </div>
+ {{/if}}
+ {{else}}
+
+ {{/if}}
+ </dt>
+ <dd>
+ <table class="light_table">
+ <thead>
+ <tr>
+ <th>
+ <strong>#{t'images.show.account'}</strong>
+ </th>
+ <th>
+ #{t'images.show.provider'}
+ </th>
+ <th>
+ #{t'images.show.image_uuid'}
+ </th>
+ <th>
+ #{t'images.show.image_uri'}
+ </th>
+ <th class="image_controls"></th>
+ </tr></thead>
+
+ <tbody>
+ {{each(index, account) groups['accounts']}}
+ <tr>
+ <td>
+ <strong>${account['label']}</strong>
+ </td>
+ <td class="light">
+ ${account['provider_name']}
+ </td>
+ <td class="light">
+ {{if provider_images[account['id']]}}
+ ${provider_images[account['id']]['target_identifier']}
+ {{/if}}
+ </td>
+ <td class="light">
+ {{if provider_images[account['id']]}}
+ ${provider_images[account['id']]['uuid']}
+ {{/if}}
+ </td>
+ <td class="image_controls light">
+ {{if target_images_by_target[driver] && active_pushes[account['id']]}}
+ <label>${active_pushes[account['id']]['status']}</label>
+ {{else target_images_by_target[driver] && !provider_images[account['id']] && build && build['uuid'] == latest_build_id}}
+ #{path = image_provider_images_path('replace_image_id', :build_id => 'replace_build_id', :target_image_id => 'replace_target_image_id', :account_id => 'replace_account_id')
+ path = path.sub('replace_image_id', '${image[\'uuid\']}')
+ path = path.sub('replace_build_id', '${build[\'uuid\']}')
+ path = path.sub('replace_target_image_id', '${target_images_by_target[driver][\'uuid\']}')
+ path = path.sub('replace_account_id', '${account[\'id\']}')
+ button_to(t('images.show.upload'), path, :method => :post, :class => 'upload_image')}
+ {{if failed_push_counts[account['id']] > 1}}
+ #{t('images.show.failed_push_attemts', :count => '${failed_push_counts[account[\'id\']]}' )}
+ {{else failed_push_counts[account['id']] == 1}}
+ #{t('images.show.failed_push_attemts', :count => 1 )}
+ {{/if}}
+ {{else provider_images[account['id']]}}
+ #{path = image_provider_image_path('replace_image_id', 'replace_provider_image_id')
+ path = path.sub('replace_image_id', '${image[\'uuid\']}')
+ path = path.sub('replace_provider_image_id', '${provider_images[account[\'id\']][\'uuid\']}')
+ button_to("", path, :method => :delete, :class => 'delete_image')}
+ {{else}}
+
+ {{/if}}
+ </td>
+ </tr>
+ {{/each}}
+ </tbody>
+ </table>
+ </dd>
+ </dl>
+ <div class="clear"></div>
+ </li>
+ {{/each}}
diff --git a/src/public/javascripts/application.js b/src/public/javascripts/application.js
index e9c0ca3..a3dd4a7 100644
--- a/src/public/javascripts/application.js
+++ b/src/public/javascripts/application.js
@@ -164,6 +164,34 @@ $.extend(Conductor, {
});
},
+ urlParams: function() {
+ var paramsData = window.location.search.slice(1).split('&');
+ var params = {};
+ $.each(paramsData, function(index, value){
+ var eqSign = value.search('=');
+ if(eqSign != -1) {
+ params[value.substring(0, eqSign)] = value.substring(eqSign+1);
+ }
+ });
+
+ return params;
+ },
+
+ extractQueryParams: function(paramsToInclude) {
+ var result = {};
+ var urlParams = Conductor.urlParams();
+
+ $.each(paramsToInclude, function(paramIndex, paramValue) {
+ for (var urlParamName in urlParams) {
+ if (urlParamName == paramValue) {
+ result[urlParamName] = urlParams[urlParamName];
+ break;
+ }
+ };
+ });
+
+ return result;
+ },
prefixedPath: function(path) {
var prefix = this.PATH_PREFIX;
@@ -205,6 +233,10 @@ $.extend(Conductor, {
return parseInt(urlFragment.split('?')[0]);
},
+ uuidFromURLFragment: function(urlFragment) {
+ return urlFragment.split('?')[0];
+ },
+
saveCheckboxes: function(checkboxSelector, $scope) {
if(!$scope) $scope = $;
diff --git a/src/public/javascripts/backbone/models.js b/src/public/javascripts/backbone/models.js
index a89cb6b..15ebf7c 100644
--- a/src/public/javascripts/backbone/models.js
+++ b/src/public/javascripts/backbone/models.js
@@ -45,3 +45,11 @@ Conductor.Models.Deployments = Backbone.Collection.extend({
return Conductor.parameterizedPath(path, this.queryParams);
}
});
+
+Conductor.Models.Image = Backbone.Model.extend({
+ queryParams: {},
+ url: function() {
+ var path = Conductor.prefixedPath('/images/' + this.id);
+ return Conductor.parameterizedPath(path, this.queryParams);
+ }
+});
diff --git a/src/public/javascripts/backbone/routers.js b/src/public/javascripts/backbone/routers.js
index ed896d6..d2f0123 100644
--- a/src/public/javascripts/backbone/routers.js
+++ b/src/public/javascripts/backbone/routers.js
@@ -85,3 +85,22 @@ Conductor.Routers.Deployments = Backbone.Router.extend({
}, Conductor.AJAX_REFRESH_INTERVAL);
}
});
+
+Conductor.Routers.Images = Backbone.Router.extend({
+ routes: {
+ 'images/:id': 'show'
+ },
+
+ show: function(id) {
+ id = Conductor.uuidFromURLFragment(id);
+
+ setInterval(function() {
+ var image = new Conductor.Models.Image({ id: id });
+
+ var view = new Conductor.Views.ImagesShow({ model: image });
+ view.model.queryParams = view.queryParams();
+
+ image.fetch({ success: function() { view.render(); } })
+ }, Conductor.AJAX_REFRESH_INTERVAL);
+ }
+});
diff --git a/src/public/javascripts/backbone/views.js b/src/public/javascripts/backbone/views.js
index 97f678d..ec558e9 100644
--- a/src/public/javascripts/backbone/views.js
+++ b/src/public/javascripts/backbone/views.js
@@ -38,32 +38,10 @@ Conductor.Views.PoolsIndex = Backbone.View.extend({
}
},
- urlParams: function() {
- var paramsData = window.location.search.slice(1).split('&');
- var params = {};
- $.each(paramsData, function(index, value){
- var eqSign = value.search('=');
- if(eqSign != -1) {
- params[value.substring(0, eqSign)] = value.substring(eqSign+1);
- }
- });
-
- return params;
- },
-
queryParams: function() {
- var result = {};
var paramsToInclude = [this.currentTab() + '_preset_filter', this.currentTab() + '_search'];
- var urlParams = this.urlParams();
+ var result = Conductor.extractQueryParams(paramsToInclude);
- $.each(paramsToInclude, function(paramIndex, paramValue) {
- for (var urlParamName in urlParams) {
- if (urlParamName == paramValue) {
- result[urlParamName] = urlParams[urlParamName];
- break;
- }
- };
- });
// If there is no URL param for the preset filter, we still need to merge in the preset filter
var filter = this.currentTab() + '_preset_filter';
if(result[filter] == undefined) {
@@ -179,3 +157,24 @@ Conductor.Views.DeploymentsShow = Backbone.View.extend({
}
});
+
+
+Conductor.Views.ImagesShow = Backbone.View.extend({
+ el: '#content',
+
+ queryParams: function() {
+ var paramsToInclude = ['build'];
+ var result = Conductor.extractQueryParams(paramsToInclude);
+
+ return result;
+ },
+
+ render: function() {
+ var $builds = this.$('ul.image_builds');
+ if($builds.length === 0) return;
+
+ $builds.empty();
+
+ $('#imageBuildsTemplate').tmpl(this.model.toJSON()).appendTo($builds);
+ }
+});
--
1.7.6.5
12 years, 1 month
[PATCH aeolus-conductor] BZ#785189 Removed sort urls from hwp table headers
by Martyn Taylor
From: Martyn Taylor <mtaylor(a)redhat.com>
---
.../_matching_provider_hardware_profiles.html.haml | 5 ++++-
.../views/hardware_profiles/_properties.html.haml | 6 +++++-
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/app/views/hardware_profiles/_matching_provider_hardware_profiles.html.haml b/src/app/views/hardware_profiles/_matching_provider_hardware_profiles.html.haml
index 09cb9d3..50f8f21 100644
--- a/src/app/views/hardware_profiles/_matching_provider_hardware_profiles.html.haml
+++ b/src/app/views/hardware_profiles/_matching_provider_hardware_profiles.html.haml
@@ -1,7 +1,10 @@
- if @matching_hwps.size > 0
%table.flat
%thead
- = sortable_table_header @provider_hwps_header
+ %tr
+ - @provider_hwps_header.each do |header|
+ %th
+ = header[:name]
- @matching_hwps.each do |hwp|
%tr{:class => cycle('nostripe','stripe')}
%td
diff --git a/src/app/views/hardware_profiles/_properties.html.haml b/src/app/views/hardware_profiles/_properties.html.haml
index d2a9053..8fae3c1 100644
--- a/src/app/views/hardware_profiles/_properties.html.haml
+++ b/src/app/views/hardware_profiles/_properties.html.haml
@@ -1,5 +1,9 @@
%table.flat
- = sortable_table_header @properties_header
+ %thead
+ %tr
+ - @properties_header.each do |header|
+ %th
+ = header[:name]
- @hwp_properties.each do |hwpp|
%tr
%td
--
1.7.6.4
12 years, 1 month
[PATCH aeolus-conductor] BZ#786126 Do not mass assign on credentials hash
by Martyn Taylor
From: Martyn Taylor <mtaylor(a)redhat.com>
---
.../controllers/provider_accounts_controller.rb | 1 +
src/app/models/provider_account.rb | 5 +++++
2 files changed, 6 insertions(+), 0 deletions(-)
diff --git a/src/app/controllers/provider_accounts_controller.rb b/src/app/controllers/provider_accounts_controller.rb
index 9b5517d..b78ee52 100644
--- a/src/app/controllers/provider_accounts_controller.rb
+++ b/src/app/controllers/provider_accounts_controller.rb
@@ -79,6 +79,7 @@ class ProviderAccountsController < ApplicationController
params[:provider_account][:provider_id] = @provider.id
@providers = Provider.all
@provider_account = ProviderAccount.new(params[:provider_account])
+ @provider_account.credentials_hash = params[:provider_account][:credentials_hash]
@provider_account.quota = @quota = Quota.new
limit = params[:quota][:maximum_running_instances] if params[:quota]
@provider_account.quota.set_maximum_running_instances(limit)
diff --git a/src/app/models/provider_account.rb b/src/app/models/provider_account.rb
index e772f52..ce4fb81 100644
--- a/src/app/models/provider_account.rb
+++ b/src/app/models/provider_account.rb
@@ -74,6 +74,11 @@ class ProviderAccount < ActiveRecord::Base
scope :enabled, lambda { where(:provider_id => Provider.enabled) }
+ # We set credentials hash as protected so that it is not set during mass assign on new
+ # This is to avoid the scenario where the credentials are set before provider which
+ # will result in an exception.
+ attr_protected :credentials_hash
+
def validate_presence_of_credentials
provider.provider_type.credential_definitions.each do |cd|
errors.add(:base, "#{cd.label} can't be blank") if credentials_hash[cd.name].blank?
--
1.7.6.4
12 years, 1 month
Initial aeolus-image gem to rubygems.org
by Jason Guiditta
Taking the same approach as my last email, barring vehement objection,
I am going to push version 0.4.0 or aeolus-image to rubygems.org
tomorrow (probably in the morning my time). This is required to exist
in order for my in-progress bundler work to be testable when I send
patches, and frankly we shoudl have had this gem up there already.
I'll probably push aeolus-cli as well, whatever is the current version
in github/master, unless someone has a better idea.
Going forward, we should try to tie these into community releases of
aeolus in general, but for now, let's call it technical debt, and take
a step towards correcting things. Again, if you object, speak up now,
and have a very good reason, otherwise this happens tomorrow.
-j
12 years, 1 month
[PATCH 0/1 conductor] BZ 785196 add check to ensure task exists before saving
by Tzu-Mainn Chen
Here's the original situation:
begin
.
.
.
task.instance.state = dcloud_to_instance_state(dcloud_instance.state)
task.instance.save!
task.state = Task::STATE_FINISHED
rescue Exception => ex
task.state = Task::STATE_FAILED
task.message = ex.message
ensure
task.save!
end
Due to optimistic locking, the above code gives stale object exceptions if the task is successfully deleted. My understanding is that this is because the after_save method in instance_observer.rb will (under certain circumstances), delete itself and its tasks, meaning task.save! will fail.
The patch replaces
task.save!
with
task.save! if Task.exists?(task.id)
If there's a better way to handle this (or if my logic is wrong), please let me know!
Mainn
12 years, 1 month
Re: [PATCH conductor] BZ 782336 ActiveRecord object is displayed in the failed instance column for pools.
by Scott Seago
On 01/31/2012 07:29 AM, Tomas Sedovic wrote:
> On 01/31/2012 01:24 PM, jzigmund(a)redhat.com wrote:
>> From: Jozef Zigmund<jzigmund(a)redhat.com>
>>
>> Now it displays the value of
>> pool.statistics[:instances_failed_visible_count].
>>
>> https://bugzilla.redhat.com/show_bug.cgi?id=782336
>> ---
>> src/app/views/pool_families/_pools.html.haml | 2 +-
>> 1 files changed, 1 insertions(+), 1 deletions(-)
>>
>> diff --git a/src/app/views/pool_families/_pools.html.haml
>> b/src/app/views/pool_families/_pools.html.haml
>> index 50a8c00..3112183 100644
>> --- a/src/app/views/pool_families/_pools.html.haml
>> +++ b/src/app/views/pool_families/_pools.html.haml
>> @@ -4,7 +4,7 @@
>> %td{:class => 'center'}= pool.deployments.count
>> %td{:class => 'center'}= pool.statistics[:total_instances]
>> %td{:class => 'center'}= pool.statistics[:instances_pending]
>> - %td{:class => 'center'}= pool.statistics[:instances_failed]
>> + %td{:class => 'center'}=
>> pool.statistics[:instances_failed_visible_count]
>> %td{:class => 'center'}= pool.statistics[:quota_percent]
>> %td{:class => 'center'}= pool.statistics[:used_quota]
>> %td{:class => 'center'}=
>> pool.statistics[:available_quota].nil? ? raw('∞') :
>> pool.statistics[:available_quota]
>
> Sorry Jozef, I didn't notice Matt's reply and this updated patch when
> I wrote the previous reply.
>
> ACK to this. Is the pool.statistics[:instances_failed] value used
> anywhere in the code?
>
> If not, we should remove it. If it is, it's still currently broken and
> we should fix it (see my previous email).
>
Yeah it's still used for summary views on the pools side (and probably
here too since the patch has changed).
This issue has caused a bit of confusion, but we really ought to show
aggregate stats across all instances _and_ filtered aggregates. i.e.
pool has 60 instances, 10 failed. Visible to the current user are 10
instances, 2 failed. The user-visible totals are important since that's
what the user sees, but the overall instance count and failures are
useful for seeing overall usage as relates to quota, and for a notion of
overall failure rates, etc. Since we're only summing up, we're not
revealing any private info to non-privileged users.
> Thomas
12 years, 1 month
[PATCH aeolus-conductor] BZ#785189 Removed sort urls from hwp table headers
by Martyn Taylor
From: Martyn Taylor <mtaylor(a)redhat.com>
---
.../_matching_provider_hardware_profiles.html.haml | 5 ++++-
.../views/hardware_profiles/_properties.html.haml | 6 +++++-
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/app/views/hardware_profiles/_matching_provider_hardware_profiles.html.haml b/src/app/views/hardware_profiles/_matching_provider_hardware_profiles.html.haml
index 09cb9d3..50f8f21 100644
--- a/src/app/views/hardware_profiles/_matching_provider_hardware_profiles.html.haml
+++ b/src/app/views/hardware_profiles/_matching_provider_hardware_profiles.html.haml
@@ -1,7 +1,10 @@
- if @matching_hwps.size > 0
%table.flat
%thead
- = sortable_table_header @provider_hwps_header
+ %tr
+ - @provider_hwps_header.each do |header|
+ %th
+ = header[:name]
- @matching_hwps.each do |hwp|
%tr{:class => cycle('nostripe','stripe')}
%td
diff --git a/src/app/views/hardware_profiles/_properties.html.haml b/src/app/views/hardware_profiles/_properties.html.haml
index d2a9053..8fae3c1 100644
--- a/src/app/views/hardware_profiles/_properties.html.haml
+++ b/src/app/views/hardware_profiles/_properties.html.haml
@@ -1,5 +1,9 @@
%table.flat
- = sortable_table_header @properties_header
+ %thead
+ %tr
+ - @properties_header.each do |header|
+ %th
+ = header[:name]
- @hwp_properties.each do |hwpp|
%tr
%td
--
1.7.6.4
12 years, 1 month