ultra_settings 2.6.0 → 2.7.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: 4682a4cd5e85642db5e79cfbb156109d6701b5ad09f6744a0b092bdfc3d9277c
4
- data.tar.gz: 8c4a0d4350ce43b7ce4b1c0187bf80c27e2d33261d9bd349ce9155d8fb80a069
3
+ metadata.gz: f70521135220d74fe5ef4fbc6261c9d5b8a93f200f898af0fead2ebb7d31cbfd
4
+ data.tar.gz: 42330a1a294bee7ab53c61f37b56cd0b5b6f309326f1809bfd012cbfcd67fa58
5
5
  SHA512:
6
- metadata.gz: 00c9249324a0c6752dd1449d7b94101050e55aa1d27ba0c47cb3527bb0e8c5909233a64b98a0a0674528acae2413c5578c0097fabe9aaeda6a5b78c47a1854d7
7
- data.tar.gz: a46516c99004973b58496e44c38c375b6698be941aba85fd2e3c0bf08db032c8b9c71ce371aef051bd8649419e5dc6752be0e1f259c2019ce3baa270c8b52460
6
+ metadata.gz: aab2c06121a67e227f1628d1dd3a22590e0bb174f132506840112278a5c09484e57dd6bc176ddbb35d576388c29bb5f6da462473f1c5ebf76724fe8677312ed2
7
+ data.tar.gz: f7e9bd08a2958def30780e0ef966d8b90321d3f9c1bf469677b7c51ab8b512e867bf0e1fc1d910d72f24c9f71818a34050b3a2dbfdc2c5adb9afa4a3e048948a
data/CHANGELOG.md CHANGED
@@ -4,11 +4,24 @@ 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.7.0
8
+
9
+ ### Added
10
+
11
+ - Added new setting to indicate if the runtime settings engine is secure. If the engine is marked as not secure, then runtime settings will be disabled for all fields marked as secret. This protects sensitive information from being exposed in the web UI or through the API. The default value is `true` to maintain backwards compatibility.
12
+
13
+ ## 2.6.1
14
+
15
+ ### Added
16
+
17
+ - Show icons on the web UI that open a dialog with the current value for each data source.
18
+ - Added support for passing the field description in `UltraSettings.runtime_settings_url` using the `${description}` placeholder in the URL.
19
+
7
20
  ## 2.6.0
8
21
 
9
22
  ### Added
10
23
 
11
- - Added support for passing the type in `UltraSettings.runtime_settings_url` as `${type}` in the URL.
24
+ - Added support for passing the type in `UltraSettings.runtime_settings_url` using the `${type}` placeholder in the URL.
12
25
 
13
26
  ### Changed
14
27
 
data/README.md CHANGED
@@ -81,7 +81,6 @@ class MyServiceConfiguration < UltraSettings::Configuration
81
81
  field :auth_token,
82
82
  type: :string,
83
83
  env_var: "MY_SERVICE_TOKEN",
84
- runtime_setting: false,
85
84
  yaml_key: false,
86
85
  description: "Bearer token for accessing the service",
87
86
  secret: true
@@ -114,7 +113,7 @@ You can customize the behavior of each field using various options:
114
113
 
115
114
  - `:default_if` - Provides a condition for when the default should be used. This should be a Proc or the name of a method within the class. Useful for ensuring values meet specific constraints. This can provide protection from misconfiguration that can break the application. In the above example, the default value for `timeout` will be used if the value is less than or equal to 0.
116
115
 
117
- - `:secret` - Marks the field as secret. Secret fields are not displayed in the web UI. By default, all fields are considered secret to avoid accidentally exposing sensitive values. You can change this default behavior by setting `fields_secret_by_default` to `false` either globally or per configuration.
116
+ - `:secret` - Marks the field as secret. Secret fields are not displayed in the web UI. By default, all fields are considered secret to avoid accidentally exposing sensitive values. You can change this default behavior by setting `fields_secret_by_default` to `false` either globally or per configuration. Additionaly, if you set `UltraSettings.runtime_settings_secure` to false, then runtime settings will be disabled on secret fields.
118
117
 
119
118
  - `:env_var` - Overrides the environment variable name used to populate the field. This is useful if the variable name does not follow the conventional pattern. Set this to `false` to disable loading the field from an environment variable.
120
119
 
@@ -172,6 +171,9 @@ UltraSettings.runtime_settings = RedisRuntimeSettings.new
172
171
 
173
172
  The runtime settings implementation may also define an `array` method that takes a single parameter to return an array value. If this method is not implemented, then array values must be returned as single line CSV strings.
174
173
 
174
+ > [!TIP]
175
+ > If your runtime settings implementation does not securely store values, you should set `UltraSettings.runtime_settings_secure` to `false`. This will disable runtime settings on fields marked as secret to prevent leaking sensitive information.
176
+
175
177
  #### Using the `super_settings` gem
176
178
 
