foreman_resource_quota 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -5
- data/app/controllers/foreman_resource_quota/api/v2/resource_quotas_controller.rb +1 -1
- data/app/controllers/foreman_resource_quota/concerns/api/v2/hosts_controller_extensions.rb +21 -0
- data/app/helpers/foreman_resource_quota/hosts_helper.rb +18 -7
- data/app/lib/foreman_resource_quota/exceptions.rb +1 -0
- data/app/models/concerns/foreman_resource_quota/host_managed_extensions.rb +9 -10
- data/app/models/concerns/foreman_resource_quota/user_extensions.rb +27 -0
- data/app/models/concerns/foreman_resource_quota/usergroup_extensions.rb +18 -0
- data/app/models/foreman_resource_quota/resource_quota.rb +23 -0
- data/app/views/foreman_resource_quota/resource_quotas/index.html.erb +9 -1
- data/app/views/hosts/_form_quota_fields.html.erb +14 -2
- data/app/views/users/_form_quota_tab.html.erb +2 -2
- data/db/migrate/20240611141939_drop_missing_hosts.rb +9 -2
- data/db/migrate/20250410082728_add_unassigned_flag_to_resource_quota.rb +7 -0
- data/db/seeds.d/030-unassigned_quota.rb +36 -0
- data/lib/foreman_resource_quota/register.rb +8 -0
- data/lib/foreman_resource_quota/version.rb +1 -1
- data/lib/tasks/foreman_resource_quota_tasks.rake +4 -5
- data/package.json +4 -5
- data/webpack/components/CreateResourceQuotaModal.js +1 -0
- data/webpack/components/ResourceQuotaEmptyState/__test__/__snapshots__/ResourceQuotaEmptyState.test.js.snap +3 -0
- data/webpack/components/ResourceQuotaEmptyState/index.js +1 -0
- data/webpack/components/ResourceQuotaForm/ResourceQuotaFormConstants.js +1 -0
- data/webpack/components/ResourceQuotaForm/components/Properties/StaticDetail.js +1 -0
- data/webpack/components/ResourceQuotaForm/components/Properties/TextInputField.js +4 -1
- data/webpack/components/ResourceQuotaForm/components/Properties/index.js +48 -21
- data/webpack/components/ResourceQuotaForm/components/Resource/UnitInputField.js +5 -1
- data/webpack/components/ResourceQuotaForm/components/Resource/UtilizationProgress.js +13 -12
- data/webpack/components/ResourceQuotaForm/components/Resource/__test__/__snapshots__/UnitInputField.test.js.snap +14 -3
- data/webpack/components/ResourceQuotaForm/components/Resource/index.js +4 -0
- data/webpack/components/ResourceQuotaForm/components/Submit.js +2 -1
- data/webpack/components/ResourceQuotaForm/index.js +33 -19
- data/webpack/components/UpdateResourceQuotaModal.js +7 -1
- metadata +5 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e395e2dfdbaf6a690ee47ed3885daa6d1bf02d66fa9ab4ddc9616214913e1bbe
|
4
|
+
data.tar.gz: c6804f4c3ac2245a891f54ddbec65b5736983466c05e18fedc3d0b6ff926c932
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54de1fc3feda746af897c9161a3079f12e6e0be2ad9ef372ab39a9987cb86866fc7216bd5df13bcc477beb8ec8587be81bd95f858e65e05f034a22d78e88fa93
|
7
|
+
data.tar.gz: '09bf9301c5b261f83e5ca90d78ae0d38b1eff943c0c680d5c8fbd6eeca080f18c8e744f29a58f753b42f35c5c1bd52f04f0a1a562384ebdfa07dd96796cc31f0'
|
data/README.md
CHANGED
@@ -14,20 +14,20 @@ For more information, see [Limiting host resources](https://docs.theforeman.org/
|
|
14
14
|
|
15
15
|
| Foreman Version | Plugin Version |
|
16
16
|
| --------------- | -------------- |
|
17
|
-
| 3.
|
17
|
+
| 3.15 | 0.5.0 |
|
18
|
+
| 3.14 | 0.4.0 |
|
19
|
+
| 3.13 | ~> 0.3.0 |
|
18
20
|
| 3.5 | 0.0.1 |
|
19
21
|
|
20
22
|
## Usage
|
21
23
|
|
22
|
-
_TODO_ Still under development: Official documentation will be added soon.
|
23
|
-
|
24
24
|
When several users share a compute resource or infrastructure, there is a concern that some users could use more than its fair share of resources. Resource Quotas are a tool for administrators to address this concern. They limit access to the shared resource in order to guarantee a fair collaboration.
|
25
25
|
|
26
26
|
In the context of Foreman, multiple users or groups usually share a fixed number of resources (limitation of compute resources like CPU cores, memory, and disk space). As of now, a user cannot be limited when allocating resources. They can create hosts with as many resources as they want. This could lead to over-usage or unequal balancing of resources under the users.
|
27
27
|
|
28
28
|
This plugin introduces the configuration of Resource Quotas. A quota limits specific resources and can be applied to a user or a user group. If a user belongs to a user group, the group’s quota is automatically applied to the user as well. When deploying a new host, a user has to choose a Resource Quota that the host counts to.
|
29
29
|
|
30
|
-
A user is hindered from deploying new hosts, if the new host would exceed the corresponding quota limits. In case, a user belongs to multiple user group with quota, the user can determine which quota new hosts belong to.
|
30
|
+
A user is hindered from deploying new hosts, if the new host would exceed the corresponding quota limits. In case, a user belongs to multiple user group with quota, the user can determine which quota new hosts belong to.
|
31
31
|
|
32
32
|
|
33
33
|
## Contributing
|
@@ -42,7 +42,7 @@ Fork and send a Pull Request. Thanks!
|
|
42
42
|
|
43
43
|
## Copyright
|
44
44
|
|
45
|
-
Copyright (c)
|
45
|
+
Copyright (c) 2025 ATIX AG - https://atix.de
|
46
46
|
|
47
47
|
This program is free software: you can redistribute it and/or modify
|
48
48
|
it under the terms of the GNU General Public License as published by
|
@@ -40,7 +40,7 @@ module ForemanResourceQuota
|
|
40
40
|
end
|
41
41
|
|
42
42
|
api :GET, '/resource_quotas/:id/missing_hosts',
|
43
|
-
N_(
|
43
|
+
N_("Show hosts' resources that could not be determined when calculating the quota utilization")
|
44
44
|
param :id, :identifier, required: true
|
45
45
|
def missing_hosts
|
46
46
|
process_response @resource_quota
|
@@ -5,7 +5,14 @@ module ForemanResourceQuota
|
|
5
5
|
module Api
|
6
6
|
module V2
|
7
7
|
module HostsControllerExtensions
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
before_action :check_if_quota_is_set, only: %i[create update]
|
12
|
+
end
|
13
|
+
|
8
14
|
extend ::Apipie::DSL::Concern
|
15
|
+
|
9
16
|
update_api(:create, :update) do
|
10
17
|
param :host, Hash do
|
11
18
|
param :resource_quota_id, :number, required: false,
|
@@ -13,6 +20,20 @@ module ForemanResourceQuota
|
|
13
20
|
This field is required if the setting `resource_quota_optional_assignment` is set to false.')
|
14
21
|
end
|
15
22
|
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def check_if_quota_is_set # rubocop:disable Metrics/AbcSize
|
27
|
+
return if User.current.quota_assignment_optional?
|
28
|
+
quota = if User.current&.admin?
|
29
|
+
ResourceQuota.where(id: params['host']['resource_quota_id']).first
|
30
|
+
else
|
31
|
+
User.current.resource_quotas.where(id: params['host']['resource_quota_id']).first
|
32
|
+
end
|
33
|
+
return unless quota.nil? || quota.unassigned?
|
34
|
+
render_error :custom_error, status: :unprocessable_entity,
|
35
|
+
locals: { message: 'No valid resource quota provided' }
|
36
|
+
end
|
16
37
|
end
|
17
38
|
end
|
18
39
|
end
|
@@ -2,17 +2,28 @@
|
|
2
2
|
|
3
3
|
module ForemanResourceQuota
|
4
4
|
module HostsHelper
|
5
|
-
def resource_quota_select(form, user_quotas)
|
6
|
-
|
7
|
-
|
5
|
+
def resource_quota_select(form, user_quotas, selected, assignment_optional, host_quota)
|
6
|
+
select_opts = { include_blank: false,
|
7
|
+
selected: selected }
|
8
|
+
html_opts = { label: _('Resource Quota'),
|
9
|
+
required: !assignment_optional,
|
10
|
+
help_inline: if assignment_optional
|
11
|
+
_('Define the Resource Quota this host counts to.')
|
12
|
+
elsif !selected.nil? && (host_quota.nil? ||
|
13
|
+
host_quota == ForemanResourceQuota::ResourceQuota.unassigned.id)
|
14
|
+
format(_("Quota required! Choosing '%s' by default, change here if needed!"),
|
15
|
+
user_quotas.find(selected))
|
16
|
+
else
|
17
|
+
_('Resource quota assignment required!')
|
18
|
+
end }
|
19
|
+
|
8
20
|
select_f form,
|
9
21
|
:resource_quota_id,
|
10
|
-
|
22
|
+
user_quotas,
|
11
23
|
:id,
|
12
24
|
:to_label,
|
13
|
-
|
14
|
-
|
15
|
-
help_inline: _('Define the Resource Quota this host counts to.')
|
25
|
+
select_opts,
|
26
|
+
html_opts
|
16
27
|
end
|
17
28
|
end
|
18
29
|
end
|
@@ -8,5 +8,6 @@ module ForemanResourceQuota
|
|
8
8
|
class HostResourcesException < ResourceQuotaException; end
|
9
9
|
class ResourceQuotaUtilizationException < ResourceQuotaException; end
|
10
10
|
class HostNotFoundException < ResourceQuotaException; end
|
11
|
+
class UnassignedQuotaDeletionException < ResourceQuotaException; end
|
11
12
|
end
|
12
13
|
end
|
@@ -128,22 +128,21 @@ module ForemanResourceQuota
|
|
128
128
|
"while processing host '#{name}'. The Resource Quota utilization values might be inconsistent.")
|
129
129
|
end
|
130
130
|
|
131
|
-
def early_return?(quota)
|
132
|
-
if quota.nil?
|
133
|
-
|
134
|
-
|
131
|
+
def early_return?(quota) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
132
|
+
if quota.nil? || quota.unassigned?
|
133
|
+
self.resource_quota = ResourceQuota.unassigned
|
134
|
+
return true if owner.quota_assignment_optional?
|
135
|
+
if ResourceQuota.assignable.empty? || (!owner.admin? && owner.resource_quotas.assignable.empty?)
|
136
|
+
raise HostResourceQuotaEmptyException,
|
137
|
+
'must be given.'
|
138
|
+
end
|
139
|
+
return true
|
135
140
|
end
|
136
141
|
return true if quota.active_resources.empty?
|
137
142
|
return true if Setting[:resource_quota_global_no_action] # quota is assigned, but not supposed to be checked
|
138
143
|
false
|
139
144
|
end
|
140
145
|
|
141
|
-
def quota_assigment_optional?
|
142
|
-
return true if Setting[:resource_quota_optional_assignment]
|
143
|
-
return true if owner.respond_to?(:resource_quota_is_optional) && owner.resource_quota_is_optional
|
144
|
-
false
|
145
|
-
end
|
146
|
-
|
147
146
|
def save_host_resources
|
148
147
|
host_resources.save
|
149
148
|
end
|
@@ -4,12 +4,39 @@ module ForemanResourceQuota
|
|
4
4
|
module UserExtensions
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
included do
|
7
|
+
after_create :set_unassigned_quota
|
8
|
+
|
7
9
|
has_many :resource_quotas_users, class_name: 'ForemanResourceQuota::ResourceQuotaUser', dependent: :destroy,
|
8
10
|
inverse_of: :user
|
9
11
|
has_many :resource_quotas, class_name: 'ForemanResourceQuota::ResourceQuota', through: :resource_quotas_users
|
10
12
|
attribute :resource_quota_is_optional, :boolean, default: false
|
11
13
|
|
12
14
|
scoped_search relation: :resource_quotas, on: :name, complete_value: true, rename: :resource_quota
|
15
|
+
|
16
|
+
def assignable_resource_quotas
|
17
|
+
resource_quotas.assignable
|
18
|
+
end
|
19
|
+
|
20
|
+
def quota_assignment_optional?
|
21
|
+
Setting['resource_quota_optional_assignment'] || resource_quota_is_optional
|
22
|
+
end
|
23
|
+
|
24
|
+
def show_unassigned_hosts_warning?
|
25
|
+
return false if Setting['resource_quota_optional_assignment']
|
26
|
+
(admin? &&
|
27
|
+
!ForemanResourceQuota::ResourceQuota.unassigned.hosts.empty?) ||
|
28
|
+
(!admin? &&
|
29
|
+
!resource_quota_is_optional &&
|
30
|
+
!ForemanResourceQuota::ResourceQuota.unassigned.hosts.where(owner: self).empty?)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def set_unassigned_quota
|
36
|
+
return if resource_quotas.include?(ForemanResourceQuota::ResourceQuota.unassigned)
|
37
|
+
|
38
|
+
resource_quotas << ForemanResourceQuota::ResourceQuota.unassigned
|
39
|
+
end
|
13
40
|
end
|
14
41
|
end
|
15
42
|
end
|
@@ -4,11 +4,29 @@ module ForemanResourceQuota
|
|
4
4
|
module UsergroupExtensions
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
included do
|
7
|
+
after_create :set_unassigned_quota
|
8
|
+
|
7
9
|
has_many :resource_quotas_usergroups, class_name: 'ForemanResourceQuota::ResourceQuotaUsergroup',
|
8
10
|
dependent: :destroy, inverse_of: :usergroup
|
9
11
|
has_many :resource_quotas, class_name: 'ForemanResourceQuota::ResourceQuota', through: :resource_quotas_usergroups
|
10
12
|
|
11
13
|
scoped_search relation: :resource_quotas, on: :name, complete_value: true, rename: :resource_quota
|
14
|
+
|
15
|
+
def assignable_resource_quotas
|
16
|
+
resource_quotas.assignable
|
17
|
+
end
|
18
|
+
|
19
|
+
def quota_assignment_optional?
|
20
|
+
Setting['resource_quota_optional_assignment']
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def set_unassigned_quota
|
26
|
+
return if resource_quotas.include?(ForemanResourceQuota::ResourceQuota.unassigned)
|
27
|
+
|
28
|
+
resource_quotas << ForemanResourceQuota::ResourceQuota.unassigned
|
29
|
+
end
|
12
30
|
end
|
13
31
|
end
|
14
32
|
end
|
@@ -23,13 +23,26 @@ module ForemanResourceQuota
|
|
23
23
|
|
24
24
|
validates :name, presence: true, uniqueness: true
|
25
25
|
|
26
|
+
before_destroy :do_not_destroy_unassigned_quota, prepend: true
|
27
|
+
before_destroy :assign_unassigned_quota, prepend: true
|
28
|
+
|
26
29
|
scoped_search on: :name, complete_value: true
|
27
30
|
scoped_search on: :id, complete_enabled: false, only_explicit: true, validator: ScopedSearch::Validators::INTEGER
|
28
31
|
|
32
|
+
scope :assignable, -> { where(unassigned: false) }
|
33
|
+
|
34
|
+
def self.unassigned
|
35
|
+
find_by(unassigned: true)
|
36
|
+
end
|
37
|
+
|
29
38
|
def self.permission_name
|
30
39
|
'resource_quotas'
|
31
40
|
end
|
32
41
|
|
42
|
+
def unassigned?
|
43
|
+
unassigned
|
44
|
+
end
|
45
|
+
|
33
46
|
def number_of_hosts
|
34
47
|
hosts_resources.size
|
35
48
|
end
|
@@ -151,6 +164,16 @@ module ForemanResourceQuota
|
|
151
164
|
|
152
165
|
private
|
153
166
|
|
167
|
+
def do_not_destroy_unassigned_quota
|
168
|
+
return unless id == ResourceQuota.unassigned.id
|
169
|
+
raise UnassignedQuotaDeletionException,
|
170
|
+
"You cannot delete the 'Unassigned' quota."
|
171
|
+
end
|
172
|
+
|
173
|
+
def assign_unassigned_quota
|
174
|
+
hosts.update(resource_quota: ResourceQuota.unassigned)
|
175
|
+
end
|
176
|
+
|
154
177
|
# Wrap into a function for easier testing
|
155
178
|
def call_utilization_helper(quota_hosts)
|
156
179
|
utilization_from_resource_origins(active_resources, quota_hosts)
|
@@ -6,6 +6,9 @@
|
|
6
6
|
<% end %>
|
7
7
|
|
8
8
|
<% title _('Resource Quotas') %>
|
9
|
+
<% if User.current.show_unassigned_hosts_warning? %>
|
10
|
+
<%= alert :class => 'alert-warning', :header => _('You have unassigned hosts!'), text: "The setting 'resource_quota_optional_assignment' is set to 'No' but there are hosts without quota assignment. Please check your hosts' quota assignments! " %>
|
11
|
+
<% end %>
|
9
12
|
|
10
13
|
<%= title_actions react_component('CreateResourceQuotaModal') %>
|
11
14
|
|
@@ -22,8 +25,10 @@
|
|
22
25
|
</thead>
|
23
26
|
<tbody>
|
24
27
|
<% @resource_quotas.each do |quota|
|
28
|
+
showAssignmentWarning = quota.unassigned? && User.current.show_unassigned_hosts_warning?
|
25
29
|
react_data = {
|
26
30
|
"isNewQuota": false,
|
31
|
+
"showAssignmentWarning": showAssignmentWarning,
|
27
32
|
"initialProperties": {
|
28
33
|
"id": quota.id,
|
29
34
|
"name": quota.name,
|
@@ -31,6 +36,7 @@
|
|
31
36
|
"cpu_cores": quota.cpu_cores,
|
32
37
|
"memory_mb": quota.memory_mb,
|
33
38
|
"disk_gb": quota.disk_gb,
|
39
|
+
"unassigned": quota.unassigned?,
|
34
40
|
},
|
35
41
|
}
|
36
42
|
%>
|
@@ -43,9 +49,11 @@
|
|
43
49
|
<td><%= h(quota.memory_mb) %></td>
|
44
50
|
<td><%= h(quota.disk_gb) %></td>
|
45
51
|
<td>
|
52
|
+
<% unless quota.unassigned? %>
|
46
53
|
<%= action_buttons(
|
47
54
|
display_delete_if_authorized(hash_for_foreman_resource_quota_resource_quota_path(id: quota), data: { confirm: _("Delete %s?") % quota.name})
|
48
|
-
|
55
|
+
) %>
|
56
|
+
<% end %>
|
49
57
|
</td>
|
50
58
|
</tr>
|
51
59
|
<% end %>
|
@@ -1,4 +1,16 @@
|
|
1
1
|
<%
|
2
|
-
user_quotas = User.current&.admin? ? ForemanResourceQuota::ResourceQuota.all : User.current.resource_quotas
|
2
|
+
user_quotas = User.current&.admin? ? ForemanResourceQuota::ResourceQuota.all : User.current.resource_quotas.distinct
|
3
|
+
user_quotas = user_quotas.assignable if !User.current.quota_assignment_optional?
|
4
|
+
user_quotas = user_quotas.order(:name)
|
5
|
+
selected = @host.resource_quota_id
|
6
|
+
# show Unassigned as default when assignment is optional
|
7
|
+
# show first selectable quota when assignment is mandatory
|
8
|
+
if selected.nil? || selected == ForemanResourceQuota::ResourceQuota.unassigned.id
|
9
|
+
if User.current.quota_assignment_optional?
|
10
|
+
selected = ForemanResourceQuota::ResourceQuota.unassigned.id
|
11
|
+
else
|
12
|
+
selected = user_quotas&.first&.id
|
13
|
+
end
|
14
|
+
end
|
3
15
|
%>
|
4
|
-
<%= resource_quota_select form, user_quotas %>
|
16
|
+
<%= resource_quota_select form, user_quotas, selected, User.current.quota_assignment_optional?, @host.resource_quota_id %>
|
@@ -10,7 +10,7 @@
|
|
10
10
|
:label_help => _("It is optional for a user to assign a quota when creating new hosts") %>
|
11
11
|
<% end %>
|
12
12
|
|
13
|
-
<%= multiple_checkboxes(form, :resource_quotas, resource, ForemanResourceQuota::ResourceQuota, :label => _("Resource Quotas")) %>
|
13
|
+
<%= multiple_checkboxes(form, :resource_quotas, resource, ForemanResourceQuota::ResourceQuota.assignable, :label => _("Resource Quotas")) %>
|
14
14
|
|
15
15
|
<% if resource_type == :user %>
|
16
16
|
<% usergroups = @user.cached_usergroups.includes(:resource_quotas).distinct %>
|
@@ -34,7 +34,7 @@
|
|
34
34
|
<% unless usergroup.resource_quotas.map(&:name).any? %>
|
35
35
|
<li data-id="<%= usergroup.id %>" class="list-group-item"><%= _('This group has no quotas') %></li>
|
36
36
|
<%end %>
|
37
|
-
<% usergroup.
|
37
|
+
<% usergroup.assignable_resource_quotas.map(&:name).each do |quota_name| %>
|
38
38
|
<li data-id="<%= usergroup.id %>" class="list-group-item"><%= quota_name %></li>
|
39
39
|
<% end %>
|
40
40
|
<% end %>
|
@@ -1,7 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class DropMissingHosts < ActiveRecord::Migration[6.1]
|
4
|
-
def
|
5
|
-
drop_table :resource_quotas_missing_hosts
|
4
|
+
def change
|
5
|
+
drop_table :resource_quotas_missing_hosts do |t|
|
6
|
+
t.references :resource_quota, null: false, foreign_key: { to_table: :resource_quotas }
|
7
|
+
t.references :missing_host, null: false, unique: true, foreign_key: { to_table: :hosts }
|
8
|
+
t.boolean :no_cpu_cores, default: false
|
9
|
+
t.boolean :no_memory_mb, default: false
|
10
|
+
t.boolean :no_disk_gb, default: false
|
11
|
+
t.timestamps
|
12
|
+
end
|
6
13
|
end
|
7
14
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Default Quota "Unassigned"
|
4
|
+
ForemanResourceQuota::ResourceQuota.without_auditing do # rubocop:disable Metrics/BlockLength
|
5
|
+
unassigned = ForemanResourceQuota::ResourceQuota.where(
|
6
|
+
name: 'Unassigned',
|
7
|
+
unassigned: true,
|
8
|
+
description: 'Here, you can see all hosts without a dedicated quota.'
|
9
|
+
).first_or_create
|
10
|
+
|
11
|
+
# Add default quota to all users and usergroups
|
12
|
+
User.without_auditing do
|
13
|
+
User.all.except_hidden.each do |user|
|
14
|
+
unless user.resource_quotas.include?(unassigned)
|
15
|
+
user.resource_quotas << unassigned
|
16
|
+
user.save!(validate: false)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Usergroup.without_auditing do
|
22
|
+
Usergroup.all.each do |usergroup|
|
23
|
+
unless usergroup.resource_quotas.include?(unassigned)
|
24
|
+
usergroup.resource_quotas << unassigned
|
25
|
+
usergroup.save!(validate: false)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Move all hosts without a quota to quota "Unassigned"
|
31
|
+
Host.without_auditing do
|
32
|
+
Host.all.each do |host|
|
33
|
+
host.update(resource_quota: unassigned) if host.resource_quota.nil?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -105,5 +105,13 @@ Foreman::Plugin.register :foreman_resource_quota do
|
|
105
105
|
# Future: Overwrite quota-specific "out of resource"-action and take no ..
|
106
106
|
end
|
107
107
|
end
|
108
|
+
extend_page 'hosts/_list' do |context|
|
109
|
+
context.with_profile :resource_quota, _('Resource Quota'), default: true do
|
110
|
+
add_pagelet :hosts_table_column_header, key: :resource_quota_id, label: s_('Resource Quota'),
|
111
|
+
sortable: true, width: '10%', class: 'hidden-xs'
|
112
|
+
add_pagelet :hosts_table_column_content, key: :resource_quota_id,
|
113
|
+
callback: ->(host) { host.resource_quota.name }, class: 'hidden-xs ellipsis'
|
114
|
+
end
|
115
|
+
end
|
108
116
|
end
|
109
117
|
# rubocop: enable Metrics/BlockLength
|
@@ -4,11 +4,10 @@ require 'rake/testtask'
|
|
4
4
|
|
5
5
|
# Tasks
|
6
6
|
namespace :foreman_resource_quota do
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
end
|
7
|
+
desc 'EXPERIMENTAL: Revert all database migrations of this plugin, preparing plugin uninstall'
|
8
|
+
task revert_db_migrations: :environment do
|
9
|
+
plugin = Foreman::Plugin.find ForemanResourceQuota.name.underscore
|
10
|
+
ActiveRecord::MigrationContext.new(plugin.migrations_paths, ActiveRecord::SchemaMigration).down
|
12
11
|
end
|
13
12
|
end
|
14
13
|
|
data/package.json
CHANGED
@@ -24,9 +24,9 @@
|
|
24
24
|
"@theforeman/vendor": ">= 12.0.1"
|
25
25
|
},
|
26
26
|
"devDependencies": {
|
27
|
-
"@babel/core": "^7.
|
27
|
+
"@babel/core": "^7.28.0",
|
28
28
|
"@sheerun/mutationobserver-shim": "^0.3.3",
|
29
|
-
"@testing-library/react": "^16.
|
29
|
+
"@testing-library/react": "^16.3.0",
|
30
30
|
"@testing-library/user-event": "^14.6.1",
|
31
31
|
"@theforeman/builder": ">= 15.0.0",
|
32
32
|
"@theforeman/eslint-plugin-foreman": ">= 15.0.0",
|
@@ -36,8 +36,7 @@
|
|
36
36
|
"babel-eslint": "^10.0.3",
|
37
37
|
"eslint": "^6.7.2",
|
38
38
|
"prettier": "^1.19.1",
|
39
|
-
"
|
40
|
-
"stylelint": "^
|
41
|
-
"stylelint-config-standard": "^37.0.0"
|
39
|
+
"stylelint": "^16.21.1",
|
40
|
+
"stylelint-config-standard": "^38.0.0"
|
42
41
|
}
|
43
42
|
}
|
@@ -7,6 +7,7 @@ exports[`ResourceQuotaEmptyState should render 1`] = `
|
|
7
7
|
<Button
|
8
8
|
id="foreman-resource-quota-welcome-create-modal-button"
|
9
9
|
onClick={[Function]}
|
10
|
+
ouiaId="foreman-resource-quota-welcome-create-modal-button"
|
10
11
|
variant="primary"
|
11
12
|
>
|
12
13
|
Create resource quota
|
@@ -57,6 +58,7 @@ exports[`ResourceQuotaEmptyState should render 1`] = `
|
|
57
58
|
"disk_gb": null,
|
58
59
|
"memory_mb": null,
|
59
60
|
"name": "",
|
61
|
+
"unassigned": false,
|
60
62
|
}
|
61
63
|
}
|
62
64
|
initialStatus={
|
@@ -75,6 +77,7 @@ exports[`ResourceQuotaEmptyState should render 1`] = `
|
|
75
77
|
isNewQuota={true}
|
76
78
|
onSubmit={[Function]}
|
77
79
|
quotaChangesCallback={null}
|
80
|
+
showAssignmentWarning={false}
|
78
81
|
/>
|
79
82
|
</Modal>
|
80
83
|
</div>
|
@@ -2,6 +2,7 @@
|
|
2
2
|
export const RESOURCE_IDENTIFIER_ID = 'id';
|
3
3
|
export const RESOURCE_IDENTIFIER_NAME = 'name';
|
4
4
|
export const RESOURCE_IDENTIFIER_DESCRIPTION = 'description';
|
5
|
+
export const RESOURCE_IDENTIFIER_UNASSIGNED = 'unassigned';
|
5
6
|
export const RESOURCE_IDENTIFIER_CPU = 'cpu_cores';
|
6
7
|
export const RESOURCE_IDENTIFIER_MEMORY = 'memory_mb';
|
7
8
|
export const RESOURCE_IDENTIFIER_DISK = 'disk_gb';
|
@@ -19,6 +19,7 @@ const TextInputField = ({
|
|
19
19
|
isRequired,
|
20
20
|
isTextArea,
|
21
21
|
isNewQuota,
|
22
|
+
isDisabled,
|
22
23
|
}) => {
|
23
24
|
const dispatch = useDispatch();
|
24
25
|
const [currentAttribute, setCurrentAttribute] = useState();
|
@@ -100,7 +101,7 @@ const TextInputField = ({
|
|
100
101
|
loading={isLoading && currentAttribute === attribute}
|
101
102
|
onEdit={onEdit}
|
102
103
|
value={value}
|
103
|
-
disabled={
|
104
|
+
disabled={isDisabled}
|
104
105
|
textArea={isTextArea}
|
105
106
|
validated={validated}
|
106
107
|
{...{ currentAttribute, setCurrentAttribute }}
|
@@ -114,6 +115,7 @@ TextInputField.defaultProps = {
|
|
114
115
|
isRequired: false,
|
115
116
|
isRestrictInputValidation: false,
|
116
117
|
isNewQuota: false,
|
118
|
+
isDisabled: false,
|
117
119
|
};
|
118
120
|
|
119
121
|
TextInputField.propTypes = {
|
@@ -127,6 +129,7 @@ TextInputField.propTypes = {
|
|
127
129
|
isTextArea: PropTypes.bool,
|
128
130
|
isRequired: PropTypes.bool,
|
129
131
|
isNewQuota: PropTypes.bool,
|
132
|
+
isDisabled: PropTypes.bool,
|
130
133
|
};
|
131
134
|
|
132
135
|
export default TextInputField;
|
@@ -17,6 +17,7 @@ import {
|
|
17
17
|
} from '@patternfly/react-core';
|
18
18
|
|
19
19
|
import {
|
20
|
+
ExclamationCircleIcon,
|
20
21
|
UserIcon,
|
21
22
|
UsersIcon,
|
22
23
|
ClusterIcon,
|
@@ -42,6 +43,8 @@ const Properties = ({
|
|
42
43
|
initialName,
|
43
44
|
initialDescription,
|
44
45
|
initialStatus,
|
46
|
+
unassigned,
|
47
|
+
showAssignmentWarning,
|
45
48
|
handleInputValidation,
|
46
49
|
onChange,
|
47
50
|
onApply,
|
@@ -89,6 +92,8 @@ const Properties = ({
|
|
89
92
|
reference={tooltipRefFetchButton}
|
90
93
|
>
|
91
94
|
<Button
|
95
|
+
id="resource-quota-index-button"
|
96
|
+
ouiaId="resource-quota-index-button"
|
92
97
|
isLoading={isFetchLoading}
|
93
98
|
icon={<SyncAltIcon />}
|
94
99
|
size="sm"
|
@@ -111,32 +116,48 @@ const Properties = ({
|
|
111
116
|
<FlexItem>
|
112
117
|
<LabelGroup isCompact>
|
113
118
|
<StatusPropertiesLabel
|
114
|
-
color=
|
115
|
-
iconChild={
|
119
|
+
color={showAssignmentWarning ? 'red' : 'blue'}
|
120
|
+
iconChild={
|
121
|
+
showAssignmentWarning ? (
|
122
|
+
<ExclamationCircleIcon />
|
123
|
+
) : (
|
124
|
+
<ClusterIcon />
|
125
|
+
)
|
126
|
+
}
|
116
127
|
statusContent={
|
117
128
|
statusProperties[RESOURCE_IDENTIFIER_STATUS_NUM_HOSTS]
|
118
129
|
}
|
119
130
|
linkUrl={`/hosts?search=resource_quota="${initialName}"`}
|
120
|
-
tooltipText=
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
statusProperties[RESOURCE_IDENTIFIER_STATUS_NUM_USERS]
|
127
|
-
}
|
128
|
-
linkUrl={`/users?search=resource_quota="${initialName}"`}
|
129
|
-
tooltipText="Number of assigned users"
|
130
|
-
/>
|
131
|
-
<StatusPropertiesLabel
|
132
|
-
color="blue"
|
133
|
-
iconChild={<UsersIcon />}
|
134
|
-
statusContent={
|
135
|
-
statusProperties[RESOURCE_IDENTIFIER_STATUS_NUM_USERGROUPS]
|
131
|
+
tooltipText={
|
132
|
+
showAssignmentWarning
|
133
|
+
? __(
|
134
|
+
"The setting 'resource_quota_optional_assignment' is set to 'No' but there are hosts with no quota assignment. Please check your hosts' quota assignments!"
|
135
|
+
)
|
136
|
+
: __('Number of assigned hosts')
|
136
137
|
}
|
137
|
-
linkUrl={`/usergroups?search=resource_quota="${initialName}"`}
|
138
|
-
tooltipText="Number of assigned usergroups"
|
139
138
|
/>
|
139
|
+
{!unassigned && (
|
140
|
+
<StatusPropertiesLabel
|
141
|
+
color="blue"
|
142
|
+
iconChild={<UserIcon />}
|
143
|
+
statusContent={
|
144
|
+
statusProperties[RESOURCE_IDENTIFIER_STATUS_NUM_USERS]
|
145
|
+
}
|
146
|
+
linkUrl={`/users?search=resource_quota="${initialName}"`}
|
147
|
+
tooltipText="Number of assigned users"
|
148
|
+
/>
|
149
|
+
)}
|
150
|
+
{!unassigned && (
|
151
|
+
<StatusPropertiesLabel
|
152
|
+
color="blue"
|
153
|
+
iconChild={<UsersIcon />}
|
154
|
+
statusContent={
|
155
|
+
statusProperties[RESOURCE_IDENTIFIER_STATUS_NUM_USERGROUPS]
|
156
|
+
}
|
157
|
+
linkUrl={`/usergroups?search=resource_quota="${initialName}"`}
|
158
|
+
tooltipText="Number of assigned usergroups"
|
159
|
+
/>
|
160
|
+
)}
|
140
161
|
</LabelGroup>
|
141
162
|
</FlexItem>
|
142
163
|
</Flex>
|
@@ -144,7 +165,7 @@ const Properties = ({
|
|
144
165
|
};
|
145
166
|
|
146
167
|
return (
|
147
|
-
<Card>
|
168
|
+
<Card id="resource-quota-index-card" ouiaId="resource-quota-index-card">
|
148
169
|
<CardHeader actions={{ actions: renderSyncButton() }}>
|
149
170
|
{renderHeaderTitle()}
|
150
171
|
</CardHeader>
|
@@ -161,6 +182,7 @@ const Properties = ({
|
|
161
182
|
onChange={onChange}
|
162
183
|
isRestrictInputValidation
|
163
184
|
isRequired
|
185
|
+
isDisabled={unassigned}
|
164
186
|
/>
|
165
187
|
<TextInputField
|
166
188
|
initialValue={initialDescription}
|
@@ -171,6 +193,7 @@ const Properties = ({
|
|
171
193
|
onApply={onApply}
|
172
194
|
onChange={onChange}
|
173
195
|
isTextArea
|
196
|
+
isDisabled={unassigned}
|
174
197
|
/>
|
175
198
|
</TextList>
|
176
199
|
</TextContent>
|
@@ -187,6 +210,8 @@ Properties.defaultProps = {
|
|
187
210
|
[RESOURCE_IDENTIFIER_STATUS_NUM_USERS]: null,
|
188
211
|
[RESOURCE_IDENTIFIER_STATUS_NUM_USERGROUPS]: null,
|
189
212
|
},
|
213
|
+
unassigned: false,
|
214
|
+
showAssignmentWarning: false,
|
190
215
|
};
|
191
216
|
|
192
217
|
Properties.propTypes = {
|
@@ -198,6 +223,8 @@ Properties.propTypes = {
|
|
198
223
|
[RESOURCE_IDENTIFIER_STATUS_NUM_USERS]: PropTypes.number,
|
199
224
|
[RESOURCE_IDENTIFIER_STATUS_NUM_USERGROUPS]: PropTypes.number,
|
200
225
|
}),
|
226
|
+
unassigned: PropTypes.bool,
|
227
|
+
showAssignmentWarning: PropTypes.bool,
|
201
228
|
handleInputValidation: PropTypes.func.isRequired,
|
202
229
|
onChange: PropTypes.func.isRequired,
|
203
230
|
onApply: PropTypes.func.isRequired,
|
@@ -49,6 +49,7 @@ const UnitInputField = ({
|
|
49
49
|
unitDropdownItems = units.map(unit => (
|
50
50
|
<DropdownItem
|
51
51
|
id={`unit-dropdownitem-${unit.symbol.toLowerCase()}`}
|
52
|
+
ouiaId={`unit-dropdownitem-${unit.symbol.toLowerCase()}`}
|
52
53
|
key={unit.symbol.toLowerCase()}
|
53
54
|
>
|
54
55
|
{unit.symbol}
|
@@ -169,10 +170,12 @@ const UnitInputField = ({
|
|
169
170
|
return (
|
170
171
|
<InputGroupItem>
|
171
172
|
<Dropdown
|
173
|
+
ouiaId="resource-quota-unit-input-field-input-group-item-dropdown"
|
172
174
|
onSelect={onUnitSelect}
|
173
175
|
toggle={
|
174
176
|
<DropdownToggle
|
175
177
|
isDisabled={isDisabled}
|
178
|
+
ouiaId="resource-quota-unit-input-field-input-group-item-dropdowni-toggle"
|
176
179
|
onToggle={(_event, _val) => onUnitToggle()}
|
177
180
|
>
|
178
181
|
{__(`${selectedUnit.symbol}`)}
|
@@ -233,7 +236,8 @@ const UnitInputField = ({
|
|
233
236
|
min={minValue}
|
234
237
|
max={maxValue}
|
235
238
|
validated={validated}
|
236
|
-
id="
|
239
|
+
id="resource-quota-reg-token-life-time-input"
|
240
|
+
ouiaId="resource-quota-reg-token-life-time-input"
|
237
241
|
onChange={(_event, val) => setInputValue(val)}
|
238
242
|
/>
|
239
243
|
</InputGroupItem>
|
@@ -105,19 +105,20 @@ const UtilizationProgress = ({
|
|
105
105
|
<Tooltip
|
106
106
|
content={resourceUtilizationTooltipText}
|
107
107
|
reference={tooltipRefUtilization}
|
108
|
-
/>
|
109
|
-
<div
|
110
|
-
className={isEnabled ? '' : 'progress-disabled'}
|
111
|
-
ref={tooltipRefUtilization}
|
112
108
|
>
|
113
|
-
<
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
109
|
+
<div
|
110
|
+
className={isEnabled ? '' : 'progress-disabled'}
|
111
|
+
ref={tooltipRefUtilization}
|
112
|
+
>
|
113
|
+
<Progress
|
114
|
+
aria-label={`resource-card-${cardId}-progress`}
|
115
|
+
value={resourceUtilizationPercent}
|
116
|
+
measureLocation={ProgressMeasureLocation.inside}
|
117
|
+
size={ProgressSize.lg}
|
118
|
+
variant={resourceProgressVariant()}
|
119
|
+
/>
|
120
|
+
</div>
|
121
|
+
</Tooltip>
|
121
122
|
</div>
|
122
123
|
);
|
123
124
|
};
|
@@ -17,11 +17,12 @@ exports[`UnitInputField should render as disabled field 1`] = `
|
|
17
17
|
<InputGroup>
|
18
18
|
<InputGroupItem>
|
19
19
|
<TextInput
|
20
|
-
id="
|
20
|
+
id="resource-quota-reg-token-life-time-input"
|
21
21
|
isDisabled={true}
|
22
22
|
max={5}
|
23
23
|
min={0}
|
24
24
|
onChange={[Function]}
|
25
|
+
ouiaId="resource-quota-reg-token-life-time-input"
|
25
26
|
validated="default"
|
26
27
|
value={0}
|
27
28
|
/>
|
@@ -32,11 +33,13 @@ exports[`UnitInputField should render as disabled field 1`] = `
|
|
32
33
|
Array [
|
33
34
|
<DropdownItem
|
34
35
|
id="unit-dropdownitem-mib"
|
36
|
+
ouiaId="unit-dropdownitem-mib"
|
35
37
|
>
|
36
38
|
MiB
|
37
39
|
</DropdownItem>,
|
38
40
|
<DropdownItem
|
39
41
|
id="unit-dropdownitem-gib"
|
42
|
+
ouiaId="unit-dropdownitem-gib"
|
40
43
|
>
|
41
44
|
GiB
|
42
45
|
</DropdownItem>,
|
@@ -44,10 +47,12 @@ exports[`UnitInputField should render as disabled field 1`] = `
|
|
44
47
|
}
|
45
48
|
isOpen={false}
|
46
49
|
onSelect={[Function]}
|
50
|
+
ouiaId="resource-quota-unit-input-field-input-group-item-dropdown"
|
47
51
|
toggle={
|
48
52
|
<DropdownToggle
|
49
53
|
isDisabled={true}
|
50
54
|
onToggle={[Function]}
|
55
|
+
ouiaId="resource-quota-unit-input-field-input-group-item-dropdowni-toggle"
|
51
56
|
>
|
52
57
|
MiB
|
53
58
|
</DropdownToggle>
|
@@ -76,11 +81,12 @@ exports[`UnitInputField should render default 1`] = `
|
|
76
81
|
<InputGroup>
|
77
82
|
<InputGroupItem>
|
78
83
|
<TextInput
|
79
|
-
id="
|
84
|
+
id="resource-quota-reg-token-life-time-input"
|
80
85
|
isDisabled={false}
|
81
86
|
max={5}
|
82
87
|
min={0}
|
83
88
|
onChange={[Function]}
|
89
|
+
ouiaId="resource-quota-reg-token-life-time-input"
|
84
90
|
validated="default"
|
85
91
|
value={0}
|
86
92
|
/>
|
@@ -91,11 +97,13 @@ exports[`UnitInputField should render default 1`] = `
|
|
91
97
|
Array [
|
92
98
|
<DropdownItem
|
93
99
|
id="unit-dropdownitem-mib"
|
100
|
+
ouiaId="unit-dropdownitem-mib"
|
94
101
|
>
|
95
102
|
MiB
|
96
103
|
</DropdownItem>,
|
97
104
|
<DropdownItem
|
98
105
|
id="unit-dropdownitem-gib"
|
106
|
+
ouiaId="unit-dropdownitem-gib"
|
99
107
|
>
|
100
108
|
GiB
|
101
109
|
</DropdownItem>,
|
@@ -103,10 +111,12 @@ exports[`UnitInputField should render default 1`] = `
|
|
103
111
|
}
|
104
112
|
isOpen={false}
|
105
113
|
onSelect={[Function]}
|
114
|
+
ouiaId="resource-quota-unit-input-field-input-group-item-dropdown"
|
106
115
|
toggle={
|
107
116
|
<DropdownToggle
|
108
117
|
isDisabled={false}
|
109
118
|
onToggle={[Function]}
|
119
|
+
ouiaId="resource-quota-unit-input-field-input-group-item-dropdowni-toggle"
|
110
120
|
>
|
111
121
|
MiB
|
112
122
|
</DropdownToggle>
|
@@ -135,11 +145,12 @@ exports[`UnitInputField should render without dropdown (single unit) 1`] = `
|
|
135
145
|
<InputGroup>
|
136
146
|
<InputGroupItem>
|
137
147
|
<TextInput
|
138
|
-
id="
|
148
|
+
id="resource-quota-reg-token-life-time-input"
|
139
149
|
isDisabled={false}
|
140
150
|
max={5}
|
141
151
|
min={0}
|
142
152
|
onChange={[Function]}
|
153
|
+
ouiaId="resource-quota-reg-token-life-time-input"
|
143
154
|
validated="default"
|
144
155
|
value={0}
|
145
156
|
/>
|
@@ -143,6 +143,8 @@ const Resource = ({
|
|
143
143
|
variant="primary"
|
144
144
|
onClick={onClickApply}
|
145
145
|
isLoading={isApplyLoading}
|
146
|
+
id="resource-quota-resource-index-button-apply"
|
147
|
+
ouiaId="resource-quota-resource-index-button-apply"
|
146
148
|
>
|
147
149
|
{__('Apply')}
|
148
150
|
</Button>
|
@@ -154,6 +156,7 @@ const Resource = ({
|
|
154
156
|
isExpanded={isExpanded}
|
155
157
|
isDisabledRaised={!isEnabled}
|
156
158
|
id={`resource-card-${cardId}`}
|
159
|
+
ouiaId={`resource-card-${cardId}`}
|
157
160
|
>
|
158
161
|
<CardHeader
|
159
162
|
actions={{ actions: renderApplyButton() }}
|
@@ -164,6 +167,7 @@ const Resource = ({
|
|
164
167
|
<FlexItem>
|
165
168
|
<Switch
|
166
169
|
id={`switch-${cardId}`}
|
170
|
+
ouiaId={`switch-${cardId}`}
|
167
171
|
aria-label={`switch-${cardId}`}
|
168
172
|
onChange={(_event, val) => onChangeEnabled(val)}
|
169
173
|
isChecked={isEnabled}
|
@@ -48,7 +48,8 @@ const Submit = ({ isValid, onCreate, onSubmit }) => {
|
|
48
48
|
onClick={handleOnSubmit}
|
49
49
|
isLoading={isSubmitLoading}
|
50
50
|
variant="primary"
|
51
|
-
id="submit-button"
|
51
|
+
id="resource-quota-submit-button"
|
52
|
+
ouiaId="resource-quota-submit-button"
|
52
53
|
>
|
53
54
|
{__('Create resource quota')}
|
54
55
|
</Button>
|
@@ -16,6 +16,7 @@ import {
|
|
16
16
|
RESOURCE_IDENTIFIER_CPU,
|
17
17
|
RESOURCE_IDENTIFIER_MEMORY,
|
18
18
|
RESOURCE_IDENTIFIER_DISK,
|
19
|
+
RESOURCE_IDENTIFIER_UNASSIGNED,
|
19
20
|
RESOURCE_IDENTIFIER_STATUS_NUM_HOSTS,
|
20
21
|
RESOURCE_IDENTIFIER_STATUS_NUM_USERS,
|
21
22
|
RESOURCE_IDENTIFIER_STATUS_NUM_USERGROUPS,
|
@@ -25,6 +26,7 @@ import {
|
|
25
26
|
|
26
27
|
const ResourceQuotaForm = ({
|
27
28
|
isNewQuota,
|
29
|
+
showAssignmentWarning,
|
28
30
|
initialProperties,
|
29
31
|
initialStatus,
|
30
32
|
onSubmit,
|
@@ -72,33 +74,41 @@ const ResourceQuotaForm = ({
|
|
72
74
|
RESOURCE_IDENTIFIER_DESCRIPTION
|
73
75
|
)}
|
74
76
|
initialStatus={modelState.getQuotaStatus()}
|
75
|
-
|
76
|
-
|
77
|
-
onApply={modelState.onApply}
|
78
|
-
onFetch={modelState.onFetchUtilization}
|
79
|
-
/>
|
80
|
-
)}
|
81
|
-
</SkeletonLoader>
|
82
|
-
</GalleryItem>
|
83
|
-
<GalleryItem key="edit-resource-quota-resources-item">
|
84
|
-
<SkeletonLoader
|
85
|
-
skeletonProps={{ width: 400 }}
|
86
|
-
status={isNewQuota || !isLoading ? STATUS.RESOLVED : STATUS.PENDING}
|
87
|
-
>
|
88
|
-
{(!isLoading || isNewQuota) && (
|
89
|
-
<Resources
|
90
|
-
isNewQuota={isNewQuota}
|
91
|
-
initialProperties={modelState.getQuotaProperties()}
|
92
|
-
initialStatus={modelState.getQuotaStatus(
|
93
|
-
RESOURCE_IDENTIFIER_STATUS_UTILIZATION
|
77
|
+
unassigned={modelState.getQuotaProperties(
|
78
|
+
RESOURCE_IDENTIFIER_UNASSIGNED
|
94
79
|
)}
|
80
|
+
showAssignmentWarning={showAssignmentWarning}
|
95
81
|
handleInputValidation={modelState.handleInputValidation}
|
96
82
|
onChange={modelState.onChange}
|
97
83
|
onApply={modelState.onApply}
|
84
|
+
onFetch={modelState.onFetchUtilization}
|
98
85
|
/>
|
99
86
|
)}
|
100
87
|
</SkeletonLoader>
|
101
88
|
</GalleryItem>
|
89
|
+
{!modelState.getQuotaProperties(RESOURCE_IDENTIFIER_UNASSIGNED) && (
|
90
|
+
<GalleryItem key="edit-resource-quota-resources-item">
|
91
|
+
<SkeletonLoader
|
92
|
+
skeletonProps={{ width: 400 }}
|
93
|
+
status={
|
94
|
+
isNewQuota || !isLoading ? STATUS.RESOLVED : STATUS.PENDING
|
95
|
+
}
|
96
|
+
>
|
97
|
+
{(!isLoading || isNewQuota) && (
|
98
|
+
<Resources
|
99
|
+
isNewQuota={isNewQuota}
|
100
|
+
initialProperties={modelState.getQuotaProperties()}
|
101
|
+
initialStatus={modelState.getQuotaStatus(
|
102
|
+
RESOURCE_IDENTIFIER_STATUS_UTILIZATION
|
103
|
+
)}
|
104
|
+
handleInputValidation={modelState.handleInputValidation}
|
105
|
+
onChange={modelState.onChange}
|
106
|
+
onApply={modelState.onApply}
|
107
|
+
/>
|
108
|
+
)}
|
109
|
+
</SkeletonLoader>
|
110
|
+
</GalleryItem>
|
111
|
+
)}
|
102
112
|
{isNewQuota && (
|
103
113
|
<GalleryItem key="edit-resource-quota-submit-item">
|
104
114
|
<Submit
|
@@ -114,6 +124,7 @@ const ResourceQuotaForm = ({
|
|
114
124
|
};
|
115
125
|
|
116
126
|
ResourceQuotaForm.defaultProps = {
|
127
|
+
showAssignmentWarning: false,
|
117
128
|
onSubmit: null,
|
118
129
|
quotaChangesCallback: null,
|
119
130
|
initialProperties: {
|
@@ -122,6 +133,7 @@ ResourceQuotaForm.defaultProps = {
|
|
122
133
|
[RESOURCE_IDENTIFIER_CPU]: null,
|
123
134
|
[RESOURCE_IDENTIFIER_MEMORY]: null,
|
124
135
|
[RESOURCE_IDENTIFIER_DISK]: null,
|
136
|
+
[RESOURCE_IDENTIFIER_UNASSIGNED]: false,
|
125
137
|
},
|
126
138
|
initialStatus: {
|
127
139
|
[RESOURCE_IDENTIFIER_STATUS_NUM_HOSTS]: null,
|
@@ -138,6 +150,7 @@ ResourceQuotaForm.defaultProps = {
|
|
138
150
|
|
139
151
|
ResourceQuotaForm.propTypes = {
|
140
152
|
isNewQuota: PropTypes.bool.isRequired,
|
153
|
+
showAssignmentWarning: PropTypes.bool,
|
141
154
|
onSubmit: PropTypes.func,
|
142
155
|
quotaChangesCallback: PropTypes.func,
|
143
156
|
initialProperties: PropTypes.shape({
|
@@ -147,6 +160,7 @@ ResourceQuotaForm.propTypes = {
|
|
147
160
|
[RESOURCE_IDENTIFIER_CPU]: PropTypes.number,
|
148
161
|
[RESOURCE_IDENTIFIER_MEMORY]: PropTypes.number,
|
149
162
|
[RESOURCE_IDENTIFIER_DISK]: PropTypes.number,
|
163
|
+
[RESOURCE_IDENTIFIER_UNASSIGNED]: PropTypes.bool,
|
150
164
|
}),
|
151
165
|
initialStatus: PropTypes.shape({
|
152
166
|
[RESOURCE_IDENTIFIER_STATUS_NUM_HOSTS]: PropTypes.oneOfType([
|
@@ -20,7 +20,11 @@ import {
|
|
20
20
|
RESOURCE_IDENTIFIER_STATUS_UTILIZATION,
|
21
21
|
} from './ResourceQuotaForm/ResourceQuotaFormConstants';
|
22
22
|
|
23
|
-
const UpdateResourceQuotaModal = ({
|
23
|
+
const UpdateResourceQuotaModal = ({
|
24
|
+
initialProperties,
|
25
|
+
initialStatus,
|
26
|
+
showAssignmentWarning,
|
27
|
+
}) => {
|
24
28
|
const staticId = `${MODAL_ID_UPDATE_RESOURCE_QUOTA}-${initialProperties[RESOURCE_IDENTIFIER_ID]}`;
|
25
29
|
const [isOpen, setIsOpen] = useState(false);
|
26
30
|
const [quotaProperties, setQuotaProperties] = useState(initialProperties);
|
@@ -52,6 +56,7 @@ const UpdateResourceQuotaModal = ({ initialProperties, initialStatus }) => {
|
|
52
56
|
>
|
53
57
|
<ResourceQuotaForm
|
54
58
|
isNewQuota={false}
|
59
|
+
showAssignmentWarning={showAssignmentWarning}
|
55
60
|
initialProperties={quotaProperties}
|
56
61
|
initialStatus={quotaStatus}
|
57
62
|
quotaChangesCallback={onQuotaChangesCallback}
|
@@ -138,6 +143,7 @@ UpdateResourceQuotaModal.propTypes = {
|
|
138
143
|
]),
|
139
144
|
}),
|
140
145
|
}),
|
146
|
+
showAssignmentWarning: PropTypes.bool.isRequired,
|
141
147
|
};
|
142
148
|
|
143
149
|
export default UpdateResourceQuotaModal;
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foreman_resource_quota
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bastian Schmidt
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: foreman-tasks
|
@@ -153,6 +152,8 @@ files:
|
|
153
152
|
- db/migrate/20240611141939_drop_missing_hosts.rb
|
154
153
|
- db/migrate/20240611142813_create_hosts_resources.rb
|
155
154
|
- db/migrate/20240618163434_remove_resource_quota_from_hosts.rb
|
155
|
+
- db/migrate/20250410082728_add_unassigned_flag_to_resource_quota.rb
|
156
|
+
- db/seeds.d/030-unassigned_quota.rb
|
156
157
|
- lib/foreman_resource_quota.rb
|
157
158
|
- lib/foreman_resource_quota/async/refresh_resource_quota_utilization.rb
|
158
159
|
- lib/foreman_resource_quota/engine.rb
|
@@ -306,7 +307,6 @@ licenses:
|
|
306
307
|
- GPL-3.0
|
307
308
|
metadata:
|
308
309
|
is_foreman_plugin: 'true'
|
309
|
-
post_install_message:
|
310
310
|
rdoc_options: []
|
311
311
|
require_paths:
|
312
312
|
- lib
|
@@ -321,8 +321,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
321
321
|
- !ruby/object:Gem::Version
|
322
322
|
version: '0'
|
323
323
|
requirements: []
|
324
|
-
rubygems_version: 3.
|
325
|
-
signing_key:
|
324
|
+
rubygems_version: 3.6.7
|
326
325
|
specification_version: 4
|
327
326
|
summary: Foreman Plug-in for resource quota
|
328
327
|
test_files: []
|