ultra_settings 2.7.0 → 2.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f70521135220d74fe5ef4fbc6261c9d5b8a93f200f898af0fead2ebb7d31cbfd
4
- data.tar.gz: 42330a1a294bee7ab53c61f37b56cd0b5b6f309326f1809bfd012cbfcd67fa58
3
+ metadata.gz: 846f2c54ef9d4019e4f97ad02dcc962813a29fccc6128fe1e18ef9f263c01c2e
4
+ data.tar.gz: 7d28f71ae610362fd8e2c4e0ffe2ae885d7b9f270fc0e942f41fd7e8daff18fc
5
5
  SHA512:
6
- metadata.gz: aab2c06121a67e227f1628d1dd3a22590e0bb174f132506840112278a5c09484e57dd6bc176ddbb35d576388c29bb5f6da462473f1c5ebf76724fe8677312ed2
7
- data.tar.gz: f7e9bd08a2958def30780e0ef966d8b90321d3f9c1bf469677b7c51ab8b512e867bf0e1fc1d910d72f24c9f71818a34050b3a2dbfdc2c5adb9afa4a3e048948a
6
+ metadata.gz: 8829e070b1f37474fc6673804d219d1485b2cb36c5d81f9dcd4b88ff6623bd1f99a5a135adedf4cc2a2f1865ac27d74b8f405b01896aabc3da2b5e41226333f3
7
+ data.tar.gz: 0e906fddfea5764d653beb41c99d534c30b44974a83b8f7e3de1d76dd1656e367fe160743f4ac65ec422bf0e1c7d0c35b28ff2d5e3ce4252bf70d53d0f367709
data/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 2.8.0
8
+
9
+ ### Added
10
+
11
+ - Added support for setting a description on configuration classes. The class description can serve to document the purpose of the configuration and will be shown in the web UI.
12
+ - Improved menu for selecting configuration classes in the web UI by adding a search box to filter the list of configurations.
13
+
14
+ ### Fixed
15
+
16
+ - Fixed filtering of data sources for configuration classes in the web UI.
17
+ - The YAML source will not be shown if the configuration file is set to nil or false.
18
+ - The runtime settings source will not be shown if the runtime settings engine is not set up.
19
+ - Fields that override the class default for a data source will now correctly show that source.
20
+
7
21
  ## 2.7.0
8
22
 
9
23
  ### Added
data/README.md CHANGED
@@ -424,7 +424,7 @@ If you prefer to embed the settings view directly into your own admin tools or d
424
424
  ```erb
425
425
  <h1>Configuration</h1>
426
426
 
427
- <%= UltraSettings::ApplicationView.new.render(select_class: "form-select") %>
427
+ <%= UltraSettings::ApplicationView.new.render %>
428
428
  ```
429
429
 
430
430
  This approach allows for seamless integration of the settings UI into your application's admin interface, leveraging your existing authentication and authorization mechanisms. The settings are rendered with navigation handled by an HTML select element. You can specify the CSS classes for the select element to match your own application styles..
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.7.0
1
+ 2.8.0
@@ -0,0 +1,30 @@
1
+ <% config_class = configuration.class
2
+ has_config_file = config_class.configuration_file.is_a?(Pathname) && config_class.fields.any?(&:yaml_key) %>
3
+
4
+ <% if config_class.description || has_config_file %>
5
+ <div class="ultra-settings-description-container">
6
+ <% if config_class.description %>
7
+ <div class="ultra-settings-description">
8
+ <%= html_escape(config_class.description).gsub("\n", "<br>") %>
9
+ </div>
10
+ <% end %>
11
+
12
+ <% if has_config_file %>
13
+ <div class="ultra-settings-description">
14
+ <span class="ultra-settings-config-file-label">
15
+ Configuration file:
16
+ </span>
17
+
18
+ <code class="ultra-settings-config-file-path">
19
+ <%= html_escape(relative_path(configuration.class.configuration_file)) %>
20
+ </code>
21
+
22
+ <% unless configuration.class.configuration_file&.exist? %>
23
+ <span class="ultra-settings-file-not-found">
24
+ (file does not exist)
25
+ </span>
26
+ <% end %>
27
+ </div>
28
+ <% end %>
29
+ </div>
30
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <div class="ultra-settings-configuration-list" style="display:none;">
2
+ <% configurations.each do |configuration| %>
3
+ <div class="ultra-settings-configuration-summary">
4
+ <a
5
+ href="#<%= html_escape(configuration.class.name) %>"
6
+ class="ultra-settings-configuration-title"
7
+ >
8
+ <%= html_escape(configuration.class.name) %>
9
+ </a>
10
+
11
+ <p><%= html_escape(configuration.class.description) %></p>
12
+ </div>
13
+ <% end %>
14
+ </div>
@@ -0,0 +1,53 @@
1
+ <div class="ultra-settings-dropdown" id="config-dropdown">
2
+ <button
3
+ type="button"
4
+ class="ultra-settings-dropdown-button"
5
+ id="config-dropdown-button"
6
+ >
7
+ Select Configuration
8
+ </button>
9
+
10
+ <div
11
+ class="ultra-settings-dropdown-menu"
12
+ id="config-dropdown-menu"
13
+ style="display: none;"
14
+ >
15
+ <div class="ultra-settings-search-container">
16
+ <input
17
+ type="text"
18
+ id="config-search"
19
+ placeholder="Search..."
20
+ autocomplete="off"
21
+ >
22
+ </div>
23
+
24
+ <ul class="ultra-settings-dropdown-list" id="config-list">
25
+ <% configurations.each do |config| %>
26
+ <% config_text = [
27
+ config.class.name,
28
+ config.class.description
29
+ ]
30
+ config.class.fields.each do |field|
31
+ config_text << field.name
32
+ config_text << field.description
33
+ config_text << field.env_var
34
+ config_text << field.runtime_setting
35
+ end
36
+ search_text = config_text.compact.join(" ").downcase %>
37
+
38
+ <li
39
+ class="ultra-settings-dropdown-item"
40
+ data-value="config-<%= html_escape(config.class.name) %>"
41
+ data-label="<%= html_escape(config.class.name) %>"
42
+ data-search="<%= html_escape(search_text) %>"
43
+ >
44
+ <span class="ultra-settings-check-icon"></span>
45
+
46
+ <span class="ultra-settings-config-name">
47
+ <%= html_escape(config.class.name) %>
48
+ </span>
49
+ </li>
50
+ <% end %>
51
+ </ul>
52
+ </div>
53
+ </div>
data/app/application.css CHANGED
@@ -2,22 +2,22 @@
2
2
  margin-bottom: 1rem;
