lesli_admin 1.1.1 → 1.1.2

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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +3 -0
  3. data/app/assets/stylesheets/lesli_admin/application.css +648 -584
  4. data/app/controllers/lesli_admin/account/details_controller.rb +11 -4
  5. data/app/controllers/lesli_admin/account/locations_controller.rb +8 -4
  6. data/app/controllers/lesli_admin/accounts_controller.rb +41 -27
  7. data/app/controllers/lesli_admin/dashboards_controller.rb +21 -0
  8. data/app/models/lesli_admin/account/currency.rb +4 -2
  9. data/app/models/lesli_admin/account/detail.rb +3 -0
  10. data/app/models/lesli_admin/account/location.rb +12 -2
  11. data/app/models/lesli_admin/account/setting.rb +4 -2
  12. data/app/models/lesli_admin/account.rb +18 -1
  13. data/app/models/lesli_admin/dashboard.rb +8 -1
  14. data/app/services/lesli_admin/account_service.rb +43 -16
  15. data/app/views/lesli_admin/accounts/show.html.erb +104 -7
  16. data/app/views/lesli_admin/dashboards/_component-account-metadata.html.erb +44 -0
  17. data/app/views/lesli_admin/dashboards/_component-installed-engines.html.erb +1 -1
  18. data/app/views/lesli_admin/dashboards/_component-roles.html.erb +1 -0
  19. data/app/views/lesli_admin/dashboards/_component-users.html.erb +1 -0
  20. data/app/views/lesli_admin/settings/_configuration_value.html.erb +52 -0
  21. data/app/views/lesli_admin/settings/_defaults.html.erb +145 -58
  22. data/db/migrate/v1/0101000210_create_lesli_admin_account_details.rb +7 -1
  23. data/db/migrate/v1/0101000410_create_lesli_admin_account_locations.rb +6 -1
  24. data/lib/lesli_admin/engine.rb +0 -4
  25. data/lib/lesli_admin/version.rb +1 -1
  26. data/readme.md +8 -5
  27. metadata +10 -5
  28. data/app/assets/config/lesli_admin_manifest.js +0 -38
  29. data/app/assets/stylesheets/lesli_admin/application.tailwind.css +0 -2
@@ -4,7 +4,7 @@ module LesliAdmin
4
4
 
5
5
  # GET /account/details
6
6
  def index
7
- @account_details = Account::Detail.all
7
+ @account_details = Account::Detail.where(account: admin_account)
8
8
  end
9
9
 
10
10
  # GET /account/details/1
@@ -13,7 +13,7 @@ module LesliAdmin
13
13
 
14
14
  # GET /account/details/new
15
15
  def new
16
- @account_detail = Account::Detail.new
16
+ @account_detail = admin_account.build_detail
17
17
  end
18
18
 
19
19
  # GET /account/details/1/edit
@@ -22,7 +22,7 @@ module LesliAdmin
22
22
 
23
23
  # POST /account/details
24
24
  def create
25
- @account_detail = Account::Detail.new(account_detail_params)
25
+ @account_detail = admin_account.build_detail(account_detail_params)
26
26
 
27
27
  if @account_detail.save
28
28
  redirect_to @account_detail, notice: "Detail was successfully created.", allow_other_host: false
@@ -49,7 +49,14 @@ module LesliAdmin
49
49
  private
50
50
  # Use callbacks to share common setup or constraints between actions.
51
51
  def set_account_detail
52
- @account_detail = Account::Detail.find(params.expect(:id))
52
+ @account_detail = Account::Detail.find_by!(
53
+ id: params.expect(:id),
54
+ account: admin_account
55
+ )
56
+ end
57
+
58
+ def admin_account
59
+ current_user.account.admin
53
60
  end
54
61
 
55
62
  # Only allow a list of trusted parameters through.
@@ -4,7 +4,7 @@ module LesliAdmin
4
4
 
5
5
  # GET /account/locations
6
6
  def index
7
- @account_locations = Account::Location.all
7
+ @account_locations = admin_account.locations
8
8
  end
9
9
 
10
10
  # GET /account/locations/1
@@ -13,7 +13,7 @@ module LesliAdmin
13
13
 
14
14
  # GET /account/locations/new
15
15
  def new
16
- @account_location = Account::Location.new
16
+ @account_location = admin_account.locations.build
17
17
  end
18
18
 
19
19
  # GET /account/locations/1/edit
@@ -22,7 +22,7 @@ module LesliAdmin
22
22
 