177
179
  There is a companion gem [super_settings](https://github.com/bdurand/super_settings) that can be used as a drop in implementation for the runtime settings. You just need to set the runtime settings to the `SuperSettings` object.
@@ -211,7 +213,7 @@ You can customize the behavior of runtime setting names with the following optio
211
213
 
212
214
  - **Disabling Runtime Settings:** You can disable runtime settings as a default source for fields by setting `runtime_settings_disabled` to `true` in your configuration class. You can disable runtime settings on individual fields by setting `runtime_setting` on the field to `false`.
213
215
 
214
- - **Editing Links** You can specify a URL for editing runtime settings from the web UI by setting `UltraSettings.runtime_settings_url` to the desired URL. This will add links to the runtime settings in the web UI. You can use the placeholders `${name}` and `${type}` in the URL which will be replaced with the name and type of the runtime setting, respectively. If you are using the `super_settings` gem for runtime settings, then you can target a setting by adding `#edit=${name}&type=${type}` to the root URL where `super_settings` is mounted.
216
+ - **Editing Links** You can specify a URL for editing runtime settings from the web UI by setting `UltraSettings.runtime_settings_url` to the desired URL. This will add links to the runtime settings in the web UI. You can use the placeholders `${name}`, `${type}`, and `${description}` in the URL which will be replaced with the name, type, and description of the field respectively. If you are using the `super_settings` gem for runtime settings, then you can target a setting by adding `#edit=${name}&type=${type}&description=${description}` to the root URL where `super_settings` is mounted.
215
217
 
216
218
  If a setting value cannot be loaded from the runtime settings, then it's value will attempt to be loaded from a YAML file.
217
219
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.6.0
1
+ 2.7.0
data/app/application.css CHANGED
@@ -6,7 +6,6 @@
6
6
  margin: 0;
7
7
  }
8
8
 
9
- /* Configuration file header */
10
9
  .ultra-settings-config-file {
11
10
  margin-bottom: 1.5rem;
12
11
  padding: 1rem;
@@ -23,7 +22,7 @@
23
22
 
24
23
  .ultra-settings-config-file-path {
25
24
  font-family: monospace;
26
- font-size: 0.9rem;
25
+ font-size: 0.875rem;
27
26
  color: var(--code-color);
28
27
  font-weight: 600;
29
28
  }
@@ -34,14 +33,16 @@
34
33
  margin-left: 0.5rem;
35
34
  }
36
35
 
37
- /* Fields container */
38
36
  .ultra-settings-fields {
39
37
  display: flex;
40
38
  flex-direction: column;
41
- gap: 1rem;
39
+ gap: 1.75rem;
40
+ }
41
+
42
+ .ultra-settings-fields svg {
43
+ vertical-align: middle;
42
44
  }
43
45
 
44
- /* Individual field card */
45
46
  .ultra-settings-field {
46
47
  border: 1px solid var(--field-border-color);
47
48
  border-radius: 0.5rem;
@@ -49,7 +50,6 @@
49
50
  overflow: hidden;
50
51
  }
51
52
 
52
- /* Field header */
53
53
  .ultra-settings-field-header {
54
54
  display: flex;
55
55
  justify-content: space-between;
@@ -80,7 +80,6 @@
80
80
  letter-spacing: 0.025em;
81
81
  }
82
82
 
83
- /* Field badges */
84
83
  .ultra-settings-field-badge {
85
84
  display: inline-block;
86
85
  padding: 0.125rem 0.5rem;
@@ -101,16 +100,15 @@
101
100
  color: var(--static-badge-text-color);
102
101
  }
103
102
 
104
- /* Field value */
105
103
  .ultra-settings-field-value {
106
104
  padding: 1rem;
107
105
  background-color: var(--value-bg-color);
108
106
  border-bottom: 1px solid var(--field-border-color);
109
107
  }
110
108
 
