ultra_settings 2.5.0 → 2.6.1

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: b07db0c8343574b5e9356dfed5e8752596df84920aa3136945e4fbf9d26c88b7
4
- data.tar.gz: 8d8b1a0d5134d9eb7e6d16a873a1c98d506a17e1e458cca704f6bf0f42a75dfb
3
+ metadata.gz: ec11cc8661b191dbb102deec0798f47ef11495feb8675f2f5b83e3e5d752855f
4
+ data.tar.gz: e3f729687eaad31be948aa8f7b4818f25fb842cec8261058aa1a9f62616639c6
5
5
  SHA512:
6
- metadata.gz: 29414f841f3fb1d365bff0c590b9bbc070e46363aff9efeba31d7d92ff7b5992787d414e923e2b7aca9b767e6e61d9db53b9baccb990779ab0eeb67485d422fd
7
- data.tar.gz: 646aa68cfa787a46a447b78f82b4ecbc37c4cf841a2217c426a2610464db735f33e1e5dbdd7441c1e2e9ee329eda194ec875bac726ed8df01ca37eaac9d4d91b
6
+ metadata.gz: 455632dc476fc7ed73a70d784d97de612db55c1c2650e31b0896727f9747c8e4ddc3a2bfcdf9c2b190180c4f3d157aca427b75abf68c6e5f312b1251d9f24fc5
7
+ data.tar.gz: 83e7686584d1ea62747d693c17f9f43908d70a5f5b45541d9f129b2e33e92521b0ce0c87d4c8ec10e3416afd186fefee93f281a6bfa0871c8b56e741d1fc44f6
data/ARCHITECTURE.md ADDED
@@ -0,0 +1,186 @@
1
+ # UltraSettings Configuration Architecture
2
+
3
+ This document describes the architecture of the UltraSettings Configuration and Field classes, which form the core of the configuration management system.
4
+
5
+ ## Overview
6
+
7
+ The UltraSettings library provides a flexible configuration management system built around two primary classes:
8
+
9
+ - **Configuration**: A singleton-based configuration manager that supports multiple data sources
10
+ - **Field**: A field definition that encapsulates metadata and value resolution logic
11
+
12
+ ## Architecture Diagram
13
+
14
+ ```mermaid
15
+ classDiagram
16
+ class Configuration {
17
+ <<Singleton>>
18
+ +ALLOWED_NAME_PATTERN : Regex
19
+ +ALLOWED_TYPES : Array~Symbol~
20
+ -@env_var_prefix : String
21
+ -@runtime_setting_prefix : String
22
+ -@ultra_settings_mutex : Mutex
23
+ -@ultra_settings_memoized_values : Hash
24
+ -@ultra_settings_override_values : Hash
25
+ -@ultra_settings_yaml_config : Hash
26
+
27
+ +field(name, **options) void
28
+ +fields() Array~Field~
29
+ +include_field?(name) Boolean
30
+ +env_var_prefix=(value) void
31
+ +env_var_prefix() String
32
+ +runtime_setting_prefix=(value) void
33
+ +runtime_setting_prefix() String
34
+ +configuration_file=(value) void
35
+ +configuration_file() Pathname
36
+ +override!(values, &block) Object
37
+ +load_yaml_config() Hash
38
+ +[](name) Object
39
+ +include?(name) Boolean
40
+ +__source__(name) Symbol
41
+ +__value_from_source__(name, source) Object
42
+ +__to_hash__() Hash
43
+ -__get_value__(name) Object
44
+ -__use_default?(value, default_if) Boolean
45
+ -__yaml_config__() Hash
46
+ -defined_fields() Hash~String, Field~
47
+ -root_name() String
48
+ -construct_env_var(name, env_var) String
49
+ -construct_runtime_setting(name, runtime_setting) String
50
+ -construct_yaml_key(name, yaml_key) String
51
+ }
52
+
53
+ class Field {
54
+ +name : String
55
+ +type : Symbol
56
+ +description : String
57
+ +default : Object
58
+ +default_if : Proc|Symbol
59
+ +env_var : String
60
+ +runtime_setting : String
61
+ +yaml_key : String
62
+ -@static : Boolean
63
+ -@secret : Boolean|Proc
64
+
65
+ +initialize(**options) void
66
+ +value(env, settings, yaml_config) Object
67
+ +source(env, settings, yaml_config) Symbol
68
+ +coerce(value) Object
69
+ +static?() Boolean
70
+ +secret?() Boolean
71
+ -fetch_value_and_source(**sources) Array
72
+ -runtime_setting_value(settings) Object
73
+ -yaml_value(yaml_config) Object
74
+ }
75
+
76
+ class Coerce {
77
+ <<Utility>>
78
+ +coerce_value(value, type) Object
79
+ }
80
+
81
+ Configuration "1" --> "*" Field : manages
82
+ Field --> Coerce : uses
83
+ Configuration --> Field : creates via field()
84
+
85
+ note for Configuration "Singleton pattern ensures single instance per class.\nSupports inheritance for configuration hierarchies.\nThread-safe with mutex protection."
86
+
87
+ note for Field "Immutable once created.\nSupports multiple data sources with precedence:\n1. Environment Variables\n2. Runtime Settings\n3. YAML Configuration\n4. Default Values"
88
+ ```
89
+
90
+ ## Key Components
91
+
92
+ ### Configuration Class
93
+
94
+ The `Configuration` class serves as the primary interface for defining and accessing configuration values. Key characteristics:
95
+
96
+ #### Singleton Pattern
97
+ - Uses Ruby's `Singleton` module to ensure one instance per configuration class
98
+ - Supports inheritance, allowing configuration subclasses to have their own singleton instances
99
+
100
+ #### Field Definition
101
+ - **`field(name, **options)`**: DSL method for defining configuration fields
102
+ - Dynamically creates getter methods using `class_eval`
103
+ - Supports various field types: `:string`, `:symbol`, `:integer`, `:float`, `:boolean`, `:datetime`, `:array`
104
+ - Validates field names against `ALLOWED_NAME_PATTERN`
105
+
106
+ #### Multi-Source Value Resolution
107
+ Fields can pull values from multiple sources in order of precedence:
108
+ 1. **Environment Variables** - Highest priority
109
+ 2. **Runtime Settings** - Dynamic configuration
110
+ 3. **YAML Files** - File-based configuration
111
+ 4. **Default Values** - Fallback values
112
+
113
+ #### Thread Safety
114
+ - Uses `Mutex` for thread-safe operations
115
+ - Protects memoized values and override values from race conditions
116
+
117
+ #### Value Overrides
118
+ - **`override!(values, &block)`**: Temporarily override field values within a block
119
+ - Thread-local overrides allow different values per thread
120
+
121
+ ### Field Class
122
+
123
+ The `Field` class encapsulates the metadata and behavior for individual configuration fields:
124
+
125
+ #### Immutable Design
126
+ - All attributes are frozen upon initialization
127
+ - Ensures consistent behavior throughout the application lifecycle
128
+
129
+ #### Value Resolution Logic
130
+ - **`value(env:, settings:, yaml_config:)`**: Resolves field value from available sources
131
+ - **`source(env:, settings:, yaml_config:)`**: Identifies which source provided the value
132
+ - **`fetch_value_and_source()`**: Core resolution algorithm with source precedence
133
+
134
+ #### Type Coercion
135
+ - **`coerce(value)`**: Converts string values to appropriate types
136
+ - Delegates to `Coerce` utility class for type-specific conversion logic
137
+
138
+ #### Security Features
139
+ - **`secret?`**: Marks fields containing sensitive data
140
+ - Supports both boolean and callable (lambda) secret detection
141
+ - Secret fields are masked in serialization methods
142
+
143
+ #### Special Behaviors
144
+ - **Static Fields**: Values cached and never change after first resolution
145
+ - **Default Conditions**: Custom logic for when to use default values via `default_if`
146
+
147
+ ## Data Flow
148
+
149
+ 1. **Field Definition**: Developer calls `Configuration.field()` to define a configuration field
150
+ 2. **Field Creation**: A new `Field` instance is created and stored in the configuration class
151
+ 3. **Dynamic Method**: A getter method is dynamically created using `class_eval`
152
+ 4. **Value Access**: When the getter is called, it delegates to `__get_value__`
153
+ 5. **Source Resolution**: The field's `value()` method checks sources in precedence order
154
+ 6. **Type Coercion**: Raw string values are coerced to the appropriate type
155
+ 7. **Caching**: Static fields are memoized; others may use override values
156
+
157
+ ## Key Design Patterns
158
+
159
+ ### Factory Pattern
160
+ The `field()` method acts as a factory for creating `Field` instances with appropriate defaults and validations.
161
+
162
+ ### Strategy Pattern
163
+ Different data sources (environment, runtime settings, YAML) are handled through a consistent interface but with source-specific logic.
164
+
165
+ ### Template Method
166
+ The value resolution follows a template where the algorithm is defined in `Field#fetch_value_and_source`, but source-specific retrieval is delegated to private methods.
167
+
168
+ ### Decorator Pattern
169
+ Field overrides decorate the normal value resolution with temporary alternative values.
170
+
171
+ ## Inheritance Support
172
+
173
+ Configuration classes support inheritance:
174
+ - Subclasses inherit field definitions from parent classes
175
+ - Each class maintains its own singleton instance
176
+ - Prefixes and settings can be overridden per class
177
+ - Field definitions are merged from parent to child
178
+
179
+ ## Thread Safety Considerations
180
+
181
+ - **Mutex Protection**: Critical sections are protected with mutex locks
182
+ - **Thread-Local Overrides**: Override values are stored per thread ID
183
+ - **Immutable Fields**: Field definitions are immutable after creation
184
+ - **Atomic Operations**: Value resolution is designed to be atomic
185
+
186
+ This architecture provides a robust, flexible, and thread-safe configuration management system that can adapt to various deployment environments and configuration sources.
data/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ 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.6.1
8
+
9
+ ### Added
10
+
11
+ - Show icons on the web UI that open a dialog with the current value for each data source.
12
+
13
+ ## 2.6.0
14
+
15
+ ### Added
16
+
17
+ - Added support for passing the type in `UltraSettings.runtime_settings_url` as `${type}` in the URL.
18
+
19
+ ### Changed
20
+
21
+ - Significantly updated the web UI to improve the layout and usability. The UI is no longer an HTML table and has a cleaner, more modern design.
22
+
7
23
  ## 2.5.0