23
23
  # POST /account/locations
24
24
  def create
25
- @account_location = Account::Location.new(account_location_params)
25
+ @account_location = admin_account.locations.build(account_location_params)
26
26
 
27
27
  if @account_location.save
28
28
  redirect_to @account_location, notice: "Location was successfully created.", allow_other_host: false
@@ -49,7 +49,11 @@ module LesliAdmin
49
49
  private
50
50
  # Use callbacks to share common setup or constraints between actions.
51
51
  def set_account_location
52
- @account_location = Account::Location.find(params.expect(:id))
52
+ @account_location = admin_account.locations.find(params.expect(:id))
53
+ end
54
+
55
+ def admin_account
56
+ current_user.account.admin
53
57
  end
54
58
 
55
59
  # Only allow a list of trusted parameters through.
@@ -36,25 +36,31 @@ module LesliAdmin
36
36
 
37
37
  def show
38
38
  @account = AccountService.new(current_user, query).show
39
+
39
40
  respond_to do |format|
40
41
  format.html
41
42
  format.turbo_stream
42
43
  format.json do
43
- respond_with_successful(@account)
44
+ respond_with_successful(account_payload(@account))
44
45
  end
45
46
  end
46
47
  end
47
48
 
48
49
  def update
49
-
50
- # check saved
51
- if @account.update(account_params)
50
+ if AccountService.new(current_user, query).update(account_params)
52
51
  log(:description => 'Updated the account details')
53
52
  respond_with_lesli(
54
- :turbo => stream_notification_success('Account updated')
53
+ turbo: stream_notification_success("Account updated successfully"),
54
+ json: account_payload(@account),
55
+ redirect: account_path
56
+ )
57
+ else
58
+ respond_with_lesli(
59
+ turbo: stream_notification_danger(@account.errors.full_messages.to_sentence),
60
+ json: { errors: @account.errors.to_hash },
61
+ html: :show,
62
+ status: :unprocessable_content
55
63
  )
56
- else
57
- #respond_with_error(@account.errors)
58
64
  end
59
65
  end
60
66
 
@@ -69,31 +75,39 @@ module LesliAdmin
69
75
  # Only allow a list of trusted parameters through.
70
76
  def account_params
71
77
  params.require(:account).permit(
72
- :id,
73
78
  :name,
74
79
  :email,
80
+ :status,
81
+ :region,
75
82
  detail_attributes: [
76
- :company_name_legal,
77
- :company_tagline
83
+ :company_name,
84
+ :company_name_legal,
85
+ :company_tagline,
86
+ :country,
87
+ :address,
88
+ :region,
89
+ :city,
90
+ :postal_code,
91
+ :website,
92
+ :phone_number_1,
93
+ :phone_number_2,
94
+ :phone_number_3,
95
+ :phone_number_4,
96
+ :public_email,
97
+ :github,
98
+ :twitter,
99
+ :youtube,
100
+ :linkedin,
101
+ :facebook
78
102
  ]
79
- # :company_name_legal,
80
- # :company_tagline,
81
- # :country,
82
- # :city,
83
- # :postal_code,
84
- # :address,
85
- # :region,
86
- # :website,
87
- # :phone_number_1,
88
- # :public_email,
89
- # :github,
90
- # :twitter,
91
- # :youtube,
92
- # :linkedin,
93
- # :facebook,
94
- # :status,
95
- # :users_id
96
103
  )
97
104
  end
105
+
106
+ def account_payload(account)
107
+ account.as_json.merge(
108
+ "detail_attributes" => (account.admin.detail&.as_json || {})
109
+ )
110
+ end
111
+
98
112
  end
99
113
  end
@@ -32,5 +32,26 @@ Building a better future, one line of code at a time.
32
32
 
33
33
  module LesliAdmin
34
34
  class DashboardsController < Lesli::Shared::DashboardsController
35
+ def show
36
+ @account_summary = AccountService.new(current_user, query).summary
37
+
38
+ synchronize_dashboard_components if defined?(LesliDashboard)
39
+
40
+ return super if defined?(LesliDashboard)
41
+
42
+ render :show
43
+ end
44
+
45
+ private
46
+
47
+ def synchronize_dashboard_components
48
+ dashboard_account = current_user.account.dashboard
49
+ return unless dashboard_account
50
+
51
+ LesliDashboard::DashboardService.new(current_user).register(dashboard_account)
52
+
53
+ dashboard = dashboard_account.dashboards.find_by(engine: "LesliAdmin")
54
+ dashboard&.components&.where(component: Dashboard::RETIRED_COMPONENTS)&.destroy_all
55
+ end
35
56
  end
