ultra_settings 0.0.1.rc1 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4bf1774f001f4b527b419e5c1f19ed05556a86ecf8f88b0b07df9f828ca3c7b
4
- data.tar.gz: a204f002cc7a2a5d0747cd00abd6d64f9b7facb899475a4f9d96c15eeaa8814e
3
+ metadata.gz: ca29611f95d52ce78e465c06876bfedfef76fa166b0f2d5d6279e08cfada0397
4
+ data.tar.gz: cee494a62d579c2518ae1ca5db8b2483ab5906a3323a1c01e86dd45430fb495d
5
5
  SHA512:
6
- metadata.gz: 1a9a7d61e85e0c0a3cd406535f33e978b1455cfcecb66cf8376ec3d76255540b35e950e39566c86b758004e3d80491d9267c4cccb6b5a889cd964d3b3119afe9
7
- data.tar.gz: e6f32cbc8d56496a7486ed05bd388e8ab93229b1df2ac016e56c9fac610dbc39ddd29c89967ae5f2ca1029f8d34919b1bbde451b31c4262adde44adde68dfa8f
6
+ metadata.gz: 99dedad75b7b425ea2b44a1b3002a5b53b54edd999e69f49cfed3ff331fab9ec907fbfb43465c30be0cb23559288c5904201764b95cfcb3b38833aa402a1c007
7
+ data.tar.gz: 010f49d61c14a24b46e4fb42ceb70ee6d714b83c8d9d9d653463b5933ad20d7f3ff7adaad53707fe53fc32d3c03af39430fc478040d39fbb481ac2e6f2863e9f
data/CHANGELOG.md CHANGED
@@ -4,6 +4,11 @@ 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
+ ## 1.0.1
8
+
9
+ ### Added
10
+ - Optimize object shapes for the Ruby interpreter by declaring instance variables in constructors.
11
+
7
12
  ## 1.0.0
8
13
 
9
14
  ### Added
data/README.md CHANGED
@@ -1,18 +1,337 @@
1
- # Unified Rails Configuration :construction:
1
+ # UltraSettings
2
2
 