3
3
  }
4
4
 
5
- .ultra-settings-nav form {
6
- margin: 0;
7
- }
8
-
9
- .ultra-settings-config-file {
5
+ .ultra-settings-description-container {
10
6
  margin-bottom: 1.5rem;
11
- padding: 1rem;
12
- background-color: var(--config-file-bg-color);
7
+ line-height: 1.5;
13
8
  border: 1px solid var(--config-file-border-color);
14
9
  border-radius: 0.375rem;
10
+ padding: 1rem 1rem 0 1rem;
11
+ }
12
+
13
+ .ultra-settings-description {
14
+ margin-bottom: 1rem;
15
+ color: var(--description-color);
15
16
  }
16
17
 
17
18
  .ultra-settings-config-file-label {
18
- font-weight: 600;
19
- color: var(--text-color);
20
- margin-right: 0.5rem;
19
+ font-weight: 500;
20
+ color: var(--description-color);
21
21
  }
22
22
 
23
23
  .ultra-settings-config-file-path {
@@ -184,9 +184,9 @@ code.ultra-settings-field-data-value {
184
184
  letter-spacing: 0.025em;
185
185
  }
186
186
 
187
- .ultra-settings-source-value-dfn {
188
- display: inline-block;
189
- margin-left: 0.25rem;
187
+ .ultra-settings-source-indicator::before {
188
+ content: "● ";
189
+ font-size: 1rem;
190
190
  }
191
191
 
192
192
  .ultra-settings-icon-info {
@@ -218,27 +218,6 @@ code.ultra-settings-field-data-value {
218
218
  opacity: 1;
219
219
  }
220
220
 
221
- .ultra-settings-select {
222
- display: inline-block;
223
- padding: .375rem 2.25rem .375rem .75rem;
224
- -moz-padding-start: calc(0.75rem - 3px);
225
- font-size: 1rem;
226
- font-weight: 400;
227
- line-height: 1.5;
228
- color: var(--form-control-color);
229
- background-color: var(--form-control-bg-color);
230
- background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
231
- background-repeat: no-repeat;
232
- background-position: right .75rem center;
233
- background-size: 16px 12px;
234
- border: 1px solid var(--form-control-border-color);
235
- border-radius: .25rem;
236
- transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
237
- -webkit-appearance: none;
238
- -moz-appearance: none;
239
- appearance: none;
240
- }
241
-
242
221
  .ultra-settings-dialog {
243
222
  min-width: 20rem;
244
223
  padding: 0;
@@ -273,4 +252,163 @@ code.ultra-settings-field-data-value {
273
252
  padding: 1rem;
274
253
  background-color: var(--background-color);
275
254
  color: var(--text-color);
255
+ }
256
+
257
+ /* Dropdown Styles */
258
+ .ultra-settings-dropdown {
259
+ position: relative;
260
+ display: inline-block;
261
+ width: 100%;
262
+ max-width: 400px;
263
+ }
264
+
265
+ .ultra-settings-dropdown-button {
266
+ width: 100%;
267
+ text-align: left;
268
+ padding: 0.5rem 1rem;
269
+ font-size: 1.25rem;
270
+ font-weight: 500;
271
+ background-color: var(--form-control-bg-color);
272
+ color: var(--form-control-color);
273
+ border: 1px solid var(--form-control-border-color);
274
+ border-radius: 0.375rem;
275
+ cursor: pointer;
276
+ display: flex;
277
+ justify-content: space-between;
278
+ align-items: center;
279
+ }
280
+
281
+ .ultra-settings-dropdown-button::after {
282
+ content: "";
283
+ display: inline-block;
284
+ width: 0;
285
+ height: 0;
286
+ margin-left: 0.5rem;
287
+ vertical-align: middle;
288
+ border-top: 4px solid;
289
+ border-right: 4px solid transparent;
290
+ border-left: 4px solid transparent;
291
+ }
292
+
293
+ .ultra-settings-dropdown-menu {
294
+ position: absolute;
295
+ top: 100%;
296
+ left: 0;
297
+ z-index: 1000;
298
+ display: none;
299
+ min-width: 100%;
300
+ padding: 0.5rem 0;
301
+ margin: 0.125rem 0 0;
302
+ font-size: 1rem;
303
+ color: var(--text-color);
304
+ text-align: left;
305
+ list-style: none;
306
+ background-color: var(--form-control-bg-color);
307
+ background-clip: padding-box;
308
+ border: 1px solid var(--form-control-border-color);
309
+ border-radius: 0.375rem;
310
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
311
+ }
312
+
313
+ .ultra-settings-search-container {
314
+ padding: 0.5rem 1rem;
315
+ border-bottom: 1px solid var(--form-control-border-color);
316
+ }
317
+
318
+ #config-search {
319
+ width: 100%;
320
+ padding: 0.375rem 0.75rem;
321
+ font-size: 0.875rem;
322
+ line-height: 1.5;
323
+ color: var(--form-control-color);
324
+ background-color: var(--value-bg-color);
325
+ background-clip: padding-box;
326
+ border: 1px solid var(--form-control-border-color);
327
+ border-radius: 0.25rem;
328
+ box-sizing: border-box;
329
+ }
330
+
331
+ .ultra-settings-dropdown-list {
332
+ list-style: none;
333
+ padding: 0;
334
+ margin: 0;
335
+ max-height: 300px;
336
+ overflow-y: auto;
337
+ }
338
+
339
+ .ultra-settings-dropdown-item {
340
+ display: flex;
341
+ align-items: center;
342
+ padding: 0.5rem 1rem;
343
+ clear: both;
344
+ font-weight: 400;
345
+ color: var(--form-control-color);
346
+ text-align: inherit;
347
+ white-space: nowrap;
348
+ background-color: transparent;
349
+ border: 0;
350
+ cursor: pointer;
351
+ }
352
+
353
+ .ultra-settings-dropdown-item:hover {
354
+ background-color: var(--field-header-bg-color);
355
+ }
356
+
357
+ .ultra-settings-check-icon {
358
+ width: 1.5rem;
359
+ display: inline-block;
360
+ text-align: center;
361
+ font-weight: bold;
362
+ }
363
+
364
+ .ultra-settings-dropdown-item.selected .ultra-settings-check-icon::before {
365
+ content: "✓";
366
+ }
367
+
368
+ .ultra-settings-title {
369
+ margin: 0;
370
+ font-size: 1.25rem;
371
+ font-weight: 500;
372
+ color: var(--text-color);
373
+ }
374
+
375
+ /* Configuration List Styles */
376
+ .ultra-settings-configuration-list {
377
+ display: grid;
378
+ grid-template-columns: repeat(auto-fill, minmax(max(300px, 30%), 1fr));
379
+ gap: 1.5rem;
380
+ margin-top: 1.5rem;
381
+ }
382
+
383
+ .ultra-settings-configuration-summary {
384
+ background-color: var(--field-bg-color);
385
+ border: 1px solid var(--field-border-color);
386
+ border-radius: 0.5rem;
387
+ padding: 1.5rem;
388
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
389
+ overflow: hidden;
390
+ }
391
+
392
+ .ultra-settings-configuration-summary:hover {
393
+ transform: translateY(-2px);
394
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
395
+ }
396
+
397
+ .ultra-settings-configuration-title {
398
+ display: block;
399
+ font-size: 1.25rem;
400
+ font-weight: 600;
401
+ color: var(--description-color);
402
+ text-decoration: none;
403
+ margin-bottom: 0.75rem;
404
+ }
405
+
406
+ .ultra-settings-configuration-title:hover {
407
+ text-decoration: underline;
408
+ }
409
+
410
+ .ultra-settings-configuration-summary p {
411
+ margin: 0;
412
+ color: var(--description-color);
413
+ line-height: 1.5;
276
414
  }
data/app/application.js CHANGED
@@ -1,41 +1,124 @@
1
1
  document.addEventListener("DOMContentLoaded", () => {
2
- const menu = document.getElementById("config-selector");
2
+ const dropdown = document.getElementById("config-dropdown");
3
+ const button = document.getElementById("config-dropdown-button");
4
+ const menu = document.getElementById("config-dropdown-menu");
5
+ const searchInput = document.getElementById("config-search");
6
+ const items = document.querySelectorAll(".ultra-settings-dropdown-item");
7
+ const configurations = document.querySelectorAll(".ultra-settings-configuration");
8
+ const configList = document.querySelector(".ultra-settings-configuration-list");
3
9
 
4
- showCurrentConfiguration = () => {
5
- const selectedId = menu.options[menu.selectedIndex].value;
6
- const hash = selectedId.replace(/^config-/, "");
10
+ // If no dropdown, we might be in single config mode or no configs.
11
+ if (!dropdown) {
12
+ // If there is exactly one configuration, show it.
13
+ if (configurations.length === 1) {
14
+ configurations[0].style.display = "block";
15
+ }
16
+ return;
17
+ }
7
18
 
8
- document.querySelectorAll(".ultra-settings-configuration").forEach((configuration) => {
9
- if (configuration.id === selectedId) {
10
- configuration.style.display = "block";
11
- window.location.hash = hash;
19
+ const toggleMenu = () => {
20
+ const isVisible = menu.style.display === "block";
21
+ menu.style.display = isVisible ? "none" : "block";
22
+ if (!isVisible) {
23
+ searchInput.value = "";
24
+ filterItems("");
25
+ searchInput.focus();
26
+ }
27
+ };
28
+
29
+ const closeMenu = () => {
30
+ menu.style.display = "none";
31
+ };
32
+
33
+ const filterItems = (query) => {
34
+ const lowerQuery = query.toLowerCase();
35
+ items.forEach(item => {
36
+ const label = item.getAttribute("data-search").toLowerCase();
37
+ if (label.includes(lowerQuery)) {
38
+ item.style.display = "flex";
12
39
  } else {
13
- configuration.style.display = "none";
40
+ item.style.display = "none";
14
41
  }
15
42
  });
16
- }
43
+ };
17
44
 
18
- menu.addEventListener("change", showCurrentConfiguration);
45
+ const showConfigList = () => {
46
+ if (configList) configList.style.display = "grid";
47
+ configurations.forEach(config => config.style.display = "none");
48
+ items.forEach(item => item.classList.remove("selected"));
49
+ button.textContent = "Select Configuration";
50
+ closeMenu();
51
+ };
19
52
 
20
- const setCurrentSelection = () => {
21
- const hash = window.location.hash.replace(/^#/, "");
22
- const selectedId = `config-${hash}`;
23
- for (const option of menu.options) {
24
- if (option.value === selectedId) {
25
- option.selected = true;
26
- break;
53
+ const showConfig = (configId) => {
54
+ if (configList) configList.style.display = "none";
55
+
56
+ configurations.forEach(config => {
57
+ config.style.display = config.id === configId ? "block" : "none";
58
+ });
59
+
60
+ items.forEach(item => {
61
+ if (item.getAttribute("data-value") === configId) {
62
+ item.classList.add("selected");
63
+ button.textContent = item.getAttribute("data-label");
64
+ } else {
65
+ item.classList.remove("selected");
27
66
  }
67
+ });
68
+
69
+ closeMenu();
70
+ };
71
+
72
+ // Event Listeners
73
+ button.addEventListener("click", (e) => {
74
+ e.stopPropagation();
75
+ toggleMenu();
76
+ });
77
+
78
+ document.addEventListener("click", (e) => {
79
+ if (!dropdown.contains(e.target)) {
80
+ closeMenu();
28
81
  }
82
+ });
29
83
 
30
- showCurrentConfiguration();
31
- }
84
+ searchInput.addEventListener("input", (e) => {
85
+ filterItems(e.target.value);
86
+ });
32
87
 
33
- window.addEventListener('hashchange', setCurrentSelection);
88
+ items.forEach(item => {
89
+ item.addEventListener("click", () => {
90
+ const configId = item.getAttribute("data-value");
91
+ const hash = configId.replace(/^config-/, "");
34
92
 
93
+ if (item.classList.contains("selected")) {
94
+ // Toggle off: clear hash
95
+ history.pushState("", document.title, window.location.pathname + window.location.search);
96
+ handleHashChange();
97
+ } else {
98
+ window.location.hash = hash;
99
+ }
100
+ });
101
+ });
35
102
 
36
- if (window.location.hash) {
37
- setCurrentSelection()
38
- }
103
+ // Initial Load & Hash Change
104
+ const handleHashChange = () => {
105
+ const hash = window.location.hash.replace(/^#/, "");
106
+ if (hash) {
107
+ const configId = `config-${hash}`;
108
+ // Check if config exists
109
+ const exists = Array.from(items).some(item => item.getAttribute("data-value") === configId);
110
+ if (exists) {
111
+ showConfig(configId);
112
+ } else {
113
+ showConfigList();
114
+ }
115
+ } else {
116
+ showConfigList();
117
+ }
118
+ };
119
+
120
+ window.addEventListener("hashchange", handleHashChange);
39
121
 
40
- showCurrentConfiguration();
122
+ // Run once on load
123
+ handleHashChange();
41
124
  });
@@ -1,28 +1,28 @@
1
1
  <div class="ultra-settings-block">
2
- <% if !configuration.class.yaml_config_disabled? && configuration.class.configuration_file.is_a?(Pathname) %>
3
- <div class="ultra-settings-config-file">
4
- <span class="ultra-settings-config-file-label">Configuration File:</span>
5
- <code class="ultra-settings-config-file-path"><%= html_escape(relative_path(configuration.class.configuration_file)) %></code>
6
- <% unless configuration.class.configuration_file&.exist? %>
7
- <span class="ultra-settings-file-not-found">(File does not exist)</span>
8
- <% end %>
9
- </div>
10
- <% end %>
2
+ <%= render_partial "config_description", configuration: configuration %>
11
3
 
12
4
  <div class="ultra-settings-fields">
13
5
  <% configuration.class.fields.each do |field| %>
14
6
  <% source = configuration.__source__(field.name) %>
7
+
15
8
  <div class="ultra-settings-field">
16
9
  <div class="ultra-settings-field-header">
17
10
  <div class="ultra-settings-field-name">
18
11
  <code><%= html_escape(field.name) %></code>
12
+
19
13
  <% if field.secret? %>
20
- <span class="ultra-settings-field-badge ultra-settings-badge-secret">secret</span>
14
+ <span class="ultra-settings-field-badge ultra-settings-badge-secret">
15
+ secret
16
+ </span>
21
17
  <% end %>
18
+
22
19
  <% if field.static? %>
23
- <span class="ultra-settings-field-badge ultra-settings-badge-static">static</span>
20
+ <span class="ultra-settings-field-badge ultra-settings-badge-static">
21
+ static
22
+ </span>
24
23
  <% end %>
25
24
  </div>
25
+
26
26
  <div class="ultra-settings-field-type">
27
27
  <%= html_escape(field.type) %>
28
28
  </div>
@@ -32,15 +32,20 @@
32
32
  <% if configuration[field.name].nil? %>
33
33
  <span class="ultra-settings-nil-value">nil</span>
34
34
  <% elsif field.secret? %>
35
- <code class="ultra-settings-field-data-value"><%= html_escape(secret_value(configuration[field.name])) %></code>
35
+ <code class="ultra-settings-field-data-value">
36
+ <%= html_escape(secret_value(configuration[field.name])) %>
37
+ </code>
36
38
  <% else %>
37
- <code class="ultra-settings-field-data-value"><%= html_escape(display_value(configuration[field.name])) %></code>
39
+ <code class="ultra-settings-field-data-value">
40
+ <%= html_escape(display_value(configuration[field.name])) %>
41
+ </code>
38
42
  <% end %>
39
43
  </div>
40
44
 
41
45
  <% unless field.description.to_s.empty? %>
42
46
  <div class="ultra-settings-field-description">
43
47
  <%= info_icon %>
48
+
44
49
  <div class="ultra-settings-description-text">
45
50
  <%= html_escape(field.description) %>
46
51
  </div>
@@ -48,61 +53,84 @@
48
53
  <% end %>
49
54
 
50
55
  <div class="ultra-settings-field-sources">
51
- <% if field.env_var && !configuration.class.environment_variables_disabled? %>
56
+ <% sources = configuration.__available_sources__(field.name) %>
57
+ <% if sources.include?(:env) %>
52
58
  <div class="ultra-settings-source <%= 'ultra-settings-source-active' if source == :env %>">
53
59
  <span class="ultra-settings-source-type">
54
60
  Environment Variable
55
61
  </span>
62
+
56
63
  <code class="ultra-settings-source-value">
57
64
  <%= field.env_var %>
58
65
  <%= show_defined_value(field.env_var, configuration.__value_from_source__(field.name, :env), field.secret?) %>
59
66
  </code>
67
+
60
68
  <% if source == :env %>
61
- <span class="ultra-settings-source-indicator">Currently active</span>
69
+ <span class="ultra-settings-source-indicator">
70
+ Currently active
71
+ </span>
62
72
  <% end %>
63
73
  </div>
64
74
  <% end %>
65
75
 
66
- <% if field.runtime_setting && !configuration.class.runtime_settings_disabled? %>
76
+ <% if sources.include?(:settings) %>
67
77
  <div class="ultra-settings-source <%= 'ultra-settings-source-active' if source == :settings %>">
68
78
  <span class="ultra-settings-source-type">Runtime Setting</span>
79
+
69
80
  <code class="ultra-settings-source-value">
70
81
  <%= field.runtime_setting %>
71
82
  <%= show_defined_value(field.runtime_setting, configuration.__value_from_source__(field.name, :settings), field.secret?) %>
72
83
  </code>
84
+
73
85
  <% if source == :settings %>
74
- <span class="ultra-settings-source-indicator">Currently active</span>
86
+ <span class="ultra-settings-source-indicator">
87
+ Currently active
88
+ </span>
75
89
  <% end %>
90
+
76
91
  <% edit_url = UltraSettings.runtime_settings_url(name: field.runtime_setting, type: field.type, description: field.description) %>
92
+
77
93
  <% if edit_url %>
78
- <a href="<%= html_escape(edit_url) %>" class="ultra-settings-edit-link" title="Edit <%= html_escape(field.runtime_setting) %>">
94
+ <a
95
+ href="<%= html_escape(edit_url) %>"
96
+ class="ultra-settings-edit-link"
97
+ title="Edit <%= html_escape(field.runtime_setting) %>"
98
+ >
79
99
  <%= edit_icon %>
80
100
  </a>
81
101
  <% end %>
82
102
  </div>
83
103
  <% end %>
84
104
 
85
- <% if field.yaml_key && !configuration.class.yaml_config_disabled? %>
105
+ <% if sources.include?(:yaml) %>
86
106
  <div class="ultra-settings-source <%= 'ultra-settings-source-active' if source == :yaml %>">
87
107
  <span class="ultra-settings-source-type">Configuration File</span>
108
+
88
109
  <code class="ultra-settings-source-value">
89
110
  <%= field.yaml_key %>
90
111
  <%= show_defined_value(field.yaml_key, configuration.__value_from_source__(field.name, :yaml), field.secret?) %>
91
112
  </code>
113
+
92
114
  <% if source == :yaml %>
93
- <span class="ultra-settings-source-indicator">Currently active</span>
115
+ <span class="ultra-settings-source-indicator">
116
+ Currently active
117
+ </span>
94
118
  <% end %>
95
119
  </div>
96
120
  <% end %>
97
121
 
98
- <% if !field.default.nil? || source == :default %>
122
+ <% if sources.include?(:default) || source == :default %>
99
123
  <div class="ultra-settings-source <%= 'ultra-settings-source-active' if source == :default %>">
100
124
  <span class="ultra-settings-source-type">Default Value</span>
125
+
101
126
  <code class="ultra-settings-source-value">
102
127
  <%= show_defined_value("Default Value", field.default, field.secret?) %>
103
128
  </code>
129
+
104
130
  <% if source == :default %>
105
- <span class="ultra-settings-source-indicator">Currently active</span>
131
+ <span class="ultra-settings-source-indicator">
132
+ Currently active
133
+ </span>
106
134
  <% end %>
107
135
  </div>
108
136
  <% end %>
@@ -114,10 +142,15 @@
114
142
  <dialog class="ultra-settings-dialog" closedby="any">
115
143
  <div class="ultra-settings-dialog-header">
116
144
  <div class="ultra-settings-dialog-title"></div>
117
- <button class="ultra-settings-dialog-close" onclick="this.closest('.ultra-settings-dialog').close();">
145
+
146
+ <button
147
+ class="ultra-settings-dialog-close"
148
+ onclick="this.closest('.ultra-settings-dialog').close();"
149
+ >
118
150
  <%= close_icon %>
119
151
  </button>
120
152
  </div>
153
+
121
154
  <div class="ultra-settings-dialog-body">
122
155
  <code class="ultra-settings-field-data-value ultra-settings-dialog-value"></code>
123
156
  </div>
data/app/index.html.erb CHANGED
@@ -2,25 +2,30 @@
2
2
 
3
3
  <div class="ultra-settings">
4
4
  <div class="ultra-settings-nav">
5
- <form onsubmit="return false" style="margin-bottom: 0.5rem;">
6
- <select class="<%= html_escape(select_class) %>" size="1" id="config-selector">
7
- <% UltraSettings.__configuration_names__.sort.each do |name| %>
8
- <% configuration = UltraSettings.send(name) %>
9
- <% next if configuration.class.fields.empty? %>
5
+ <% configurations = UltraSettings.__configurations__.reject { |config| config.class.fields.empty? }.sort_by { |config| config.class.name.downcase } %>
10
6
 
11
- <option value="config-<%= html_escape(name) %>"><%= html_escape(UltraSettings.send(name).class.name) %></option>
12
- <% end %>
13
- </select>
14
- </form>
7
+ <% if configurations.size == 1 %>
8
+ <h2 class="ultra-settings-title">
9
+ <%= html_escape(configurations.first.class.name) %>
10
+ </h2>
11
+ <% else %>
12
+ <%= render_partial "select_menu", configurations: configurations %>
13
+ <% end %>
15
14
  </div>
16
15
 
17
- <% UltraSettings.__configuration_names__.sort.each do |name| %>
18
- <% configuration = UltraSettings.send(name) %>
19
- <% next if configuration.class.fields.empty? %>
20
-
21
- <div class="ultra-settings-configuration" id="config-<%= html_escape(name) %>" style="display:none;">
22
- <%= UltraSettings::ConfigurationView.new(configuration).render(table_class: table_class) %>
23
- </div>
16
+ <% if configurations.size == 1 %>
17
+ <%= UltraSettings::ConfigurationView.new(configurations.first).render(table_class: table_class) %>
18
+ <% else %>
19
+ <%= render_partial "config_list", configurations: configurations %>
20
+ <% configurations.each do |configuration| %>
21
+ <div
22
+ class="ultra-settings-configuration"
23
+ id="config-<%= html_escape(configuration.class.name) %>"
24
+ style="display:none;"
25
+ >
26
+ <%= UltraSettings::ConfigurationView.new(configuration).render(table_class: table_class) %>
27
+ </div>
28
+ <% end %>
24
29
  <% end %>
25
30
  </div>
26
31
 
@@ -4,15 +4,15 @@ module UltraSettings
4
4
  # This class can render information about all configurations. It is used by the bundled
5
5
  # web UI, but you can use it to embed the configuration information in your own web pages.
6
6
  #
7
- # The output will be a simple HTML drop down list that can be used to display an HTML element
8
- # showing each configuration. You can specify the CSS class for the select element by passing
9
- # the `select_class` option to the `render` method. By default the select element has
10
- # the class `ultra-settings-select`.
7
+ # The output will be a simple HTML drop down menu that can be used to select the configuration
8
+ # you want to see.
11
9
  #
12
10
  # @example
13
11
  # <h1>Application Configuration</h1>
14
- # <%= UltraSettings::ApplicationView.new.render(select_class: 'form-control') %>
12
+ # <%= UltraSettings::ApplicationView.new.render %>
15
13
  class ApplicationView
14
+ include RenderHelper
15
+
16
16
  attr_reader :css
17
17
 
18
18
  # Initialize the application view with a color scheme.
@@ -25,10 +25,10 @@ module UltraSettings
25
25
 
26
26
  # Render the HTML for the configuration settings UI.
27
27
  #
28
- # @param select_class [String] CSS class for the select element.
29
- # @param table_class [String] CSS class for the table element (for backwards compatibility).
28
+ # @param select_class [String] Deprecated; no longer used.
29
+ # @param table_class [String] Deprecated; no longer used.
30
30
  # @return [String] The rendered HTML.
31
- def render(select_class: "ultra-settings-select", table_class: "")
31
+ def render(select_class: nil, table_class: nil)
32
32
  html = ViewHelper.erb_template("index.html.erb").result(binding)
33
33
  html = html.html_safe if html.respond_to?(:html_safe)
34
34
  html
@@ -52,10 +52,6 @@ module UltraSettings
52
52
 
53
53
  private
54
54
 
55
- def html_escape(value)
56
- ERB::Util.html_escape(value)
57
- end
58
-
59
55
  def javascript
60
56
  ViewHelper.read_app_file("application.js")
61
57
  end
@@ -9,8 +9,20 @@ module UltraSettings
9
9
 
10
10
  @env_var_prefix = nil
11
11
  @runtime_setting_prefix = nil
12
+ @description = nil
12
13
 
13
14
  class << self
15
+ # Set a description for the configuration. This is optional. It will be displayed
16
+ # in the web UI if provided. On large projects with many configurations, this can
17
+ # help identify the purpose of each configuration.
18
+ #
19
+ # @param text [String] The description text.
20
+ # @return [void]
21
+ def description(text = nil)
22
+ @description = text.to_s.strip unless text.nil?
23
+ @description
24
+ end
25
+
14
26
  # Define a field on the configuration. This will create a getter method for the field.
15
27
  # The field value will be read from the environment, runtime settings, or a YAML file
16
28
  # and coerced to the specified type. Empty strings will be converted to nil.
@@ -27,14 +39,19 @@ module UltraSettings
27
39
  # @param static [Boolean] If true, the field value should never be changed. This is useful for
28
40
  # fields that are used at startup to set static values in the application. Static field cannot
29
41
  # be read from runtime settings.
30
- # @param runtime_setting [String, Symbol] The name of the runtime setting to use for the field.
42
+ # @param secret [Boolean, Proc] If true, the field value will be obscured in the output of
43
+ # to_hash. If a proc is provided, it will be called to determine if the field is secret.
44
+ # @param runtime_setting [String, Symbol, Boolean] The name of the runtime setting to use for the field.
31
45
  # By default this will be the underscored name of the class plus a dot plus the field name
32
- # (i.e. MyServiceConfiguration#foo becomes "my_service.foo").
33
- # @param env_var [String, Symbol] The name of the environment variable to use for the field.
46
+ # (i.e. MyServiceConfiguration#foo becomes "my_service.foo"). If set to false, runtime settings
47
+ # will be ignored for this field. This can be set to true to use the default name.
48
+ # @param env_var [String, Symbol, Boolean] The name of the environment variable to use for the field.
34
49
  # By default this will be the underscored name of the class plus an underscore plus the field name
35
- # all in uppercase (i.e. MyServiceConfiguration#foo becomes "MY_SERVICE_FOO").
36
- # @param yaml_key [String, Symbol] The name of the YAML key to use for the field. By default
37
- # this is the name of the field.
50
+ # all in uppercase (i.e. MyServiceConfiguration#foo becomes "MY_SERVICE_FOO"). If set to false,
51
+ # environment variables will be ignored for this field. This can be set to true to use the default name.
52
+ # @param yaml_key [String, Symbol, Boolean] The name of the YAML key to use for the field. By default
53
+ # this is the name of the field. If set to false, YAML configuration will be ignored for this field.
54
+ # This can be set to true to use the default name.
38
55
  # @return [void]
39
56
  def field(name, type: :string, description: nil, default: nil, default_if: nil, static: nil, secret: nil, runtime_setting: nil, env_var: nil, yaml_key: nil)
40
57
  name = name.to_s
@@ -145,7 +162,7 @@ module UltraSettings
145
162
  # directory (i.e. MyServiceConfiguration has a default config path of
146
163
  # "my_service.yml").
147
164
  #
148
- # @param value [String, Pathname]
165
+ # @param value [String, Pathname, false, nil]
149
166
  # @return [void]
150
167
  def configuration_file=(value)
151
168
  value = nil if value == false
@@ -513,6 +530,22 @@ module UltraSettings
513
530
  end
514
531
  end
515
532
 
533
+ # Returns an array of the available data sources for the field.
534
+ #
535
+ # @param name [String, Symbol] the name of the field.
536
+ # @return [Array<Symbol>] The available sources (:env, :settings, :yaml, :default).
537
+ def __available_sources__(name)
538
+ field = self.class.send(:defined_fields)[name.to_s]
539
+ raise ArgumentError.new("Unknown field: #{name.inspect}") unless field
540
+
541
+ sources = []
542
+ sources << :env if field.env_var
543
+ sources << :settings if field.runtime_setting && UltraSettings.__runtime_settings__
544
+ sources << :yaml if field.yaml_key && self.class.configuration_file
545
+ sources << :default unless field.default.nil?
546
+ sources
547
+ end
548
+
516
549
  # Output the current state of the configuration as a hash. If the field is marked as a secret,
517
550
  # then the value will be a secure hash of the value instead of the value itself.
518
551
  #
@@ -11,6 +11,8 @@ module UltraSettings
11
11
  # <h1>Service Configuration</h1>
12
12
  # <%= UltraSettings::ConfigurationView.new(ServiceConfiguration.instance).render %>
13
13
  class ConfigurationView
14
+ include RenderHelper
15
+
14
16
  # Initialize the configuration view with a configuration instance.
15
17
  #
16
18
  # @param configuration [UltraSettings::Configuration] The configuration instance to display.
@@ -38,10 +40,6 @@ module UltraSettings
38
40
 
39
41
  private
40
42
 
41
- def html_escape(value)
42
- ERB::Util.html_escape(value)
43
- end
44
-
45
43
  def display_value(value)
46
44
  case value
47
45
  when Time
@@ -147,8 +145,6 @@ module UltraSettings
147
145
  HTML
148
146
  end
149
147
 
150
- private
151
-
152
148
  def open_dialog_script
153
149
  <<~JAVASCRIPT.gsub(/\s+/, " ").tr('"', "'")
154
150
  this.closest('.ultra-settings-configuration').querySelector('.ultra-settings-dialog-title').textContent = this.dataset.label;
@@ -16,12 +16,12 @@ module UltraSettings
16
16
  # @param type [Symbol] The type of the field.
17
17
  # @param description [String] The description of the field.
18
18
  # @param default [Object] The default value of the field.
19
- # @param default_if [Proc] A proc that returns true if the default value should be used.
19
+ # @param default_if [Proc, Symbol] A proc that returns true if the default value should be used.
20
20
  # @param env_var [String, Symbol] The name of the environment variable to use for the field.
21
21
  # @param runtime_setting [String, Symbol] The name of the setting to use for the field.
22
22
  # @param yaml_key [String, Symbol] The name of the YAML key to use for the field.
23
23
  # @param static [Boolean] Whether or not the field is static and cannot be changed at runtime.
24
- # @param secret [Boolean] Whether or not the field contains a value that should be kept secret.
24
+ # @param secret [Boolean, Proc] Whether or not the field contains a value that should be kept secret.
25
25
  def initialize(
26
26
  name:,
27
27
  type: :string,
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UltraSettings
4
+ # Helper methods for rendering views.
5
+ module RenderHelper
6
+ # HTML escape a value.
7
+ #
8
+ # @param value [String] The value to escape.
9
+ # @return [String] The escaped value.
10
+ def html_escape(value)
11
+ ERB::Util.html_escape(value)
12
+ end
13
+
14
+ # Render a partial template with the given locals.
15
+ #
16
+ # @param partial_name [String] The name of the partial template (without the leading underscore and file extension).
17
+ # @param locals [Hash] A hash of local variables to pass to the template.
18
+ # @return [String] The rendered HTML of the partial.
19
+ def render_partial(partial_name, locals = {})
20
+ template = ViewHelper.erb_template("_#{partial_name}.html.erb")
21
+ b = binding
22
+ locals.each do |key, value|
23
+ b.local_variable_set(key, value)
24
+ end
25
+ template.result(b)
26
+ end
27
+ end
28
+ end
@@ -11,6 +11,7 @@ module UltraSettings
11
11
  # @param path [String] The path to the template file.
12
12
  # @return [ERB] The compiled ERB template.
13
13
  def erb_template(path)
14
+ @cache.clear if development_mode?
14
15
  @cache["erb:#{path}"] ||= ERB.new(read_app_file(path))
15
16
  end
16
17
 
@@ -19,6 +20,7 @@ module UltraSettings
19
20
  # @param path [String] The path to the file relative to the app directory.
20
21
  # @return [String] The contents of the file.
21
22
  def read_app_file(path)
23
+ @cache.clear if development_mode?
22
24
  @cache["file:#{path}"] ||= File.read(File.join(app_dir, path))
23
25
  end
24
26
 
@@ -28,6 +30,12 @@ module UltraSettings
28
30
  def app_dir
29
31
  File.expand_path(File.join("..", "..", "app"), __dir__)
30
32
  end
33
+
34
+ private
35
+
36
+ def development_mode?
37
+ ENV.fetch("RACK_ENV", "development") == "development"
38
+ end
31
39
  end
32
40
  end
33
41
  end
@@ -14,6 +14,7 @@ require_relative "ultra_settings/config_helper"
14
14
  require_relative "ultra_settings/field"
15
15
  require_relative "ultra_settings/rack_app"
16
16
  require_relative "ultra_settings/view_helper"
17
+ require_relative "ultra_settings/render_helper"
17
18
  require_relative "ultra_settings/web_view"
18
19
  require_relative "ultra_settings/application_view"
19
20
  require_relative "ultra_settings/configuration_view"
@@ -259,6 +260,19 @@ module UltraSettings
259
260
  @configurations.keys
260
261
  end
261
262
 
263
+ # Get an array of all of the configuration instances that have been loaded into memory.
264
+ #
265
+ # @return [Array<UltraSettings::Configuration>] The configuration instances.
266
+ # @api private
267
+ def __configurations__
268
+ @configurations.each do |name, class_name|
269
+ __load_config__(name, class_name)
270
+ end
271
+
272
+ config_classes = ObjectSpace.each_object(Class).select { |klass| klass < Configuration }
273
+ config_classes.collect(&:instance)
274
+ end
275
+
262
276
  private
263
277
 
264
278
  # Load a configuration class.
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ultra_settings
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.0
4
+ version: 2.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-09-12 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bundler
@@ -24,7 +23,6 @@ dependencies:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
25
  version: '0'
27
- description:
28
26
  email:
29
27
  - bbdurand@gmail.com
30
28
  executables: []
@@ -36,6 +34,9 @@ files:
36
34
  - MIT-LICENSE.txt
37
35
  - README.md
38
36
  - VERSION
37
+ - app/_config_description.html.erb
38
+ - app/_config_list.html.erb
39
+ - app/_select_menu.html.erb
39
40
  - app/application.css
40
41
  - app/application.js
41
42
  - app/application_vars.css.erb
@@ -53,6 +54,7 @@ files:
53
54
  - lib/ultra_settings/field.rb
54
55
  - lib/ultra_settings/rack_app.rb
55
56
  - lib/ultra_settings/railtie.rb
57
+ - lib/ultra_settings/render_helper.rb
56
58
  - lib/ultra_settings/uninitialized_runtime_settings.rb
57
59
  - lib/ultra_settings/version.rb
58
60
  - lib/ultra_settings/view_helper.rb
@@ -66,7 +68,6 @@ metadata:
66
68
  homepage_uri: https://github.com/bdurand/ultra_settings
67
69
  source_code_uri: https://github.com/bdurand/ultra_settings
68
70
  changelog_uri: https://github.com/bdurand/ultra_settings/blob/main/CHANGELOG.md
69
- post_install_message:
70
71
  rdoc_options: []
71
72
  require_paths:
72
73
  - lib
@@ -81,8 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
82
  - !ruby/object:Gem::Version
82
83
  version: '0'
83
84
  requirements: []
84
- rubygems_version: 3.4.10
85
- signing_key:
85
+ rubygems_version: 3.6.9
86
86
  specification_version: 4
87
87
  summary: UltraSettings is a Ruby gem that provides a flexible and documented approach
88
88
  to managing application configurations from multiple sources, including environment