36
57
  end
@@ -1,4 +1,6 @@
1
1
  module LesliAdmin
2
- class Account::Currency < ApplicationRecord
3
- end
2
+ class Account::Currency < ApplicationRecord
3
+ belongs_to :account, class_name: "LesliAdmin::Account", inverse_of: :currencies
4
+ belongs_to :user, class_name: "Lesli::User", optional: true
5
+ end
4
6
  end
@@ -1,4 +1,7 @@
1
1
  module LesliAdmin
2
2
  class Account::Detail < ApplicationRecord
3
+ belongs_to :account, class_name: "LesliAdmin::Account", inverse_of: :detail
4
+
5
+ validates :account_id, uniqueness: true
3
6
  end
4
7
  end
@@ -1,4 +1,14 @@
1
1
  module LesliAdmin
2
- class Account::Location < ApplicationRecord
3
- end
2
+ class Account::Location < ApplicationRecord
3
+ belongs_to :account, class_name: "LesliAdmin::Account", inverse_of: :locations
4
+ belongs_to :parent, class_name: "LesliAdmin::Account::Location", optional: true
5
+
6
+ has_many(
7
+ :children,
8
+ class_name: "LesliAdmin::Account::Location",
9
+ foreign_key: :parent_id,
10
+ inverse_of: :parent,
11
+ dependent: :nullify
12
+ )
13
+ end
4
14
  end
@@ -1,4 +1,6 @@
1
1
  module LesliAdmin
2
- class Account::Setting < ApplicationRecord
3
- end
2
+ class Account::Setting < ApplicationRecord
3
+ belongs_to :account, class_name: "LesliAdmin::Account", inverse_of: :settings
4
+ belongs_to :user, class_name: "Lesli::User", optional: true
5
+ end
4
6
  end
@@ -32,8 +32,25 @@ Building a better future, one line of code at a time.
32
32
 
33
33
  module LesliAdmin
34
34
  class Account < ApplicationRecord
35
- belongs_to :account, class_name: "Lesli::Account"
35
+ belongs_to :account, class_name: "Lesli::Account", inverse_of: :admin
36
+ validates :account_id, uniqueness: true
36
37
  has_many :dashboards
38
+ has_many :settings, class_name: "LesliAdmin::Account::Setting", dependent: :destroy
39
+ has_many :currencies, class_name: "LesliAdmin::Account::Currency", dependent: :destroy
40
+ has_many(
41
+ :locations,
42
+ class_name: "LesliAdmin::Account::Location",
43
+ inverse_of: :account,
44
+ dependent: :destroy
45
+ )
46
+ has_one(
47
+ :detail,
48
+ class_name: "LesliAdmin::Account::Detail",
49
+ inverse_of: :account,
50
+ dependent: :destroy
51
+ )
52
+
53
+ accepts_nested_attributes_for :detail, update_only: true
37
54
 
38
55
  after_create :initialize_account
39
56
 
@@ -32,6 +32,13 @@ Building a better future, one line of code at a time.
32
32
 
33
33
  module LesliAdmin
34
34
  class Dashboard < Lesli::Shared::Dashboard
35
- COMPONENTS = %i[installed_engines]
35
+ RETIRED_COMPONENTS = %w[locations currencies settings].freeze
36
+
37
+ COMPONENTS = [
38
+ { users: { size: 4, position: 1 } },
39
+ { roles: { size: 4, position: 2 } },
40
+ { installed_engines: { size: 4, position: 3 } },
41
+ { account_metadata: { size: 12, position: 4 } }
42
+ ].freeze
36
43
  end
37
44
  end
@@ -32,25 +32,52 @@ Building a better future, one line of code at a time.
32
32
 
33
33
  module LesliAdmin
34
34
  class AccountService < Lesli::ApplicationLesliService