8
24
 
9
25
  ### Added
data/README.md CHANGED
@@ -211,7 +211,7 @@ You can customize the behavior of runtime setting names with the following optio
211
211
 
212
212
  - **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
213
 
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 placeholder `${name}` in the URL which will be replaced with the name of the runtime setting. If you are using the `super_settings` gem for runtime settings, then you can target a setting by adding `#edit=${name}` to the root URL where `super_settings` is mounted.
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.
215
215
 
216
216
  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
217
 
@@ -422,17 +422,25 @@ If you prefer to embed the settings view directly into your own admin tools or d
422
422
  ```erb
423
423
  <h1>Configuration</h1>
424
424
 
425
- <%= UltraSettings::ApplicationView.new.render(select_class: "form-select", table_class: "table table-striped") %>
425
+ <%= UltraSettings::ApplicationView.new.render(select_class: "form-select") %>
426
426
  ```
427
427
 
428
- 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 in an HTML table with navigation handled by an HTML select element. You can specify the CSS classes for these elements and use your own stylesheets to customize the appearance.
428
+ 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..
429
429
 
430
430
  You can also embed the view for individual configurations within your own views using the `UltraSettings::ConfigurationView` class if you want more customization:
431
431
 
432
432
  ```erb
433
433
  <h1>My Service Settings</h1>
434
434
 
435
- <%= UltraSettings::ConfigurationView.new(MyServiceConfiguration.instance).render(table_class: "table table-striped") %>
435
+ <%= UltraSettings::ConfigurationView.new(MyServiceConfiguration.instance).render %>
436
+ ```
437
+
438
+ You'll also need to include the CSS for the configuration view on your page.
439
+
440
+ ```erb
441
+ <head>
442
+ <%= UltraSettings::ApplicationView.new.style_tag %>
443
+ </head>
436
444
  ```
437
445
 
438
446
  ### Testing With UltraSettings
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.5.0
1
+ 2.6.1
data/app/application.css CHANGED
@@ -6,41 +6,216 @@
6
6
  margin: 0;
7
7
  }
8
8
 
9
- .ultra-settings-table {
10
- width: 100%;
11
- max-width: 100%;
12
- margin-bottom: 1rem;
13
- border-collapse: collapse;
9
+ .ultra-settings-config-file {
10
+ margin-bottom: 1.5rem;
11
+ padding: 1rem;
12
+ background-color: var(--config-file-bg-color);
13
+ border: 1px solid var(--config-file-border-color);
14
+ border-radius: 0.375rem;
15
+ }
16
+
17
+ .ultra-settings-config-file-label {
18
+ font-weight: 600;
19
+ color: var(--text-color);
20
+ margin-right: 0.5rem;
21
+ }
22
+
23
+ .ultra-settings-config-file-path {
24
+ font-family: monospace;
25
+ font-size: 0.875rem;
26
+ color: var(--code-color);
27
+ font-weight: 600;
14
28
  }
15
29
 
16
- .ultra-settings-table thead th {
17
- vertical-align: bottom;
18
- border-bottom: 2px solid var(--table-border-color);
19
- background-color: var(--table-header-bg-color);
30
+ .ultra-settings-file-not-found {
31
+ color: var(--warning-color);
32
+ font-style: italic;
33
+ margin-left: 0.5rem;
20
34
  }
21
35
 
22
- .ultra-settings-table td, .ultra-settings-table th {
23
- padding: 0.75rem;
24
- vertical-align: top;
25
- border-top: 1px solid var(--table-border-color);
36
+ .ultra-settings-fields {
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: 1.75rem;
26
40
  }
27
41
 
28
- .ultra-settings-table tbody tr:nth-of-type(odd) {
29
- background-color: var(--alt-row-color);
42
+ .ultra-settings-fields svg {
43
+ vertical-align: middle;
30
44
  }
31
45
 
32
- .ultra-settings-table code {
46
+ .ultra-settings-field {
47
+ border: 1px solid var(--field-border-color);
48
+ border-radius: 0.5rem;
49
+ background-color: var(--field-bg-color);
50
+ overflow: hidden;
51
+ }
52
+
53
+ .ultra-settings-field-header {
54
+ display: flex;
55
+ justify-content: space-between;
56
+ align-items: center;
57
+ padding: 0.75rem 1rem;
58
+ background-color: var(--field-header-bg-color);
59
+ border-bottom: 1px solid var(--field-border-color);
60
+ }
61
+
62
+ .ultra-settings-field-name {
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 0.5rem;
66
+ }
67
+
68
+ .ultra-settings-field-name code {
33
69
  font-family: monospace;
34
- font-size: 0.9rem;
35
- display: inline;
70
+ font-size: 1rem;
71
+ font-weight: 600;
36
72
  color: var(--code-color);
73
+ }
74
+
75
+ .ultra-settings-field-type {
76
+ font-size: 0.875rem;
37
77
  font-weight: 600;
78
+ color: var(--type-color);
79
+ text-transform: uppercase;
80
+ letter-spacing: 0.025em;
81
+ }
82
+
83
+ .ultra-settings-field-badge {
84
+ display: inline-block;
85
+ padding: 0.125rem 0.5rem;
86
+ font-size: 0.75rem;
87
+ font-weight: 500;
88
+ text-transform: uppercase;
89
+ letter-spacing: 0.025em;
90
+ border-radius: 0.25rem;
91
+ }
92
+
93
+ .ultra-settings-badge-secret {
94
+ background-color: var(--secret-badge-bg-color);
95
+ color: var(--secret-badge-text-color);
96
+ }
97
+
98
+ .ultra-settings-badge-static {
99
+ background-color: var(--static-badge-bg-color);
100
+ color: var(--static-badge-text-color);
101
+ }
102
+
103
+ .ultra-settings-field-value {
104
+ padding: 1rem;
105
+ background-color: var(--value-bg-color);
106
+ border-bottom: 1px solid var(--field-border-color);
38
107
  }
39
108
 
40
- .ultra-settings-table em {
41
- color: var(--em-color);
109
+ code.ultra-settings-field-data-value {
110
+ font-family: monospace;
111
+ font-size: 0.875rem;
112
+ word-break: break-all;
113
+ color: var(--value-text-color);
114
+ background-color: var(--value-code-bg-color);
115
+ padding: 0.25rem 0.5rem;
116
+ border-radius: 0.25rem;
117
+ border: 1px solid var(--value-code-border-color);
118
+ }
119
+
120
+ .ultra-settings-nil-value {
42
121
  font-style: italic;
43
- font-size: 0.9rem;
122
+ color: var(--nil-color);
123
+ }
124
+
125
+ .ultra-settings-field-description {
126
+ padding: 0.75rem 1rem;
127
+ color: var(--description-color);
128
+ line-height: 1.5;
129
+ border-bottom: 1px solid var(--field-border-color);
130
+ display: flex;
131
+ align-items: flex-start;
132
+ gap: 0.5rem;
133
+ }
134
+
135
+ .ultra-settings-description-text {
136
+ flex: 1;
137
+ }
138
+
139
+ .ultra-settings-field-sources {
140
+ padding: 0.5rem;
141
+ }
142
+
143
+ .ultra-settings-source {
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: space-between;
147
+ padding: 0.5rem 0.75rem;
148
+ margin-bottom: 0.25rem;
149
+ border-radius: 0.25rem;
150
+ background-color: var(--source-bg-color);
151
+ border: 1px solid var(--source-border-color);
152
+ transition: all 0.2s ease;
153
+ }
154
+
155
+ .ultra-settings-source-active {
156
+ background-color: var(--source-active-bg-color);
157
+ border-color: var(--source-active-border-color);
158
+ }
159
+
160
+ .ultra-settings-source-type {
161
+ font-size: 0.875rem;
162
+ font-weight: 500;
163
+ color: var(--source-type-color);
164
+ text-transform: uppercase;
165
+ letter-spacing: 0.025em;
166
+ min-width: 120px;
167
+ }
168
+
169
+ .ultra-settings-source-value {
170
+ font-family: monospace;
171
+ font-size: 0.875rem;
172
+ color: var(--source-value-color);
173
+ font-weight: 550;
174
+ flex: 1;
175
+ margin: 0 0.75rem;
176
+ word-break: break-all;
177
+ }
178
+
179
+ .ultra-settings-source-indicator {
180
+ font-size: 0.75rem;
181
+ font-weight: 600;
182
+ color: var(--source-indicator-color);
183
+ text-transform: uppercase;
184
+ letter-spacing: 0.025em;
185
+ }
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
+
207
+ .ultra-settings-edit-link {
208
+ color: var(--edit-link-color);
209
+ text-decoration: none;
210
+ margin-left: 0.5rem;
211
+ opacity: 0.7;
212
+ transition: opacity 0.2s ease;
213
+ display: inline-flex;
214
+ align-items: center;
215
+ }
216
+
217
+ .ultra-settings-edit-link:hover {
218
+ opacity: 1;
44
219
  }
45
220
 
46
221
  .ultra-settings-select {
@@ -63,3 +238,39 @@
63
238
  -moz-appearance: none;
64
239
  appearance: none;
65
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
+ }
@@ -1,13 +1,45 @@
1
1
  <% unless color_scheme == :dark %>
2
- .ultra-settings {
3
- --table-header-bg-color: #fff;
4
- --table-border-color: #dee2e6;
5
- --alt-row-color: rgba(0, 0, 0, .05);
6
- --form-control-color: #495057;
2
+ .ultra-settings, .ultra-settings-block {
3
+ --form-control-color: #484848;
7
4
  --form-control-bg-color: #fff;
8
- --form-control-border-color: #ced4da;
9
- --code-color: darkred;
10
- --em-color: gray;
5
+ --form-control-border-color: #e8e8e8;
6
+ --code-color: #d63384;
7
+
8
+ /* Card layout colors */
9
+ --config-file-bg-color: #f8f8f8;
10
+ --config-file-border-color: #e8e8e8;
11
+ --field-bg-color: #ffffff;
12
+ --field-border-color: #b8b8b8;
13
+ --field-header-bg-color: #e8e8e8;
14
+ --field-header-text-color: #212529;
15
+ --value-bg-color: #fdfdfe;
16
+ --value-text-color: #212529;
17
+ --value-code-bg-color: #f8f8f8;
18
+ --value-code-border-color: #e8e8e8;
19
+ --type-color: #6c757d;
20
+ --description-color: #4a83b5;
21
+ --nil-color: #6c757d;
22
+ --warning-color: #b9202f;
23
+ --info-color: #6ea8fe;
24
+ --disabled-color: #adb5bd;
25
+
26
+ /* Source colors */
27
+ --source-bg-color: #f8f9fa;
28
+ --source-border-color: #e9ecef;
29
+ --source-active-bg-color: #e7f3ff;
30
+ --source-active-border-color: #0d6efd;
31
+ --source-type-color: #666666;
32
+ --source-value-color: #444444;
33
+ --source-indicator-color: #0d6efd;
34
+
35
+ /* Badge colors */
36
+ --secret-badge-bg-color: #dc3545;
37
+ --secret-badge-text-color: #ffffff;
38
+ --static-badge-bg-color: #888888;
39
+ --static-badge-text-color: #ffffff;
40
+
41
+ /* Edit link */
42
+ --edit-link-color: #0d6efd;
11
43
  }
12
44
  <% end %>
13
45
 
@@ -15,15 +47,48 @@
15
47
  @media (prefers-color-scheme: dark) {
16
48
  <% end %>
17
49
  <% if color_scheme == :system || color_scheme == :dark %>
18
- .ultra-settings {
19
- --table-header-bg-color: #333;
20
- --table-border-color: #555;
21
- --alt-row-color: rgba(0, 0, 0, .30);
50
+ .ultra-settings, .ultra-settings-block {
22
51
  --form-control-color: #eee;
23
52
  --form-control-bg-color: #666;
24
53
  --form-control-border-color: #555;
25
- --code-color: pink;
26
- --em-color: #999;
54
+ --code-color: #fd76a3;
55
+ --em-color: #adb5bd;
56
+
57
+ /* Card layout colors */
58
+ --config-file-bg-color: #4b4b4b;
59
+ --config-file-border-color: #444;
60
+ --field-bg-color: #1e1e1e;
61
+ --field-border-color: #444;
62
+ --field-header-bg-color: #2b2b2b;
63
+ --field-header-text-color: #ced4da;
64
+ --value-bg-color: #252525;
65
+ --value-text-color: #e9ecef;
66
+ --value-code-bg-color: #2b2b2b;
67
+ --value-code-border-color: #444;
68
+ --type-color: #adb5bd;
69
+ --description-color: #a9d9f8;
70
+ --nil-color: #cdcecfff;
71
+ --warning-color: #dc3545;
72
+ --info-color: #6ea8fe;
73
+ --disabled-color: #adb5bd;
74
+
75
+ /* Source colors */
76
+ --source-bg-color: #2b2b2b;
77
+ --source-border-color: #444444;
78
+ --source-active-bg-color: #1a3a52;
79
+ --source-active-border-color: #0d6efd;
80
+ --source-type-color: #adb5bd;
81
+ --source-value-color: #ced4da;
82
+ --source-indicator-color: #6ea8fe;
83
+
84
+ /* Badge colors */
85
+ --secret-badge-bg-color: #dc3545;
86
+ --secret-badge-text-color: #ffffff;
87
+ --static-badge-bg-color: #6c757d;
88
+ --static-badge-text-color: #ffffff;
89
+
90
+ /* Edit link */
91
+ --edit-link-color: #a7c6f5;
27
92
  }
28
93
  <% end %>
29
94
  <% if color_scheme == :system %>
@@ -1,141 +1,125 @@
1
- <table class="<%= html_escape(table_class.to_s) %>">
2
- <thead>
3
- <% if !configuration.class.yaml_config_disabled? && configuration.class.configuration_file.is_a?(Pathname) %>
4
- <tr>
5
- <th colspan="6">
6
- Configuration File:
7
- <span style="font-weight: normal;">
8
- <%= html_escape(relative_path(configuration.class.configuration_file)) %>
9
- <% unless configuration.class.configuration_file&.exist? %>
10
- <em>(File does not exist)</em>
11
- <% end %>
12
- </span>
13
- </th>
14
- </tr>
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>
15
8
  <% end %>
16
- <tr>
17
- <th>Name</th>
18
- <th>Value</th>
19
- <th>Type</th>
20
- <th>Notes</th>
21
- </tr>
22
- </thead>
23
- <tbody translate="no">
24
- <% configuration.class.fields.each do |field| %>
25
- <% source = configuration.__source__(field.name) %>
26
- <tr>
27
- <td>
28
- <code><%= html_escape(field.name) %></code>
29
- </td>
9
+ </div>
10
+ <% end %>
30
11
 
31
- <td style="word-wrap: break-word; max-width:30em;">
32
- <% if configuration[field.name].nil? %>
33
- <em>nil</em>
34
- <% elsif field.secret? %>
35
- <%= html_escape(secret_value(configuration[field.name])) %>
36
- <% else %>
37
- <%= html_escape(display_value(configuration[field.name])) %>
12
+ <div class="ultra-settings-fields">
13
+ <% configuration.class.fields.each do |field| %>
14
+ <% source = configuration.__source__(field.name) %>
15
+ <div class="ultra-settings-field">
16
+ <div class="ultra-settings-field-header">
17
+ <div class="ultra-settings-field-name">
18
+ <code><%= html_escape(field.name) %></code>
19
+ <% if field.secret? %>
20
+ <span class="ultra-settings-field-badge ultra-settings-badge-secret">secret</span>
38
21
  <% end %>
39
- </td>
40
-
41
- <td>
42
- <%= html_escape(field.type) %>
43
- <%
44
- options = []
45
- options << 'static' if field.static?
46
- options << 'secret' if field.secret?
47
- %>
48
- <% unless options.empty? %>
49
- <div>
50
- <em><%= html_escape(options.join(', ')) %></em>
51
- </div>
22
+ <% if field.static? %>
23
+ <span class="ultra-settings-field-badge ultra-settings-badge-static">static</span>
52
24
  <% end %>
53
- </td>
25
+ </div>
26
+ <div class="ultra-settings-field-type">
27
+ <%= html_escape(field.type) %>
28
+ </div>
29
+ </div>
54
30
 
55
- <td style="word-wrap: break-word;">
56
- <% unless field.description.to_s.empty? %>
57
- <div>
58
- <%= html_escape(field.description) %>
59
- </div>
60
- <% end %>
31
+ <div class="ultra-settings-field-value">
32
+ <% if configuration[field.name].nil? %>
33
+ <span class="ultra-settings-nil-value">nil</span>
34
+ <% elsif field.secret? %>
35
+ <code class="ultra-settings-field-data-value"><%= html_escape(secret_value(configuration[field.name])) %></code>
36
+ <% else %>
37
+ <code class="ultra-settings-field-data-value"><%= html_escape(display_value(configuration[field.name])) %></code>
38
+ <% end %>
39
+ </div>
61
40
 
62
- <ul style="margin: 0 0 0 1rem; padding: 0; list-style-type: disc; list-style-position: outside;">
63
- <% if field.env_var && !configuration.class.environment_variables_disabled? %>
64
- <li>
65
- <% if source == :env %>
66
- <strong>
67
- Currently
68
- <% else %>
69
- Can be
70
- <% end %>
71
- set with the environment variable
72
- <code><%= show_defined_value(field.env_var, configuration.__value_from_source__(field.name, :env), field.secret?) %></code>
73
- <% if source == :env %>
74
- </strong>
75
- <% end %>
76
- </li>
41
+ <% unless field.description.to_s.empty? %>
42
+ <div class="ultra-settings-field-description">
43
+ <%= info_icon %>
44
+ <div class="ultra-settings-description-text">
45
+ <%= html_escape(field.description) %>
46
+ </div>
47
+ </div>
48
+ <% end %>
49
+
50
+ <div class="ultra-settings-field-sources">
51
+ <% if field.env_var && !configuration.class.environment_variables_disabled? %>
52
+ <div class="ultra-settings-source <%= 'ultra-settings-source-active' if source == :env %>">
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>
60
+ <% if source == :env %>
61
+ <span class="ultra-settings-source-indicator">Currently active</span>
77
62
  <% end %>
78
- <% if field.runtime_setting && !configuration.class.runtime_settings_disabled? %>
79
- <li>
80
- <% if source == :settings %>
81
- <strong>
82
- Currently
83
- <% else %>
84
- Can be
85
- <% end %>
86
- set with the runtime setting
87
- <code><%= show_defined_value(field.runtime_setting, configuration.__value_from_source__(field.name, :settings), field.secret?) %></code>
88
- <% if source == :settings %>
89
- </strong>
90
- <% end %>
63
+ </div>
64
+ <% end %>
91
65
 
92
- <% edit_url = UltraSettings.runtime_settings_url(field.runtime_setting) %>
93
- <% if edit_url %>
94
- <a href="<%= html_escape(edit_url) %>" title="Edit <%= html_escape(field.runtime_setting) %>" style="text-decoration: none; color: inherit; vertical-align: middle;">
95
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil-square" viewBox="0 0 16 16">
96
- <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"/>
97
- <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"/>
98
- </svg>
99
- </a>
100
- <% end %>
101
- </li>
66
+ <% if field.runtime_setting && !configuration.class.runtime_settings_disabled? %>
67
+ <div class="ultra-settings-source <%= 'ultra-settings-source-active' if source == :settings %>">
68
+ <span class="ultra-settings-source-type">Runtime Setting</span>
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>
73
+ <% if source == :settings %>
74
+ <span class="ultra-settings-source-indicator">Currently active</span>
102
75
  <% end %>
103
- <% if field.yaml_key && !configuration.class.yaml_config_disabled? %>
104
- <li>
105
- <% if source == :yaml %>
106
- <strong>
107
- Currently
108
- <% else %>
109
- Can be
110
- <% end %>
111
- set with the configuration file key
112
- <code><%= show_defined_value(field.yaml_key, configuration.__value_from_source__(field.name, :yaml), field.secret?) %></code>
113
- <% if source == :yaml %>
114
- </strong>
115
- <% end %>
116
- </li>
76
+ <% edit_url = UltraSettings.runtime_settings_url(field.runtime_setting, field.type) %>
77
+ <% if edit_url %>
78
+ <a href="<%= html_escape(edit_url) %>" class="ultra-settings-edit-link" title="Edit <%= html_escape(field.runtime_setting) %>">
79
+ <%= edit_icon %>
80
+ </a>
117
81
  <% end %>
118
- <% if field.default.nil? %>
119
- <% if source == :default %>
120
- <li>
121
- <strong>Not set</strong>
122
- </li>
123
- <% end %>
124
- <% else %>
125
- <li>
126
- <% if source == :default %>
127
- <strong>
128
- Currently set with the
129
- <%= show_defined_value("default value", field.default, field.secret?) %>.
130
- </strong>
131
- <% else %>
132
- This field has a <%= show_defined_value("default value", field.default, field.secret?) %>.
133
- <% end %>
134
- </li>
82
+ </div>
83
+ <% end %>
84
+
85
+ <% if field.yaml_key && !configuration.class.yaml_config_disabled? %>
86
+ <div class="ultra-settings-source <%= 'ultra-settings-source-active' if source == :yaml %>">
87
+ <span class="ultra-settings-source-type">Configuration File</span>
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>
92
+ <% if source == :yaml %>
93
+ <span class="ultra-settings-source-indicator">Currently active</span>
135
94
  <% end %>
136
- </ul>
137
- </td>
138
- </tr>
139
- <% end %>
140
- </tbody>
141
- </table>
95
+ </div>
96
+ <% end %>
97
+
98
+ <% if !field.default.nil? || source == :default %>
99
+ <div class="ultra-settings-source <%= 'ultra-settings-source-active' if source == :default %>">
100
+ <span class="ultra-settings-source-type">Default Value</span>
101
+ <code class="ultra-settings-source-value">
102
+ <%= show_defined_value("Default Value", field.default, field.secret?) %>
103
+ </code>
104
+ <% if source == :default %>
105
+ <span class="ultra-settings-source-indicator">Currently active</span>
106
+ <% end %>
107
+ </div>
108
+ <% end %>
109
+ </div>
110
+ </div>
111
+ <% end %>
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>
125
+ </div>
data/app/index.html.erb CHANGED
@@ -1,3 +1,5 @@
1
+ <%= style_tag %>
2
+
1
3
  <div class="ultra-settings">
2
4
  <div class="ultra-settings-nav">
3
5
  <form onsubmit="return false" style="margin-bottom: 0.5rem;">
data/app/layout.html.erb CHANGED
@@ -7,8 +7,7 @@
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
8
  <meta name="format-detection" content="telephone=no email=no date=no address=no">
9
9
  <style type="text/css">
10
- <%= @layout_css %>
11
- <%= css %>
10
+ <%= layout_css %>
12
11
  </style>
13
12
  </head>
14
13
  <body>
@@ -4,44 +4,34 @@ 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 table
8
- # showing each configuration. You can specify the CSS class for the select element and the tables
9
- # by passing the `select_class` and `table_class` option to the `render` method. By default the
10
- # select elewment have the class `ultra-settings-select` and the table will have the class
11
- # `ultra-settings-table`.
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`.
12
11
  #
13
12
  # @example
14
13
  # <h1>Application Configuration</h1>
15
- # <%= UltraSettings::ApplicationView.new.render(select_class: 'form-control', table_class: "table table-striped") %>
14
+ # <%= UltraSettings::ApplicationView.new.render(select_class: 'form-control') %>
16
15
  class ApplicationView
17
- @template = nil
16
+ attr_reader :css
18
17
 
19
- class << self
20
- def template
21
- @template ||= ERB.new(read_app_file("index.html.erb"))
22
- end
23
-
24
- def javascript
25
- @javascript = read_app_file("application.js")
26
- end
27
-
28
- private
29
-
30
- def read_app_file(path)
31
- File.read(File.join(app_dir, path))
32
- end
33
-
34
- def app_dir
35
- File.expand_path(File.join("..", "..", "app"), __dir__)
36
- end
18
+ def initialize(color_scheme: :light)
19
+ @css = application_css(color_scheme)
20
+ @css = @css.html_safe if @css.respond_to?(:html_safe)
37
21
  end
38
22
 
39
- def render(select_class: "ultra-settings-select", table_class: "ultra-settings-table")
40
- html = self.class.template.result(binding)
23
+ def render(select_class: "ultra-settings-select", table_class: "")
24
+ html = ViewHelper.erb_template("index.html.erb").result(binding)
41
25
  html = html.html_safe if html.respond_to?(:html_safe)
42
26
  html
43
27
  end
44
28
 
29
+ def style_tag
30
+ tag = "<style type=\"text/css\">\n#{css}\n</style>"
31
+ tag = tag.html_safe if tag.respond_to?(:html_safe)
32
+ tag
33
+ end
34
+
45
35
  def to_s
46
36
  render
47
37
  end
@@ -53,7 +43,13 @@ module UltraSettings
53
43
  end
54
44
 
55
45
  def javascript
56
- self.class.javascript
46
+ ViewHelper.read_app_file("application.js")
47
+ end
48
+
49
+ def application_css(color_scheme)
50
+ vars = ViewHelper.erb_template("application_vars.css.erb").result(binding).strip
51
+ css = ViewHelper.read_app_file("application.css").strip
52
+ "#{vars}\n#{css}"
57
53
  end
58
54
  end
59
55
  end
@@ -1,42 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module UltraSettings
4
- # This class can render information about a configuration in an HTML table. It is used by the
4
+ # This class can render information about a configuration in a clean card-based layout. It is used by the
5
5
  # bundled web UI, but you can use it to embed the configuration information in your own web pages.
6
6
  #
7
- # The output will be an HTML table. You can specify the CSS class for the table by passing the
8
- # `table_class` option to the `render` method. By default the table will have the class
9
- # `ultra-settings-table`.
7
+ # The output will be HTML with a card-based layout for better readability. The `table_class` option is
8
+ # still supported for backward compatibility but is no longer used in the new card layout.
10
9
  #
11
10
  # @example
12
11
  # <h1>Service Configuration</h1>
13
- # <%= UltraSettings::ConfigurationView.new(ServiceConfiguration.instance).render(table_class: "table table-striped") %>
12
+ # <%= UltraSettings::ConfigurationView.new(ServiceConfiguration.instance).render %>
14
13
  class ConfigurationView
15
- @template = nil
16
-
17
- class << self
18
- def template
19
- @template ||= ERB.new(read_app_file("configuration.html.erb"))
20
- end
21
-
22
- private
23
-
24
- def read_app_file(path)
25
- File.read(File.join(app_dir, path))
26
- end
27
-
28
- def app_dir
29
- File.expand_path(File.join("..", "..", "app"), __dir__)
30
- end
31
- end
32
-
33
14
  def initialize(configuration)
34
15
  @configuration = configuration
35
16
  end
36
17
 
37
- def render(table_class: "ultra-settings-table")
18
+ def render(table_class: "")
38
19
  configuration = @configuration
39
- html = self.class.template.result(binding)
20
+ html = ViewHelper.erb_template("configuration.html.erb").result(binding)
40
21
  html = html.html_safe if html.respond_to?(:html_safe)
41
22
  html
42
23
  end
@@ -61,14 +42,29 @@ module UltraSettings
61
42
  end
62
43
 
63
44
  def show_defined_value(label, value, secret)
64
- title = if value.nil?
65
- "Not set"
45
+ val = nil
46
+ icon = nil
47
+ css_class = nil
48
+
49
+ if value.nil?
50
+ val = "Not set"
51
+ icon = not_set_icon
52
+ css_class = "ultra-settings-icon-not-set"
66
53
  elsif secret
67
- "Secret value"
54
+ val = secret_value(value)
55
+ icon = lock_icon
56
+ css_class = "ultra-settings-icon-secret"
68
57
  else
69
- "Value: #{display_value(value)}"
58
+ val = display_value(value)
59
+ icon = eye_icon
60
+ css_class = "ultra-settings-icon-info"
70
61
  end
71
- "<dfn style=\"text-decoration: underline dotted;\" title=\"#{html_escape(title)}\">#{html_escape(label)}</dfn>"
62
+
63
+ <<~HTML
64
+ <dfn class="#{css_class}" title="#{html_escape(val)}" onclick="#{html_escape(open_dialog_script)}" data-label="#{html_escape(label)}">
65
+ #{icon}
66
+ </dfn>
67
+ HTML
72
68
  end
73
69
 
74
70
  def secret_value(value)
@@ -87,5 +83,69 @@ module UltraSettings
87
83
  end
88
84
  path.relative_path_from(root_path)
89
85
  end
86
+
87
+ def info_icon(size = 16)
88
+ <<~HTML
89
+ <svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
90
+ <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"/>
91
+ <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"/>
92
+ </svg>
93
+ HTML
94
+ end
95
+
96
+ def not_set_icon(size = 16)
97
+ <<~HTML
98
+ <svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
99
+ <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"/>
100
+ <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"/>
101
+ </svg>
102
+ HTML
103
+ end
104
+
105
+ def lock_icon(size = 16)
106
+ <<~HTML
107
+ <svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
108
+ <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"/>
109
+ </svg>
110
+ HTML
111
+ end
112
+
113
+ def edit_icon(size = 16)
114
+ <<~HTML
115
+ <svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
116
+ <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"/>
117
+ <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"/>
118
+ </svg>
119
+ HTML
120
+ end
121
+
122
+ def eye_icon(size = 16)
123
+ <<~HTML
124
+ <svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
125
+ <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"/>
126
+ <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"/>
127
+ </svg>
128
+ HTML
129
+ end
130
+
131
+ def close_icon(size = 16)
132
+ <<~HTML
133
+ <svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
134
+ <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"/>
135
+ <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"/>
136
+ </svg>
137
+ HTML
138
+ end
139
+
140
+ private
141
+
142
+ def open_dialog_script
143
+ <<~JAVASCRIPT.gsub(/\s+/, " ").tr('"', "'")
144
+ this.closest('.ultra-settings-configuration').querySelector('.ultra-settings-dialog-title').textContent = this.dataset.label;
145
+ this.closest('.ultra-settings-configuration').querySelector('.ultra-settings-dialog-value').textContent = this.title;
146
+ this.closest('.ultra-settings-configuration').querySelector('.ultra-settings-dialog').showModal();
147
+ this.closest('.ultra-settings-configuration').querySelector('.ultra-settings-dialog-close').blur();
148
+ JAVASCRIPT
149
+ end
90
150
  end
91
151
  end
@@ -9,7 +9,6 @@ module UltraSettings
9
9
  config.ultra_settings = ActiveSupport::OrderedOptions.new
10
10
  config.ultra_settings.auto_load_directories ||= [File.join("app", "configurations")]
11
11
 
12
- # initializer "ultra_settings.before_bootstrap", before: :bootstrap_hook do
13
12
  config.before_configuration do
14
13
  UltraSettings::Configuration.yaml_config_env ||= Rails.env
15
14
  UltraSettings::Configuration.yaml_config_path ||= Rails.root.join("config")
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UltraSettings
4
+ # Base class for rendering views.
5
+ module ViewHelper
6
+ @cache = {}
7
+
8
+ class << self
9
+ def erb_template(path)
10
+ @cache["erb:#{path}"] ||= ERB.new(read_app_file(path))
11
+ end
12
+
13
+ def read_app_file(path)
14
+ @cache["file:#{path}"] ||= File.read(File.join(app_dir, path))
15
+ end
16
+
17
+ def app_dir
18
+ File.expand_path(File.join("..", "..", "app"), __dir__)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -3,15 +3,14 @@
3
3
  module UltraSettings
4
4
  # Helper class for rendering the settings information in an HTML page.
5
5
  class WebView
6
- attr_reader :css
6
+ attr_reader :layout_css
7
7
 
8
8
  # @param color_scheme [Symbol] The color scheme to use in the UI. This can be `:light`,
9
9
  # `:dark`, or `:system`. The default is `:light`.
10
10
  def initialize(color_scheme: :light)
11
- color_scheme = (color_scheme || :light).to_sym
12
- @layout_template = erb_template("layout.html.erb")
13
- @layout_css = layout_css(color_scheme)
14
- @css = application_css(color_scheme)
11
+ @color_scheme = (color_scheme || :light).to_sym
12
+ @layout_template = ViewHelper.erb_template("layout.html.erb")
13
+ @layout_css = scheme_layout_css(@color_scheme)
15
14
  end
16
15
 
17
16
  def render_settings
@@ -19,32 +18,14 @@ module UltraSettings
19
18
  end
20
19
 
21
20
  def content
22
- UltraSettings::ApplicationView.new.render
21
+ UltraSettings::ApplicationView.new(color_scheme: @color_scheme).render
23
22
  end
24
23
 
25
24
  private
26
25
 
27
- def erb_template(path)
28
- ERB.new(read_app_file(path))
29
- end
30
-
31
- def read_app_file(path)
32
- File.read(File.join(app_dir, path))
33
- end
34
-
35
- def app_dir
36
- File.expand_path(File.join("..", "..", "app"), __dir__)
37
- end
38
-
39
- def layout_css(color_scheme)
40
- vars = erb_template("layout_vars.css.erb").result(binding)
41
- css = read_app_file("layout.css")
42
- "#{vars}\n#{css}"
43
- end
44
-
45
- def application_css(color_scheme)
46
- vars = erb_template("application_vars.css.erb").result(binding)
47
- css = read_app_file("application.css")
26
+ def scheme_layout_css(color_scheme)
27
+ vars = ViewHelper.erb_template("layout_vars.css.erb").result(binding)
28
+ css = ViewHelper.read_app_file("layout.css")
48
29
  "#{vars}\n#{css}"
49
30
  end
50
31
  end
@@ -13,6 +13,7 @@ require_relative "ultra_settings/coerce"
13
13
  require_relative "ultra_settings/config_helper"
14
14
  require_relative "ultra_settings/field"
15
15
  require_relative "ultra_settings/rack_app"
16
+ require_relative "ultra_settings/view_helper"
16
17
  require_relative "ultra_settings/web_view"
17
18
  require_relative "ultra_settings/application_view"
18
19
  require_relative "ultra_settings/configuration_view"
@@ -182,11 +183,12 @@ module UltraSettings
182
183
  # @param name [String] The name of the setting.
183
184
  # @return [String, nil]
184
185
  # @api private
185
- def runtime_settings_url(name)
186
+ def runtime_settings_url(name, type)
186
187
  url = @runtime_settings_url.to_s
187
188
  return nil if url.empty?
188
189
 
189
- url.gsub("${name}", URI.encode_www_form_component(name))
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))
190
192
  end
191
193
 
192
194
  def fields_secret_by_default=(value)
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.5.0
4
+ version: 2.6.1
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-04-24 00:00:00.000000000 Z
11
+ date: 2025-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -31,6 +31,7 @@ executables: []
31
31
  extensions: []
32
32
  extra_rdoc_files: []
33
33
  files:
34
+ - ARCHITECTURE.md
34
35
  - CHANGELOG.md
35
36
  - MIT-LICENSE.txt
36
37
  - README.md
@@ -54,6 +55,7 @@ files:
54
55
  - lib/ultra_settings/railtie.rb
55
56
  - lib/ultra_settings/uninitialized_runtime_settings.rb
56
57
  - lib/ultra_settings/version.rb
58
+ - lib/ultra_settings/view_helper.rb
57
59
  - lib/ultra_settings/web_view.rb
58
60
  - lib/ultra_settings/yaml_config.rb
59
61
  - ultra_settings.gemspec