3
3
  [![Continuous Integration](https://github.com/bdurand/ultra_settings/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/ultra_settings/actions/workflows/continuous_integration.yml)
4
+ [![Regression Test](https://github.com/bdurand/ultra_settings/actions/workflows/regression_test.yml/badge.svg)](https://github.com/bdurand/ultra_settings/actions/workflows/regression_test.yml)
4
5
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
5
6
 
6
- TODO
7
+ This gem provides a method for managing application settings and loading their values from a variety of sources. It can help you get a handle on you application's configuration by providing a consistent method for accessing settings. It also provides a method for documenting your application's settings.
8
+
9
+ It allows you to define a hierarchy with three layers of sources for your configuration values:
10
+
11
+ 1. Environment variables
12
+ 2. Runtime settings (i.e. settings updatable from within the running application)
13
+ 3. YAML configuration files
14
+
15
+ Settings at a higher level will override those set at a lower level. So, for instance, you can override values set in a YAML file with either environment variables or runtime settings. The hierarchy is an optional feature and can be disabled in favor of explicitly defined data sources if you want.
16
+
17
+ Your application code does not need to concern itself with how a setting value is being loaded or from what source. It can just reference configuration settings using plain old Ruby objects and methods.
18
+
19
+ Settings are also type cast so you can always be assured that values are returned as a predetermined class and your application does not need to worry about type coercion. The supported types are:
20
+
21
+ - `String`
22
+ - `Integer`
23
+ - `Float`
24
+ - `Boolean`
25
+ - `Time`
26
+ - `Symbol`
27
+ - `Array<String>`
28
+
29
+ You can also define default values to be returned in case the configured value is missing or it fails to match a constraint.
30
+
31
+ Settings are accessed through singleton classes that you define.
7
32
 
8
33
  ## Usage
9
34
 
35
+ ### Defining Configurations
36
+
37
+ Configurations are classes that extend from the `UltraSettings::Configuration` class. Configuration classes are [singleton classes](https://ruby-doc.org/3.2.2/stdlibs/singleton/Singleton.html).
38
+
39
+ You can define fields on your configuration classes with the `field` method. This will define a method on your configuration object with the given name.
40
+
41
+ ```ruby
42
+ class MyServiceConfiguration < UltraSettings::Configuration
43
+ field :host, type: :string
44
+
45
+ field :port, type: :integer, default: 80
46
+
47
+ field :protocol, type: :string, default: "https"
48
+
49
+ field :timeout, type: :float, default: 1.0, default_if: ->(val) { val <= 0 }
50
+
51
+ field :auth_token,
52
+ type: :string,
53
+ env_var: "MY_SERVICE_TOKEN",
54
+ runtime_setting: false,
55
+ yaml_key: false,
56
+ description: "Bearer token for accessing the service"
57
+
58
+ # You aren't limited to just defining fields, you can define other
59
+ # helper methods to make using the configuration easier.
60
+ def uri
61
+ URI("#{protocol}://#{host}:#{port}")
62
+ end
63
+ end
64
+ ```
65
+
66
+ - You can specify a return type with the `:type` option. The value of the setting will be cast to this type. Valid types are:
67
+
68
+ - `:string` (the default)
69
+ - `:integer`
70
+ - `:float`
71
+ - `:boolean`
72
+ - `:datetime`
73
+ - `:symbol`
74
+ - `:array` (of strings)
75
+
76
+ - You can specify a default value with the `:default` option. Note that this value will still be cast to the defined type.
77
+
78
+ - You can specify a trigger of when the default should be used with the `:default_if` option. If this option is provided, then it should be either a `Proc` or the name of a method in the class to call with the value from the settings. You can use this feature, for example, to always ensure that a value meets certain constraints.
79
+
80
+ - You can describe what your setting does with the `:description` option. This value is only for documentation purposes.
81
+
82
+ - You can override the environment variable used to populate the setting with the `:env_var` option. You can use this to point to an environment variable name that does not match the conventional pattern. You can also set this to `false` to disable loading the field from an environment variable.
83
+
84
+ - You can override the key in the YAML file with the `:yaml_key` option. You can use this to map a setting to a key in the YAML hash if the key doesn't match the field name. You can also set this to `false` to disable loading the field from a YAML file.
85
+
86
+ - You can override the name of the runtime setting used to populate the setting with the `:runtime_setting` option. You can use this to point to a setting whose name does not match the conventional pattern. You can also set this to `false` to disable loading the field from runtime settings.
87
+
88
+ - You can define a value as a static value by setting the `:static` option to true. Static values will not be changed once they are set. Static values also cannot be set from runtime settings. If you are referencing a setting during your application's initialization, then you should declare it as a static field.
89
+
90
+ ### Environment Variables
91
+
92
+ Settings will first try to load values from environment variables. Environment variables are a good place to define environment specific values.
93
+
94
+ By default settings will be loaded from environment variables by constructing a prefix from the configuration class name (i.e. `Configs::MySettingsConfiguration` uses the prefix `"CONFIGS_MY_SETTINGS_"`) with the field name appended to it. By default environment variables will be in all uppercase letters.
95
+
96
+ You can use lowercase environment variable names by setting `env_var_upcase` to `false` on your configuration class.
97
+
98
+ You can use a different delimiter by setting `env_var_delimiter` on your configuration class. The delimiter is used between modules and before the setting name so a delimiter of "." on `Configs::MySettingsConfiguration#setting` would produce "CONFIGS.MY_SETTINGS.SETTING".
99
+
100
+ You can set an explicit prefix by setting `env_var_prefix` on your configuration class.
101
+
102
+ You can disable environment variables as a default source on your fields by setting `environment_variables_disabled` to `true` on your configuration class.
103
+
104
+ If a setting value cannot be loaded from an environment variable, then it's value will attempt to be loaded from a runtime setting.
105
+
106
+ ### Runtime Settings
107
+
108
+ Runtime settings are settings that are loaded at runtime while your application is running. The advantage to this kind of setting is that your application does not need to restart in order to get an updated value.
109
+
110
+ To use runtime settings, you need to set the `UltraSettings.runtime_settings` attribute to an object that defines the `[]` method and takes a string as the argument. For instance, if you wanted to load runtime settings from a Redis database, you could implement them like this.
111
+
112
+ ```ruby
113
+ class RedisRuntimeSettings
114
+ def initialize
115
+ @redis = Redis.new
116
+ end
117
+
118
+ def [](name)
119
+ @redis.get(name)
120
+ end
121
+ end
122
+
123
+ UltraSettings.runtime_settings = RedisRuntimeSettings.new
124
+ ```
125
+
126
+ 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 itself.
127
+
128
+ ```ruby
129
+ UltraSettings.runtime_settings = SuperSettings
130
+ ```
131
+
132
+ By default settings will be loaded from runtime settings by constructing a prefix from the configuration class name (i.e. `Configs::MySettingsConfiguration` uses the prefix `"configs.my_settings."`) with the field name appended to it. By default runtime settings will be in all lowercase letters.
133
+
134
+ You can use uppercase runtime setting names by setting `runtime_setting_upcase` to `true` on your configuration class.
135
+
136
+ You can use a different delimiter by setting `runtime_setting_delimiter` on your configuration class. The delimiter is used between modules and before the setting name so a delimiter of "/" on `Configs::MySettingsConfiguration#setting` would produce "configs/my_settings/setting".
137
+
138
+ You can set an explicit prefix by setting `runtime_setting_prefix` on your configuration class.
139
+
140
+ You can disable runtime settings as a default source on your fields by setting `runtime_settings_disabled` to `true` on your configuration class.
141
+
142
+ If a setting value cannot be loaded from the runtime settings, then it's value will attempt to be loaded from a YAML file.
143
+
144
+ ### YAML Files
145
+
146
+ The last place settings will be loaded from are from static YAML files. These can provide a good place to store default values for you application since they can be distributed with your application code.
147
+
148
+ By default settings will be loaded from a YAML file determined by its class name (i.e. `Configs::MySettingsConfiguration` uses the file `"configs/my_settings.yml"`). The file will be searched for in the path defined by `UltraSettings.yaml_config_path`. If the file does not exist, then settings will not use the YAML source strategy.
149
+
150
+ You can specify an explicit YAML file to use by setting `configuration_file` on your configuration class to the path to the file.
151
+
152
+ You can disable YAML files as a default source on your fields by setting `yaml_config_disabled` to `true` on your configuration class.
153
+
154
+ YAML files will be evaluated for an ERB markup (i.e. `<%= %>`) before the YAML itself is evaluated. You can use this feature to dynamically generate values within the YAML file.
155
+
156
+ YAML files define environment specific configurations. YAML files must define a hash where the keys are the names of your application environments (i.e. development, test, production, etc.). You define which environment to use with `UltraSettings.yaml_config_env` (the default environment is "development"). There is also a special key `"shared"` which, if defined, will be merged with the environment hash.
157
+
158
+ So, for this YAML file:
159
+
160
+ ```yaml
161
+ shared:
162
+ timeout: 5
163
+ port: 8000
164
+
165
+ development:
166
+ timeout: 10
167
+ host: localhost
168
+
169
+ production:
170
+ host: prod.example.com
171
+ ```
172
+
173
+ The values for the "development" environment would be the combination of development and shared:
174
+
175
+ ```ruby
176
+ {
177
+ timeout: 10,
178
+ port: 8000,
179
+ host: "localhost"
180
+ }
181
+ ```
182
+
183
+ While for "production", the values would be the combination of production and shared:
184
+
185
+ ```ruby
186
+ {
187
+ timeout: 5,
188
+ port: 8000,
189
+ host: "prod.example.com"
190
+ }
191
+ ```
192
+
193
+ Values for the environment will always overwrite values from the shared hash.
194
+
195
+ In a Rails application, the YAML environment will be set to the Rails environment and YAML files will be assumed to exist in the `config` directory.
196
+
197
+ ### Removing The Hierarchy
198
+
199
+ If you don't like the default behavior of the hierarchy of environment variables, runtime settings, and YAML files, you can disable it and then explicitly enable only the appropriate data source on each field.
200
+
201
+ ```ruby
202
+ class MyServiceConfiguration < UtraSettings::Configuration
203
+ self.environment_variables_disabled = false
204
+ self.runtime_settings_disabled = false
205
+ self.yaml_config_disabled = false
206
+
207
+ field :host, yaml_key: "host"
208
+ field :token, env_var: "MY_SERVICE_TOKEN"
209
+ field :timeout, runtime_setting: "my_service.timeout", default: 5
210
+ end
211
+ ```
212
+
213
+ If you don't want the hierarchy in any configuration, then you can disable it globally.
214
+
215
+ ```ruby
216
+ UltraSettings.environment_variables_disabled = true
217
+ UltraSettings.runtime_settings_disabled = true
218
+ UltraSettings.yaml_config_disabled = true
219
+ ```
220
+
221
+ ### Accessing settings
222
+
223
+ Configurations are singleton objects. Settings are accessed by calling methods.
224
+
10
225
  ```ruby
226
+ MyServiceConfiguration.instance.host
227
+ ```
228
+
229
+ You can add configurations as methods onto the `UltraSettings` object. By default the configuration class name will be guessed (i.e. "my_service" maps to `MyServiceConfiguration`).
230
+ ```ruby
231
+ UltraSettings.add(:my_service)
11
232
  UltraSettings.my_service.host
233
+ ```
234
+
235
+ You can also specify the class name to map to a different method name.
236
+ ```ruby
237
+ UltraSettings.add(:my, "MyServiceConfiguration")
238
+ UltraSettings.my.host
239
+ ```
240
+
241
+ In a Rails application, you could add some syntactic sugar and expose the `UltraSettings` object as a helper method in application.rb.
242
+
243
+ ```ruby
244
+ module MyApp
245
+ class Application < Rails::Application
246
+ def settings
247
+ UltraSettings
248
+ end
249
+ end
250
+ end
12
251
 
13
252
  Rails.application.settings.my_service.host
14
253
  ```
15
254
 
255
+ You can also keep things clean if your configuration is mostly accessed from within another class.
256
+
257
+ ```ruby
258
+ class MyService
259
+
260
+ # Reference the value as `settings.host`
261
+
262
+ private
263
+
264
+ def settings
265
+ MyServiceConfiguration.instance
266
+ end
267
+ end
268
+ ```
269
+
270
+ ### Web UI
271
+
272
+ There is a web UI available via a mountable Rack application. The UI will only expose the source of where settings are being loaded from. For security reasons, it will not show any of the setting values. It is still highly advised to put it behind whatever authorization framework you application uses.
273
+
274
+ ![Web UI](assets/web_ui.png)
275
+
276
+ Here is a simple example of how to mount in a Rails application behind HTTP Basic authentication with hard coded credentials.
277
+
278
+ ```ruby
279
+ # config/routes.rb
280
+
281
+ mount Rack::Builder.new do
282
+ use Rack::Auth::Basic do |username, password|
283
+ username == ENV.fetch("AUTH_USER") && password == ENV.fetch("AUTH_PASSWORD")
284
+ end
285
+ run UltraSettings::RackApp
286
+ end, at: "/ultra_settings"
287
+ ```
288
+
289
+ ### Testing
290
+
291
+ You can use the `UltraSettings.override!` method to force different configuration settings in you automated tests. Here's examples of overriding the `TestConfiguration#foo` value in a test block:
292
+
293
+ ```ruby
294
+ # Override a configuration added on the global namespace
295
+
296
+ UltraSettings.override!(test: {foo: "bar"}) do
297
+ expect(TestConfiguration.instance.foo).to eq "bar"
298
+ end
299
+
300
+ # or directly on the configuration class
301
+
302
+ TestConfiguration.override!(foo: "bar") do
303
+ expect(TestConfiguration.instance.foo).to eq "bar"
304
+ end
305
+
306
+ # or on the instance itself
307
+
308
+ TestConfiguration.instance.override!(foo: "bar") do
309
+ expect(TestConfiguration.instance.foo).to eq "bar"
310
+ end
311
+ ```
312
+
313
+ If you are using RSpec, you can set up a global before handler to make it easier to specify settings within your test blocks.
314
+
315
+ ```ruby
316
+ # RSpec setup
317
+ RSpec.configure do |config|
318
+ config.around do |example|
319
+ if example.metadata[:ultra_settings].is_a?(Hash)
320
+ UltraSettings.override!(example.metadata[:ultra_settings]) do
321
+ example.run
322
+ end
323
+ else
324
+ example.run
325
+ end
326
+ end
327
+ end
328
+
329
+ # In a test
330
+ it 'has the settings I want', ultra_settings: {test: {foo: "bar"}} do
331
+ expect(UltraSettings.test.foo).to eq("bar")
332
+ end
333
+ ```
334
+
16
335
  ## Installation
17
336
 
18
337
  Add this line to your application's Gemfile:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1.rc1
1
+ 1.0.1
@@ -0,0 +1,85 @@
1
+ .ultra-settings-nav {
2
+ margin-bottom: 1rem;
3
+ }
4
+
5
+ .ultra-settings-nav form {
6
+ margin: 0;
7
+ }
8
+
9
+ .ultra-settings-table {
10
+ width: 100%;
11
+ max-width: 100%;
12
+ margin-bottom: 1rem;
13
+ border-collapse: collapse;
14
+ }
15
+
16
+ .ultra-settings-table thead th {
17
+ vertical-align: bottom;
18
+ border-bottom: 2px solid #dee2e6;
19
+ }
20
+
21
+ .ultra-settings-table td, .ultra-settings-table th {
22
+ padding: 0.75rem;
23
+ vertical-align: top;
24
+ border-top: 1px solid #dee2e6;
25
+ }
26
+
27
+ .ultra-settings-table tbody tr:nth-of-type(odd) {
28
+ background-color: rgba(0, 0, 0, .03);
29
+ }
30
+
31
+ .ultra-settings-code {
32
+ font-family: monospace;
33
+ font-size: 0.9rem;
34
+ display: inline;
35
+ }
36
+
37
+ .ultra-settings-static {
38
+ color: gray;
39
+ font-style: italic;
40
+ font-size: 0.9rem;
41
+ }
42
+
43
+ .ultra-settings-current-source {
44
+ font-weight: 600;
45
+ color: blue;
46
+ }
47
+
48
+ .ultra-settings-description {
49
+ font-size: 0.9rem;
50
+ color: gray;
51
+ }
52
+
53
+ .ultra-settings-info {
54
+ margin-bottom: 1rem;
55
+ }
56
+
57
+ .ultra-settings-error {
58
+ color: darkred;
59
+ }
60
+
61
+ .ultra-settings-not-applicable {
62
+ color: #999;
63
+ font-style: italic;
64
+ }
65
+
66
+ .ultra-settings-select {
67
+ display: inline-block;
68
+ padding: .375rem 2.25rem .375rem .75rem;
69
+ -moz-padding-start: calc(0.75rem - 3px);
70
+ font-size: 1rem;
71
+ font-weight: 400;
72
+ line-height: 1.5;
73
+ color: #212529;
74
+ background-color: #fff;
75
+ 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");
76
+ background-repeat: no-repeat;
77
+ background-position: right .75rem center;
78
+ background-size: 16px 12px;
79
+ border: 1px solid #ced4da;
80
+ border-radius: .25rem;
81
+ transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
82
+ -webkit-appearance: none;
83
+ -moz-appearance: none;
84
+ appearance: none;
85
+ }
@@ -0,0 +1,19 @@
1
+ document.addEventListener("DOMContentLoaded", () => {
2
+ const menu = document.getElementById("config-selector");
3
+
4
+ showCurrentConfiguration = () => {
5
+ const selectedId = menu.options[menu.selectedIndex].value;
6
+
7
+ document.querySelectorAll(".ultra-settings-configuration").forEach((configuration) => {
8
+ if (configuration.id === selectedId) {
9
+ configuration.style.display = "block";
10
+ } else {
11
+ configuration.style.display = "none";
12
+ }
13
+ });
14
+ }
15
+
16
+ menu.addEventListener("change", showCurrentConfiguration);
17
+
18
+ showCurrentConfiguration();
19
+ });
@@ -0,0 +1,93 @@
1
+ <div class="ultra-settings-nav">
2
+ <form onsubmit="return false">
3
+ <select class="ultra-settings-select" size="1" id="config-selector">
4
+ <% UltraSettings.__configuration_names__.sort.each do |name| %>
5
+ <option value="config-<%= name %>"><%= name %></option>
6
+ <% end %>
7
+ </select>
8
+ </form>
9
+ </div>
10
+
11
+ <% UltraSettings.__configuration_names__.sort.each do |name| %>
12
+ <% configuration = UltraSettings.send(name) %>
13
+
14
+ <div class="ultra-settings-configuration" id="config-<%= name %>" style="display:none;">
15
+ <% unless configuration.class.yaml_config_disabled? %>
16
+ <div class="ultra-settings-info">
17
+ YAML File:
18
+ <span class="ultra-settings-code <%= 'ultra-settings-error' unless configuration.class.configuration_file %>">
19
+ <%= configuration.class.configuration_file.to_s.sub(/\A#{Regexp.escape(UltraSettings::Configuration.yaml_config_path.to_s)}\//, "") %>
20
+ </span>
21
+ </div>
22
+ <% end %>
23
+
24
+ <table class="ultra-settings-table">
25
+ <thead>
26
+ <tr>
27
+ <th>Name</th>
28
+ <th>Type</th>
29
+ <th>Environment Variable</th>
30
+ <th>Runtime Setting</th>
31
+ <th>YAML Key</th>
32
+ <th>Default</th>
33
+ </tr>
34
+ </thead>
35
+ <tbody>
36
+ <% configuration.class.fields.sort_by(&:name).each do |field| %>
37
+ <tr>
38
+ <td>
39
+ <%= field.name %>
40
+ <% unless field.description.to_s.empty? %>
41
+ <div class="ultra-settings-description">
42
+ <%= field.description %>
43
+ </div>
44
+ <% end %>
45
+ </td>
46
+ <td>
47
+ <%= field.type %>
48
+ <% if field.static? %>
49
+ <div class="ultra-settings-static">
50
+ static
51
+ </div>
52
+ <% end %>
53
+ </td>
54
+ <td>
55
+ <% if field.env_var && !configuration.class.environment_variables_disabled? %>
56
+ <pre class="ultra-settings-code <%= 'ultra-settings-current-source' if configuration.__source__(field.name) == :env %>"><%= field.env_var %></pre>
57
+ <% else %>
58
+ <span class="ultra-settings-not-applicable">n/a</span>
59
+ <% end %>
60
+ </td>
61
+ <td>
62
+ <% if field.runtime_setting && !configuration.class.runtime_settings_disabled? %>
63
+ <pre class="ultra-settings-code <%= 'ultra-settings-current-source' if configuration.__source__(field.name) == :settings %>"><%= field.runtime_setting %></pre>
64
+ <% else %>
65
+ <span class="ultra-settings-not-applicable">n/a</span>
66
+ <% end %>
67
+ </td>
68
+ <td>
69
+ <% if field.yaml_key && !configuration.class.yaml_config_disabled? %>
70
+ <pre class="ultra-settings-code <%= 'ultra-settings-current-source' if configuration.__source__(field.name) == :yaml %>"><%= field.yaml_key %></pre>
71
+ <% else %>
72
+ <span class="ultra-settings-not-applicable">n/a</span>
73
+ <% end %>
74
+ </td>
75
+ <td>
76
+ <span class="<%= 'ultra-settings-current-source' if configuration.__source__(field.name) == :default %>">
77
+ <% if field.default.nil? %>
78
+ <em>nil</em>
79
+ <% else %>
80
+ &#x2714;
81
+ <% end %>
82
+ </span>
83
+ </td>
84
+ </tr>
85
+ <% end %>
86
+ </tbody>
87
+ </table>
88
+ </div>
89
+ <% end %>
90
+
91
+ <script>
92
+ <%= @javascript %>
93
+ </script>
data/app/layout.css ADDED
@@ -0,0 +1,27 @@
1
+ * {box-sizing:border-box;}
2
+
3
+ body {
4
+ font-family: sans-serif;
5
+ font-size: 1rem;
6
+ line-height: 1.5;
7
+ text-align: left;
8
+ color: #212529;
9
+ background-color: #ffffff;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+
14
+ header {
15
+ background-color: #666;
16
+ color: white;
17
+ padding: 1rem;
18
+ margin-bottom: 1rem;
19
+ }
20
+
21
+ header h1 {
22
+ margin: 0;
23
+ }
24
+
25
+ main {
26
+ margin: 1rem;
27
+ }
@@ -0,0 +1,21 @@
1
+ <!DOCTYPE html>
2
+
3
+ <html>
4
+ <head>
5
+ <title>Application Settings</title>
6
+ <meta charset="utf-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <style type="text/css">
9
+ <%= @layout_css %>
10
+ <%= css %>
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <header>
15
+ <h1>Application Settings</h1>
16
+ </header>
17
+ <main>
18
+ <%= content %>
19
+ </main>
20
+ </body>
21
+ </html>