35
- def show
36
- account = Lesli::Account #.left_joins(:detail)
37
- .where(:id => current_user.account.id)
38
- .select(:id, :email, :name, :status) #, :company_name_legal, :company_tagline)
39
- .first
35
+ def show
36
+ account = Lesli::Account
37
+ .includes(:user, admin: :detail)
38
+ .find(current_user.account.id)
39
+
40
+ account.admin.build_detail unless account.admin.detail
41
+ account
42
+ end
43
+
44
+ def update(attributes)
45
+ account = current_user.account
46
+ account_attributes = attributes.to_h.deep_symbolize_keys
47
+ detail_attributes = account_attributes.delete(:detail_attributes)
48
+
49
+ ActiveRecord::Base.transaction do
50
+ account.update!(account_attributes)
51
+ account.admin.update!(detail_attributes: detail_attributes) if detail_attributes
52
+ end
53
+
54
+ account
55
+ rescue ActiveRecord::RecordInvalid => exception
56
+ copy_errors(exception.record, to: account)
57
+ false
58
+ end
59
+
60
+ def summary
61
+ account = Lesli::Account
62
+ .includes(:user, :users, :roles)
63
+ .find(current_user.account.id)
40
64
 
41
65
  {
42
- id: account.id,
43
- name: account.name,
44
- email: account.email,
45
- status: account.status,
46
- # detail_attributes: {
47
- # company_name_legal: account.company_name_legal,
48
- # company_tagline: account.company_tagline
49
- # }
66
+ account: account,
67
+ users: account.users.size,
68
+ roles: account.roles.size,
69
+ installed_engines: LesliSystem.engines.size
50
70
  }
71
+ end
72
+
73
+ private
74
+
75
+ def copy_errors(source, to:)
76
+ return if source.equal?(to)
51
77
 
52
- #current_user.account.joins(:detail)
53
- return account
54
- end
78
+ source.errors.each do |error|
79
+ to.errors.add(:detail, error.full_message)
80
+ end
81
+ end
55
82
  end
56
83
  end
@@ -1,15 +1,112 @@
1
-
2
1
  <%= render(LesliView::Layout::Container.new("accounts")) do %>
2
+ <%= render(
3
+ LesliView::Components::Header.new(
4
+ "Account",
5
+ "Manage the core identity, company profile, and resources for #{@account.name}."
6
+ )
7
+ ) %>
3
8
 
4
- <%= render(LesliView::Components::Header.new("Account information")) %>
9
+ <% if @account.errors.any? %>
10
+ <div class="mb-6 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">
11
+ <p class="font-semibold">The account could not be updated.</p>
12
+ <ul class="mt-2 list-disc space-y-1 pl-5">
13
+ <% @account.errors.full_messages.each do |message| %>
14
+ <li><%= message %></li>
15
+ <% end %>
16
+ </ul>
17
+ </div>
18
+ <% end %>
5
19
 
6
20
  <%= render LesliView::Components::Tabs.new do |tabs| %>
7
- <% tabs.with_tab(title: "General Information", icon: "business") do %>
21
+ <% tabs.with_tab(id: "general", title: "General", icon: "business") do %>
8
22
  <%= form_with(model: @account, url: account_path, builder: LesliView::Forms::Builder) do |form| %>
9
- <%= form.fieldset do %>
10
- <%= form.field_control(:name, label: "Company name")%>
11
- <%= form.field_control(:email, label: "Email") %>
12
- <%= form.field_control_submit %>
23
+ <%= form.fieldset("Core account") do %>
24
+ <p class="mb-5 text-sm leading-6 text-slate-500">
25
+ These values identify the account throughout every installed Lesli engine.
26
+ </p>
27
+
28
+ <div class="grid grid-cols-1 gap-x-5 md:grid-cols-2">
29
+ <%= form.field_control(
30
+ :name,
31
+ label: "Account name",
32
+ autocomplete: "organization",
33
+ required: true
34
+ ) %>
35
+ <%= form.field_control_email(
36
+ :email,
37
+ label: "Account email",
38
+ autocomplete: "email",
39
+ required: true
40
+ ) %>
41
+ <%= form.field_control_select(
42
+ :status,
43
+ Lesli::Account.statuses.keys.map { |status| [status, status] },
44
+ label: "Status"
45
+ ) %>
46
+ <%= form.field_control_select(
47
+ :region,
48
+ Lesli::Account.regions.keys.map { |region| [region, region] },
49
+ label: "Data region"
50
+ ) %>
51
+ </div>
52
+
53
+ <div class="mt-2 flex justify-end">
54
+ <%= form.field_control_button("Save account", category: "primary", icon: "save") %>
55
+ </div>
56
+ <% end %>
57
+ <% end %>
58
+ <% end %>
59
+
60
+ <% tabs.with_tab(id: "company-profile", title: "Company profile", icon: "domain") do %>
61
+ <%= form_with(model: @account, url: account_path, builder: LesliView::Forms::Builder) do |form| %>
62
+ <%= form.fields_for(:detail, @account.admin.detail) do |detail| %>
63
+ <%= detail.fieldset("Company information") do %>
64
+ <div class="grid grid-cols-1 gap-x-5 md:grid-cols-2">
65
+ <%= detail.field_control(:company_name, label: "Public company name", autocomplete: "organization") %>
66
+ <%= detail.field_control(:company_name_legal, label: "Legal company name") %>
67
+ <div class="md:col-span-2">
68
+ <%= detail.field_control(:company_tagline, label: "Tagline") %>
69
+ </div>
70
+ </div>
71
+ <% end %>
72
+
73
+ <%= detail.fieldset("Address and contact") do %>
74
+ <div class="grid grid-cols-1 gap-x-5 md:grid-cols-2">
75
+ <div class="md:col-span-2">
76
+ <%= detail.field_control(:address, label: "Address", autocomplete: "street-address") %>
77
+ </div>
78
+ <%= detail.field_control(:city, label: "City", autocomplete: "address-level2") %>
79
+ <%= detail.field_control(:region, label: "State or region", autocomplete: "address-level1") %>
80
+ <%= detail.field_control(
81
+ :country,
82
+ label: "Country catalog ID",
83
+ message: "Numeric identifier from the configured country catalog.",
84
+ type: "number",
85
+ min: 0
86
+ ) %>
87
+ <%= detail.field_control(:postal_code, label: "Postal code", autocomplete: "postal-code") %>
88
+ <%= detail.field_control_email(:public_email, label: "Public email", autocomplete: "email") %>
89
+ <%= detail.field_control(:website, label: "Website", type: "url", autocomplete: "url") %>
90
+ <%= detail.field_control(:phone_number_1, label: "Primary phone", type: "tel", autocomplete: "tel") %>
91
+ <%= detail.field_control(:phone_number_2, label: "Secondary phone", type: "tel") %>
92
+ <%= detail.field_control(:phone_number_3, label: "Additional phone 1", type: "tel") %>
93
+ <%= detail.field_control(:phone_number_4, label: "Additional phone 2", type: "tel") %>
94
+ </div>
95
+ <% end %>
96
+
97
+ <%= detail.fieldset("Social profiles") do %>
98
+ <div class="grid grid-cols-1 gap-x-5 md:grid-cols-2">
99
+ <%= detail.field_control(:github, label: "GitHub", type: "url") %>
100
+ <%= detail.field_control(:linkedin, label: "LinkedIn", type: "url") %>
101
+ <%= detail.field_control(:twitter, label: "X / Twitter", type: "url") %>
102
+ <%= detail.field_control(:facebook, label: "Facebook", type: "url") %>
103
+ <%= detail.field_control(:youtube, label: "YouTube", type: "url") %>
104
+ </div>
105
+
106
+ <div class="mt-2 flex justify-end">
107
+ <%= detail.field_control_button("Save company profile", category: "primary", icon: "save") %>
108
+ </div>
109
+ <% end %>
13
110
  <% end %>
14
111
  <% end %>
15
112
  <% end %>
@@ -0,0 +1,44 @@
1
+ <%
2
+ account = @account_summary[:account]
3
+ status_classes = case account.status
4
+ when "active"
5
+ "bg-emerald-100 text-emerald-700"
6
+ when "suspended"
7
+ "bg-red-100 text-red-700"
8
+ when "onboarding"
9
+ "bg-amber-100 text-amber-700"
10
+ else
11
+ "bg-sky-100 text-sky-700"
12
+ end
13
+ %>
14
+
15
+ <section class="h-full rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6" aria-labelledby="account-metadata-title">
16
+ <div class="flex flex-wrap items-start justify-between gap-4">
17
+ <div>
18
+ <h2 id="account-metadata-title" class="text-base font-semibold text-slate-900">Account metadata</h2>
19
+ <p class="mt-1 text-sm text-slate-500">Read-only identifiers and lifecycle information.</p>
20
+ </div>
21
+ <span class="inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold <%= status_classes %>">
22
+ <%= account.status.humanize %>
23
+ </span>
24
+ </div>
25
+
26
+ <dl class="mt-5 grid grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-2 xl:grid-cols-4">
27
+ <div>
28
+ <dt class="text-sm font-medium text-slate-500">Account ID</dt>
29
+ <dd class="mt-1 break-all text-sm text-slate-900"><%= account.id %></dd>
30
+ </div>
31
+ <div>
32
+ <dt class="text-sm font-medium text-slate-500">Owner</dt>
33
+ <dd class="mt-1 break-all text-sm text-slate-900"><%= account.user&.email || "Not assigned" %></dd>
34
+ </div>
35
+ <div>
36
+ <dt class="text-sm font-medium text-slate-500">Created</dt>
37
+ <dd class="mt-1 text-sm text-slate-900"><%= l(account.created_at, format: :long) %></dd>
38
+ </div>
39
+ <div>
40
+ <dt class="text-sm font-medium text-slate-500">Last updated</dt>
41
+ <dd class="mt-1 text-sm text-slate-900"><%= l(account.updated_at, format: :long) %></dd>
42
+ </div>
43
+ </dl>
44
+ </section>
@@ -1 +1 @@
1
- <%= render(LesliView::Widgets::Count.new('Installed engines', LesliSystem.engines.size)) %>
1
+ <%= render(LesliView::Widgets::Count.new("Installed engines", @account_summary[:installed_engines])) %>
@@ -0,0 +1 @@
1
+ <%= render(LesliView::Widgets::Count.new("Roles", @account_summary[:roles])) %>
@@ -0,0 +1 @@
1
+ <%= render(LesliView::Widgets::Count.new("Users", @account_summary[:users])) %>
@@ -0,0 +1,52 @@
1
+ <%
2
+ sensitive_key = setting_key.to_s.match?(/password|secret|token|api[_-]?key|private[_-]?key|credential|encryption|salt/i)
3
+ %>
4
+
5
+ <% if sensitive_key %>
6
+ <span class="inline-flex items-center gap-1.5 rounded-full bg-slate-200 px-2.5 py-1 text-xs font-semibold text-slate-600">
7
+ <span class="material-symbols-outlined text-sm" aria-hidden="true">lock</span>
8
+ Filtered
9
+ </span>
10
+ <% elsif value.is_a?(Hash) %>
11
+ <dl class="space-y-2">
12
+ <% value.each do |nested_key, nested_value| %>
13
+ <div class="rounded-lg border border-slate-200 bg-white p-3">
14
+ <dt class="text-xs font-medium text-slate-500"><%= nested_key.to_s.humanize %></dt>
15
+ <dd class="mt-1.5">
16
+ <%= render("configuration_value", setting_key: nested_key, value: nested_value) %>
17
+ </dd>
18
+ </div>
19
+ <% end %>
20
+ </dl>
21
+ <% elsif value.is_a?(Array) %>
22
+ <% if value.empty? %>
23
+ <span class="text-slate-400">Empty list</span>
24
+ <% else %>
25
+ <ul class="flex flex-wrap gap-2">
26
+ <% value.each do |item| %>
27
+ <li class="rounded-lg border border-slate-200 bg-white px-2.5 py-1 text-xs text-slate-700">
28
+ <%= render("configuration_value", setting_key: :item, value: item) %>
29
+ </li>
30
+ <% end %>
31
+ </ul>
32
+ <% end %>
33
+ <% elsif value == true %>
34
+ <span class="inline-flex items-center gap-1.5 rounded-full bg-emerald-100 px-2.5 py-1 text-xs font-semibold text-emerald-700">
35
+ <span class="h-1.5 w-1.5 rounded-full bg-emerald-500" aria-hidden="true"></span>
36
+ Enabled
37
+ </span>
38
+ <% elsif value == false %>
39
+ <span class="inline-flex items-center gap-1.5 rounded-full bg-red-100 px-2.5 py-1 text-xs font-semibold text-red-700">
40
+ <span class="h-1.5 w-1.5 rounded-full bg-red-500" aria-hidden="true"></span>
41
+ Disabled
42
+ </span>
43
+ <% elsif value.nil? %>
44
+ <span class="italic text-slate-400">Not configured</span>
45
+ <% elsif value.to_s.match?(/\A#[0-9a-f]{3,8}\z/i) %>
46
+ <span class="inline-flex items-center gap-2">
47
+ <span class="h-5 w-5 rounded-md border border-slate-300 shadow-sm" style="background-color: <%= value %>" aria-hidden="true"></span>
48
+ <code class="break-all rounded-md bg-slate-200 px-2 py-1 text-xs text-slate-700"><%= value %></code>
49
+ </span>
50
+ <% else %>
51
+ <code class="break-all rounded-md bg-slate-200 px-2 py-1 text-xs leading-5 text-slate-700"><%= value %></code>
52
+ <% end %>