111
- .ultra-settings-field-value code {
109
+ code.ultra-settings-field-data-value {
112
110
  font-family: monospace;
113
- font-size: 0.95rem;
111
+ font-size: 0.875rem;
114
112
  word-break: break-all;
115
113
  color: var(--value-text-color);
116
114
  background-color: var(--value-code-bg-color);
@@ -122,14 +120,11 @@
122
120
  .ultra-settings-nil-value {
123
121
  font-style: italic;
124
122
  color: var(--nil-color);
125
- font-size: 0.9rem;
126
123
  }
127
124
 
128
- /* Field description */
129
125
  .ultra-settings-field-description {
130
126
  padding: 0.75rem 1rem;
131
127
  color: var(--description-color);
132
- font-size: 0.9rem;
133
128
  line-height: 1.5;
134
129
  border-bottom: 1px solid var(--field-border-color);
135
130
  display: flex;
@@ -141,15 +136,6 @@
141
136
  flex: 1;
142
137
  }
143
138
 
144
- .ultra-settings-info-icon {
145
- width: 16px;
146
- height: 16px;
147
- flex-shrink: 0;
148
- margin-top: 0.125rem; /* Align with first line of text based on line-height */
149
- color: var(--description-color);
150
- }
151
-
152
- /* Sources section */
153
139
  .ultra-settings-field-sources {
154
140
  padding: 0.5rem;
155
141
  }
@@ -172,7 +158,7 @@
172
158
  }
173
159
 
174
160
  .ultra-settings-source-type {
175
- font-size: 0.8rem;
161
+ font-size: 0.875rem;
176
162
  font-weight: 500;
177
163
  color: var(--source-type-color);
178
164
  text-transform: uppercase;
@@ -182,8 +168,9 @@
182
168
 
183
169
  .ultra-settings-source-value {
184
170
  font-family: monospace;
185
- font-size: 0.85rem;
171
+ font-size: 0.875rem;
186
172
  color: var(--source-value-color);
173
+ font-weight: 550;
187
174
  flex: 1;
188
175
  margin: 0 0.75rem;
189
176
  word-break: break-all;
@@ -197,6 +184,26 @@
197
184
  letter-spacing: 0.025em;
198
185
  }
199
186
 
187
+ .ultra-settings-source-value-dfn {
188
+ display: inline-block;
189
+ margin-left: 0.25rem;
190
+ }
191
+
192
+ .ultra-settings-icon-info {
193
+ color: var(--info-color);
194
+ cursor: pointer;
195
+ }
196
+
197
+ .ultra-settings-icon-not-set {
198
+ color: var(--warning-color);
199
+ cursor: pointer;
200
+ }
201
+
202
+ .ultra-settings-icon-secret {
203
+ color: var(--disabled-color);
204
+ cursor: pointer;
205
+ }
206
+
200
207
  .ultra-settings-edit-link {
201
208
  color: var(--edit-link-color);
202
209
  text-decoration: none;
@@ -211,11 +218,6 @@
211
218
  opacity: 1;
212
219
  }
213
220
 
214
- .ultra-settings-edit-icon {
215
- width: 16px;
216
- height: 16px;
217
- }
218
-
219
221
  .ultra-settings-select {
220
222
  display: inline-block;
221
223
  padding: .375rem 2.25rem .375rem .75rem;
@@ -236,3 +238,39 @@
236
238
  -moz-appearance: none;
237
239
  appearance: none;
238
240
  }
241
+
242
+ .ultra-settings-dialog {
243
+ min-width: 20rem;
244
+ padding: 0;
245
+ border: 1px solid var(--field-border-color);
246
+ border-radius: 0.375rem;
247
+ }
248
+
249
+ .ultra-settings-dialog-header {
250
+ padding: 0.5rem;
251
+ background-color: var(--field-header-bg-color);
252
+ color: var(--field-header-text-color);
253
+ font-size: 1rem;
254
+ display: flex;
255
+ align-items: top;
256
+ }
257
+
258
+ .ultra-settings-dialog-title {
259
+ flex: 1;
260
+ text-align: center;
261
+ font-weight: 550;
262
+ }
263
+
264
+ .ultra-settings-dialog-close {
265
+ border: none;
266
+ background: none;
267
+ color: var(--field-header-text-color);
268
+ cursor: pointer;
269
+ padding: 0.25rem;
270
+ }
271
+
272
+ .ultra-settings-dialog-body {
273
+ padding: 1rem;
274
+ background-color: var(--background-color);
275
+ color: var(--text-color);
276
+ }
@@ -10,7 +10,8 @@
10
10
  --config-file-border-color: #e8e8e8;
11
11
  --field-bg-color: #ffffff;
12
12
  --field-border-color: #b8b8b8;
13
- --field-header-bg-color: #f8f8f8;
13
+ --field-header-bg-color: #e8e8e8;
14
+ --field-header-text-color: #212529;
14
15
  --value-bg-color: #fdfdfe;
15
16
  --value-text-color: #212529;
16
17
  --value-code-bg-color: #f8f8f8;
@@ -18,7 +19,9 @@
18
19
  --type-color: #6c757d;
19
20
  --description-color: #4a83b5;
20
21
  --nil-color: #6c757d;
21
- --warning-color: #dc3545;
22
+ --warning-color: #b9202f;
23
+ --info-color: #6ea8fe;
24
+ --disabled-color: #adb5bd;
22
25
 
23
26
  /* Source colors */
24
27
  --source-bg-color: #f8f9fa;
@@ -26,7 +29,7 @@
26
29
  --source-active-bg-color: #e7f3ff;
27
30
  --source-active-border-color: #0d6efd;
28
31
  --source-type-color: #666666;
29
- --source-value-color: #666666;
32
+ --source-value-color: #444444;
30
33
  --source-indicator-color: #0d6efd;
31
34
 
32
35
  /* Badge colors */
@@ -48,15 +51,16 @@
48
51
  --form-control-color: #eee;
49
52
  --form-control-bg-color: #666;
50
53
  --form-control-border-color: #555;
51
- --code-color: #ff6b9d;
54
+ --code-color: #fd76a3;
52
55
  --em-color: #adb5bd;
53
56
 
54
57
  /* Card layout colors */
55
- --config-file-bg-color: #2b2b2b;
58
+ --config-file-bg-color: #4b4b4b;
56
59
  --config-file-border-color: #444;
57
60
  --field-bg-color: #1e1e1e;
58
61
  --field-border-color: #444;
59
62
  --field-header-bg-color: #2b2b2b;
63
+ --field-header-text-color: #ced4da;
60
64
  --value-bg-color: #252525;
61
65
  --value-text-color: #e9ecef;
62
66
  --value-code-bg-color: #2b2b2b;
@@ -65,6 +69,8 @@
65
69
  --description-color: #a9d9f8;
66
70
  --nil-color: #cdcecfff;
67
71
  --warning-color: #dc3545;
72
+ --info-color: #6ea8fe;
73
+ --disabled-color: #adb5bd;
68
74
 
69
75
  /* Source colors */
70
76
  --source-bg-color: #2b2b2b;
@@ -32,17 +32,15 @@
32
32
  <% if configuration[field.name].nil? %>
33
33
  <span class="ultra-settings-nil-value">nil</span>
34
34
  <% elsif field.secret? %>
35
- <code><%= html_escape(secret_value(configuration[field.name])) %></code>
35
+ <code class="ultra-settings-field-data-value"><%= html_escape(secret_value(configuration[field.name])) %></code>
36
36
  <% else %>
37
- <code><%= html_escape(display_value(configuration[field.name])) %></code>
37
+ <code class="ultra-settings-field-data-value"><%= html_escape(display_value(configuration[field.name])) %></code>
38
38
  <% end %>
39
39
  </div>
40
40
 
41
41
  <% unless field.description.to_s.empty? %>
42
42
  <div class="ultra-settings-field-description">
43
- <svg class="ultra-settings-info-icon" width="16" height="16" fill="currentColor">
44
- <use href="#info-icon"/>
45
- </svg>
43
+ <%= info_icon %>
46
44
  <div class="ultra-settings-description-text">
47
45
  <%= html_escape(field.description) %>
48
46
  </div>
@@ -52,8 +50,13 @@
52
50
  <div class="ultra-settings-field-sources">
53
51
  <% if field.env_var && !configuration.class.environment_variables_disabled? %>
54
52
  <div class="ultra-settings-source <%= 'ultra-settings-source-active' if source == :env %>">
55
- <span class="ultra-settings-source-type">Environment Variable</span>
56
- <code class="ultra-settings-source-value"><%= show_defined_value(field.env_var, configuration.__value_from_source__(field.name, :env), field.secret?) %></code>
53
+ <span class="ultra-settings-source-type">
54
+ Environment Variable
55
+ </span>
56
+ <code class="ultra-settings-source-value">
57
+ <%= field.env_var %>
58
+ <%= show_defined_value(field.env_var, configuration.__value_from_source__(field.name, :env), field.secret?) %>
59
+ </code>
57
60
  <% if source == :env %>
58
61
  <span class="ultra-settings-source-indicator">Currently active</span>
59
62
  <% end %>
@@ -63,16 +66,17 @@
63
66
  <% if field.runtime_setting && !configuration.class.runtime_settings_disabled? %>
64
67
  <div class="ultra-settings-source <%= 'ultra-settings-source-active' if source == :settings %>">
65
68
  <span class="ultra-settings-source-type">Runtime Setting</span>
66
- <code class="ultra-settings-source-value"><%= show_defined_value(field.runtime_setting, configuration.__value_from_source__(field.name, :settings), field.secret?) %></code>
69
+ <code class="ultra-settings-source-value">
70
+ <%= field.runtime_setting %>
71
+ <%= show_defined_value(field.runtime_setting, configuration.__value_from_source__(field.name, :settings), field.secret?) %>
72
+ </code>
67
73
  <% if source == :settings %>
68
74
  <span class="ultra-settings-source-indicator">Currently active</span>
69
75
  <% end %>
70
- <% edit_url = UltraSettings.runtime_settings_url(field.runtime_setting, field.type) %>
76
+ <% edit_url = UltraSettings.runtime_settings_url(name: field.runtime_setting, type: field.type, description: field.description) %>
71
77
  <% if edit_url %>
72
78
  <a href="<%= html_escape(edit_url) %>" class="ultra-settings-edit-link" title="Edit <%= html_escape(field.runtime_setting) %>">
73
- <svg class="ultra-settings-edit-icon" width="16" height="16" fill="currentColor">
74
- <use href="#edit-icon"/>
75
- </svg>
79
+ <%= edit_icon %>
76
80
  </a>
77
81
  <% end %>
78
82
  </div>
@@ -81,25 +85,22 @@
81
85
  <% if field.yaml_key && !configuration.class.yaml_config_disabled? %>
82
86
  <div class="ultra-settings-source <%= 'ultra-settings-source-active' if source == :yaml %>">
83
87
  <span class="ultra-settings-source-type">Configuration File</span>
84
- <code class="ultra-settings-source-value"><%= show_defined_value(field.yaml_key, configuration.__value_from_source__(field.name, :yaml), field.secret?) %></code>
88
+ <code class="ultra-settings-source-value">
89
+ <%= field.yaml_key %>
90
+ <%= show_defined_value(field.yaml_key, configuration.__value_from_source__(field.name, :yaml), field.secret?) %>
91
+ </code>
85
92
  <% if source == :yaml %>
86
93
  <span class="ultra-settings-source-indicator">Currently active</span>
87
94
  <% end %>
88
95
  </div>
89
96
  <% end %>
90
97
 
91
- <% if field.default.nil? %>
92
- <% if source == :default %>
93
- <div class="ultra-settings-source ultra-settings-source-active">
94
- <span class="ultra-settings-source-type">Default Value</span>
95
- <span class="ultra-settings-source-value"><em>Not set</em></span>
96
- <span class="ultra-settings-source-indicator">Currently active</span>
97
- </div>
98
- <% end %>
99
- <% else %>
98
+ <% if !field.default.nil? || source == :default %>
100
99
  <div class="ultra-settings-source <%= 'ultra-settings-source-active' if source == :default %>">
101
100
  <span class="ultra-settings-source-type">Default Value</span>
102
- <code class="ultra-settings-source-value"><%= show_defined_value("", field.default, field.secret?) %></code>
101
+ <code class="ultra-settings-source-value">
102
+ <%= show_defined_value("Default Value", field.default, field.secret?) %>
103
+ </code>
103
104
  <% if source == :default %>
104
105
  <span class="ultra-settings-source-indicator">Currently active</span>
105
106
  <% end %>
@@ -109,4 +110,16 @@
109
110
  </div>
110
111
  <% end %>
111
112
  </div>
113
+
114
+ <dialog class="ultra-settings-dialog" closedby="any">
115
+ <div class="ultra-settings-dialog-header">
116
+ <div class="ultra-settings-dialog-title"></div>
117
+ <button class="ultra-settings-dialog-close" onclick="this.closest('.ultra-settings-dialog').close();">
118
+ <%= close_icon %>
119
+ </button>
120
+ </div>
121
+ <div class="ultra-settings-dialog-body">
122
+ <code class="ultra-settings-field-data-value ultra-settings-dialog-value"></code>
123
+ </div>
124
+ </dialog>
112
125
  </div>
data/app/index.html.erb CHANGED
@@ -1,19 +1,5 @@
1
1
  <%= style_tag %>
2
2
 
3
- <!-- SVG Icons -->
4
- <svg style="display: none;" xmlns="http://www.w3.org/2000/svg">
5
- <defs>
6
- <symbol id="edit-icon" viewBox="0 0 16 16">
7
- <path d="M15.502 1.94a.5.5 0 0 1 0 .706L14.459 3.69l-2-2L13.502.646a.5.5 0 0 1 .707 0l1.293 1.293zm-1.75 2.456-2-2L4.939 9.21a.5.5 0 0 0-.121.196l-.805 2.414a.25.25 0 0 0 .316.316l2.414-.805a.5.5 0 0 0 .196-.12l6.813-6.814z"/>
8
- <path fill-rule="evenodd" d="M1 13.5A1.5 1.5 0 0 0 2.5 15h11a1.5 1.5 0 0 0 1.5-1.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5H9a.5.5 0 0 0 0-1H2.5A1.5 1.5 0 0 0 1 2.5z"/>
9
- </symbol>
10
- <symbol id="info-icon" viewBox="0 0 16 16">
11
- <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
12
- <path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
13
- </symbol>
14
- </defs>
15
- </svg>
16
-
17
3
  <div class="ultra-settings">
18
4
  <div class="ultra-settings-nav">
19
5
  <form onsubmit="return false" style="margin-bottom: 0.5rem;">
@@ -15,23 +15,37 @@ module UltraSettings
15
15
  class ApplicationView
16
16
  attr_reader :css
17
17
 
18
+ # Initialize the application view with a color scheme.
19
+ #
20
+ # @param color_scheme [Symbol] The color scheme to use (:light, :dark, or :system).
18
21
  def initialize(color_scheme: :light)
19
22
  @css = application_css(color_scheme)
20
23
  @css = @css.html_safe if @css.respond_to?(:html_safe)
21
24
  end
22
25
 
26
+ # Render the HTML for the configuration settings UI.
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).
30
+ # @return [String] The rendered HTML.
23
31
  def render(select_class: "ultra-settings-select", table_class: "")
24
32
  html = ViewHelper.erb_template("index.html.erb").result(binding)
25
33
  html = html.html_safe if html.respond_to?(:html_safe)
26
34
  html
27
35
  end
28
36
 
37
+ # Generate an HTML style tag with the CSS for the view.
38
+ #
39
+ # @return [String] The HTML style tag with CSS.
29
40
  def style_tag
30
41
  tag = "<style type=\"text/css\">\n#{css}\n</style>"
31
42
  tag = tag.html_safe if tag.respond_to?(:html_safe)
32
43
  tag
33
44
  end
34
45
 
46
+ # Convert the view to a string by rendering it.
47
+ #
48
+ # @return [String] The rendered HTML.
35
49
  def to_s
36
50
  render
37
51
  end
@@ -93,11 +93,13 @@ module UltraSettings
93
93
  time
94
94
  end
95
95
 
96
+ # @param value [Object] The value to check.
96
97
  # @return [Boolean] true if the value is a numeric type or a string representing a number.
97
98
  def numeric?(value)
98
99
  value.is_a?(Numeric) || (value.is_a?(String) && value.to_s.match?(NUMERIC_REGEX))
99
100
  end
100
101
 
102
+ # @param value [Object] The value to check.
101
103
  # @return [Boolean] true if the value is nil or empty.
102
104
  def blank?(value)
103
105
  return true if value.nil?
@@ -109,6 +111,7 @@ module UltraSettings
109
111
  end
110
112
  end
111
113
 
114
+ # @param value [Object] The value to check.
112
115
  # @return [Boolean] true if the value is not nil and not empty.
113
116
  def present?(value)
114
117
  !blank?(value)
@@ -1,16 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module UltraSettings
4
- # Helper module for setting up a class to use the config methods
4
+ # Helper module for setting up a class to use the config methods.
5
5
  #
6
- # Usage:
7
- # class TestClass
8
- # extend UltraSettings::ConfigHelper
9
- # configuration_class TestConfiguration
10
- # end
11
- # TestClass.config => TestConfiguration.instance
12
- # TestClass.new.config => TestConfiguration.instance
6
+ # @example
7
+ # class TestClass
8
+ # extend UltraSettings::ConfigHelper
9
+ # configuration_class TestConfiguration
10
+ # end
11
+ # TestClass.config # => TestConfiguration.instance
12
+ # TestClass.new.config # => TestConfiguration.instance
13
13
  module ConfigHelper
14
+ # Define the configuration class and create config methods.
15
+ #
16
+ # @param config_class [Class] The configuration class to use.
17
+ # @return [void]
14
18
  def configuration_class(config_class)
15
19
  define_singleton_method :config do
16
20
  config_class.instance
@@ -61,7 +61,7 @@ module UltraSettings
61
61
  default: default,
62
62
  default_if: default_if,
63
63
  env_var: construct_env_var(name, env_var),
64
- runtime_setting: (static ? nil : construct_runtime_setting(name, runtime_setting)),
64
+ runtime_setting: construct_runtime_setting(name, runtime_setting),
65
65
  yaml_key: construct_yaml_key(name, yaml_key),
66
66
  static: static,
67
67
  secret: secret
@@ -11,10 +11,17 @@ module UltraSettings
11
11
  # <h1>Service Configuration</h1>
12
12
  # <%= UltraSettings::ConfigurationView.new(ServiceConfiguration.instance).render %>
13
13
  class ConfigurationView
14
+ # Initialize the configuration view with a configuration instance.
15
+ #
16
+ # @param configuration [UltraSettings::Configuration] The configuration instance to display.
14
17
  def initialize(configuration)
15
18
  @configuration = configuration
16
19
  end
17
20
 
21
+ # Render the HTML for the configuration view.
22
+ #
23
+ # @param table_class [String] CSS class for the table element (maintained for backwards compatibility).
24
+ # @return [String] The rendered HTML.
18
25
  def render(table_class: "")
19
26
  configuration = @configuration
20
27
  html = ViewHelper.erb_template("configuration.html.erb").result(binding)
@@ -22,6 +29,9 @@ module UltraSettings
22
29
  html
23
30
  end
24
31
 
32
+ # Convert the view to a string by rendering it.
33
+ #
34
+ # @return [String] The rendered HTML.
25
35
  def to_s
26
36
  render
27
37
  end
@@ -42,14 +52,29 @@ module UltraSettings
42
52
  end
43
53
 
44
54
  def show_defined_value(label, value, secret)
45
- title = if value.nil?
46
- "Not set"
55
+ val = nil
56
+ icon = nil
57
+ css_class = nil
58
+
59
+ if value.nil?
60
+ val = "Not set"
61
+ icon = not_set_icon
62
+ css_class = "ultra-settings-icon-not-set"
47
63
  elsif secret
48
- "Secret value"
64
+ val = secret_value(value)
65
+ icon = lock_icon
66
+ css_class = "ultra-settings-icon-secret"
49
67
  else
50
- "Value: #{display_value(value)}"
68
+ val = display_value(value)
69
+ icon = eye_icon
70
+ css_class = "ultra-settings-icon-info"
51
71
  end
52
- "<dfn style=\"text-decoration: underline dotted;\" title=\"#{html_escape(title)}\">#{html_escape(label)}</dfn>"
72
+
73
+ <<~HTML
74
+ <dfn class="#{css_class}" title="#{html_escape(val)}" onclick="#{html_escape(open_dialog_script)}" data-label="#{html_escape(label)}">
75
+ #{icon}
76
+ </dfn>
77
+ HTML
53
78
  end
54
79
 
55
80
  def secret_value(value)
@@ -68,5 +93,69 @@ module UltraSettings
68
93
  end
69
94
  path.relative_path_from(root_path)
70
95
  end
96
+
97
+ def info_icon(size = 16)
98
+ <<~HTML
99
+ <svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
100
+ <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
101
+ <path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
102
+ </svg>
103
+ HTML
104
+ end
105
+
106
+ def not_set_icon(size = 16)
107
+ <<~HTML
108
+ <svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
109
+ <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
110
+ <path d="M11.354 4.646a.5.5 0 0 0-.708 0l-6 6a.5.5 0 0 0 .708.708l6-6a.5.5 0 0 0 0-.708"/>
111
+ </svg>
112
+ HTML
113
+ end
114
+
115
+ def lock_icon(size = 16)
116
+ <<~HTML
117
+ <svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
118
+ <path fill-rule="evenodd" d="M8 0a4 4 0 0 1 4 4v2.05a2.5 2.5 0 0 1 2 2.45v5a2.5 2.5 0 0 1-2.5 2.5h-7A2.5 2.5 0 0 1 2 13.5v-5a2.5 2.5 0 0 1 2-2.45V4a4 4 0 0 1 4-4m0 1a3 3 0 0 0-3 3v2h6V4a3 3 0 0 0-3-3"/>
119
+ </svg>
120
+ HTML
121
+ end
122
+
123
+ def edit_icon(size = 16)
124
+ <<~HTML
125
+ <svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
126
+ <path d="M15.502 1.94a.5.5 0 0 1 0 .706L14.459 3.69l-2-2L13.502.646a.5.5 0 0 1 .707 0l1.293 1.293zm-1.75 2.456-2-2L4.939 9.21a.5.5 0 0 0-.121.196l-.805 2.414a.25.25 0 0 0 .316.316l2.414-.805a.5.5 0 0 0 .196-.12l6.813-6.814z"/>
127
+ <path fill-rule="evenodd" d="M1 13.5A1.5 1.5 0 0 0 2.5 15h11a1.5 1.5 0 0 0 1.5-1.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5H9a.5.5 0 0 0 0-1H2.5A1.5 1.5 0 0 0 1 2.5z"/>
128
+ </svg>
129
+ HTML
130
+ end
131
+
132
+ def eye_icon(size = 16)
133
+ <<~HTML
134
+ <svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
135
+ <path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8M1.173 8a13 13 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5s3.879 1.168 5.168 2.457A13 13 0 0 1 14.828 8q-.086.13-.195.288c-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5s-3.879-1.168-5.168-2.457A13 13 0 0 1 1.172 8z"/>
136
+ <path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5M4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0"/>
137
+ </svg>
138
+ HTML
139
+ end
140
+
141
+ def close_icon(size = 16)
142
+ <<~HTML
143
+ <svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
144
+ <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
145
+ <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708"/>
146
+ </svg>
147
+ HTML
148
+ end
149
+
150
+ private
151
+
152
+ def open_dialog_script
153
+ <<~JAVASCRIPT.gsub(/\s+/, " ").tr('"', "'")
154
+ this.closest('.ultra-settings-configuration').querySelector('.ultra-settings-dialog-title').textContent = this.dataset.label;
155
+ this.closest('.ultra-settings-configuration').querySelector('.ultra-settings-dialog-value').textContent = this.title;
156
+ this.closest('.ultra-settings-configuration').querySelector('.ultra-settings-dialog').showModal();
157
+ this.closest('.ultra-settings-configuration').querySelector('.ultra-settings-dialog-close').blur();
158
+ JAVASCRIPT
159
+ end
71
160
  end
72
161
  end
@@ -118,6 +118,7 @@ module UltraSettings
118
118
  end
119
119
 
120
120
  def runtime_setting_value(settings)
121
+ return nil if static? || (secret? && !UltraSettings.runtime_settings_secure?)
121
122
  return nil unless settings && runtime_setting
122
123
 
123
124
  if type == :array && settings.respond_to?(:array)
@@ -5,11 +5,18 @@ module UltraSettings
5
5
  # No setting values are displayed, but you should still add some
6
6
  # sort of authentication if you want to use this in production.
7
7
  class RackApp
8
+ # Initialize a new Rack application for displaying settings.
9
+ #
10
+ # @param color_scheme [Symbol, nil] The color scheme to use in the UI (:light, :dark, or :system).
8
11
  def initialize(color_scheme: nil)
9
12
  @webview = nil
10
13
  @color_scheme = color_scheme
11
14
  end
12
15
 
16
+ # Handle Rack requests and return the settings HTML page.
17
+ #
18
+ # @param env [Hash] The Rack environment.
19
+ # @return [Array] A Rack response array [status, headers, body].
13
20
  def call(env)
14
21
  [200, {"content-type" => "text/html; charset=utf8"}, [webview.render_settings]]
15
22
  end
@@ -3,26 +3,31 @@
3
3
  module UltraSettings
4
4
  # This class is used to represent runtime settings that have not been initialized yet.
5
5
  # You can use this to protect your application from accidentally accessing runtime settings
6
- # before they are initialized. Doing this can cquse unexpected behavior if the runtime settings
7
- # engine has not yet been initialized. For instance, if your runtime settings enging reads from
6
+ # before they are initialized. Doing this can cause unexpected behavior if the runtime settings
7
+ # engine has not yet been initialized. For instance, if your runtime settings engine reads from
8
8
  # a database it would not be available until the database connection is established.
9
9
  #
10
- # The intention of this class is to set it a the runtime settings at the beginning of initialization
10
+ # The intention of this class is to set it as the runtime settings at the beginning of initialization
11
11
  # and then set the actual runtime settings engine after the initialization is complete. It will
12
12
  # act as a guard to prevent invalid runtime settings backed configurations from being used during
13
13
  # initialization.
14
14
  #
15
15
  # @example
16
16
  #
17
- # UltraSettings.runtime_settings = UltraSettings::UninitializedRuntimeSettings
18
- # ActiveSupport.on_load(:active_record) do
19
- # UltraSettings.runtime_settings = SuperSettings
20
- # end
17
+ # UltraSettings.runtime_settings = UltraSettings::UninitializedRuntimeSettings
18
+ # ActiveSupport.on_load(:active_record) do
19
+ # UltraSettings.runtime_settings = SuperSettings
20
+ # end
21
21
  class UninitializedRuntimeSettings
22
22
  class Error < StandardError
23
23
  end
24
24
 
25
25
  class << self
26
+ # Raises an error when attempting to access runtime settings during initialization.
27
+ #
28
+ # @param key [String] The key being accessed.
29
+ # @return [void]
30
+ # @raise [Error] Always raises an error to prevent access during initialization.
26
31
  def [](key)
27
32
  raise Error.new("Attempt to call runtime setting #{key} during initialization")
28
33
  end
@@ -6,14 +6,25 @@ module UltraSettings
6
6
  @cache = {}
7
7
 
8
8
  class << self
9
+ # Get an ERB template for rendering.
10
+ #
11
+ # @param path [String] The path to the template file.
12
+ # @return [ERB] The compiled ERB template.
9
13
  def erb_template(path)
10
14
  @cache["erb:#{path}"] ||= ERB.new(read_app_file(path))
11
15
  end
12
16
 
17
+ # Read a file from the app directory.
18
+ #
19
+ # @param path [String] The path to the file relative to the app directory.
20
+ # @return [String] The contents of the file.
13
21
  def read_app_file(path)
14
22
  @cache["file:#{path}"] ||= File.read(File.join(app_dir, path))
15
23
  end
16
24
 
25
+ # Get the app directory path.
26
+ #
27
+ # @return [String] The absolute path to the app directory.
17
28
  def app_dir
18
29
  File.expand_path(File.join("..", "..", "app"), __dir__)
19
30
  end
@@ -5,18 +5,26 @@ module UltraSettings
5
5
  class WebView
6
6
  attr_reader :layout_css
7
7
 
8
+ # Initialize a new WebView with the specified color scheme.
9
+ #
8
10
  # @param color_scheme [Symbol] The color scheme to use in the UI. This can be `:light`,
9
- # `:dark`, or `:system`. The default is `:light`.
11
+ # `:dark`, or `:system`. The default is `:light`.
10
12
  def initialize(color_scheme: :light)
11
13
  @color_scheme = (color_scheme || :light).to_sym
12
14
  @layout_template = ViewHelper.erb_template("layout.html.erb")
13
15
  @layout_css = scheme_layout_css(@color_scheme)
14
16
  end
15
17
 
18
+ # Render the complete settings page HTML.
19
+ #
20
+ # @return [String] The rendered HTML page.
16
21
  def render_settings
17
22
  @layout_template.result(binding)
18
23
  end
19
24
 
25
+ # Get the content for the settings page.
26
+ #
27
+ # @return [String] The HTML content for the settings.
20
28
  def content
21
29
  UltraSettings::ApplicationView.new(color_scheme: @color_scheme).render
22
30
  end
@@ -33,11 +33,18 @@ module UltraSettings
33
33
  # In addition, the keys are flattened into a one level deep hash with dots
34
34
  # separating the keys.
35
35
  class YamlConfig
36
+ # Initialize a YAML configuration loader.
37
+ #
38
+ # @param path [String, Pathname] The path to the YAML configuration file.
39
+ # @param environment [String] The environment section to load from the YAML file.
36
40
  def initialize(path, environment)
37
41
  yaml = load_yaml(path)
38
42
  @config = environment_config(yaml, environment)
39
43
  end
40
44
 
45
+ # Convert the loaded configuration to a hash.
46
+ #
47
+ # @return [Hash] The flattened configuration hash.
41
48
  def to_h
42
49
  @config
43
50
  end
@@ -38,6 +38,7 @@ module UltraSettings
38
38
  @mutex = Mutex.new
39
39
  @runtime_settings = nil
40
40
  @runtime_settings_url = nil
41
+ @runtime_settings_secure = true
41
42
 
42
43
  class << self
43
44
  # Adds a configuration to the root namespace. The configuration will be
@@ -111,6 +112,7 @@ module UltraSettings
111
112
  # Defaults to "development".
112
113
  #
113
114
  # @param value [String] The environment name to use.
115
+ # @return [void]
114
116
  def yaml_config_env=(value)
115
117
  Configuration.yaml_config_env = value
116
118
  end
@@ -128,6 +130,7 @@ module UltraSettings
128
130
  # this is a period.
129
131
  #
130
132
  # @param value [String] The delimiter to use.
133
+ # @return [void]
131
134
  def runtime_setting_delimiter=(value)
132
135
  Configuration.runtime_setting_delimiter = value.to_s
133
136
  end
@@ -162,6 +165,9 @@ module UltraSettings
162
165
  # Set the object to use for runtime settings. This can be any object that
163
166
  # responds to the [] method. If you are using the `super_settings` gem,
164
167
  # you can set this to `SuperSettings`.
168
+ #
169
+ # @param value [#[]] The object to use for runtime settings.
170
+ # @return [void]
165
171
  attr_writer :runtime_settings
166
172
 
167
173
  # Get the object to use for runtime settings.
@@ -176,21 +182,46 @@ module UltraSettings
176
182
  # URL will be displayed in the web view for fields that support runtime settings.
177
183
  # The URL may contain a `${name}` placeholder that will be replaced with the name
178
184
  # of the setting.
185
+ #
186
+ # @param value [String] The URL for changing runtime settings.
187
+ # @return [void]
179
188
  attr_writer :runtime_settings_url
180
189
 
181
190
  # Get the URL for changing runtime settings.
182
191
  #
183
192
  # @param name [String] The name of the setting.
193
+ # @param type [String] The type of the setting.
194
+ # @param description [String] The description of the setting.
184
195
  # @return [String, nil]
185
196
  # @api private
186
- def runtime_settings_url(name, type)
197
+ def runtime_settings_url(name: nil, type: nil, description: nil)
187
198
  url = @runtime_settings_url.to_s
188
199
  return nil if url.empty?
189
200
 
190
- url = url.gsub("${name}", URI.encode_www_form_component(name.to_s))
191
- url.gsub("${type}", URI.encode_www_form_component(type.to_s))
201
+ url.gsub("${name}", URI.encode_www_form_component(name.to_s))
202
+ .gsub("${type}", URI.encode_www_form_component(type.to_s))
203
+ .gsub("${description}", URI.encode_www_form_component(description.to_s))
204
+ end
205
+
206
+ # Set whether or not the runtime settings engine is considered secure. If this is set
207
+ # to false, then runtime settings will be disabled for all fields marked as secret.
208
+ # The default value is true.
209
+ #
210
+ # @param value [Boolean] Whether the runtime settings engine is secure.
211
+ # @return [void]
212
+ attr_writer :runtime_settings_secure
213
+
214
+ # Check if the runtime settings engine is considered secure.
215
+ #
216
+ # @return [Boolean]
217
+ def runtime_settings_secure?
218
+ @runtime_settings_secure
192
219
  end
193
220
 
221
+ # Set whether fields should be considered secret by default.
222
+ #
223
+ # @param value [Boolean] Whether fields should be secret by default.
224
+ # @return [void]
194
225
  def fields_secret_by_default=(value)
195
226
  Configuration.fields_secret_by_default = value
196
227
  end
@@ -245,6 +276,7 @@ module UltraSettings
245
276
  unless klass < Configuration
246
277
  raise TypeError.new("Configuration class #{class_name} does not inherit from UltraSettings::Configuration")
247
278
  end
279
+
248
280
  @configurations[name] = klass
249
281
  end
250
282
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ultra_settings
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.0
4
+ version: 2.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-22 00:00:00.000000000 Z
11
+ date: 2025-09-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler