ultra_settings 0.0.1.rc1 → 1.0.0

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: f277e747e90903a62b41f8e79074dcd6d8790f160eda187cb03a89f8fdf5df40
4
+ data.tar.gz: d6054c97b25ed971ec4601e522e5b7c6b1921a5d60d94e761b360b1f09bbc00c
5
5
  SHA512:
6
- metadata.gz: 1a9a7d61e85e0c0a3cd406535f33e978b1455cfcecb66cf8376ec3d76255540b35e950e39566c86b758004e3d80491d9267c4cccb6b5a889cd964d3b3119afe9
7
- data.tar.gz: e6f32cbc8d56496a7486ed05bd388e8ab93229b1df2ac016e56c9fac610dbc39ddd29c89967ae5f2ca1029f8d34919b1bbde451b31c4262adde44adde68dfa8f
6
+ metadata.gz: cbf68fab75a43227bcddb7b0db63c42090910a89f55a264c9fd3e731a0dfc1b05cdb834acd549e8215c76cc0b049f57b500e2d1b85081ae75e70efd1eb8a3e53
7
+ data.tar.gz: 13302c44130fa23473fddf517c768ef963ee505d9ddb2f71ad3bcd8d24261e2dfce562705490d553ef8b0883519485c615dca0c0c0dad1e47efbcf24ce2be49b
data/README.md CHANGED
@@ -1,18 +1,336 @@
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
4
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
5
5
 
6
- TODO
6
+ 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.
7
+
8
+ It allows you to define a hierarchy with three layers of sources for your configuration values:
9
+
10
+ 1. Environment variables
11
+ 2. Runtime settings (i.e. settings updatable from within the running application)
12
+ 3. YAML configuration files
13
+
14
+ 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.
15
+
16
+ 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.
17
+
18
+ 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:
19
+
20
+ - `String`
21
+ - `Integer`
22
+ - `Float`
23
+ - `Boolean`
24
+ - `Time`
25
+ - `Symbol`
26
+ - `Array<String>`
27
+
28
+ You can also define default values to be returned in case the configured value is missing or it fails to match a constraint.
29
+
30
+ Settings are accessed through singleton classes that you define.
7
31
 
8
32
  ## Usage
9
33
 
34
+ ### Defining Configurations
35
+
36
+ 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).
37
+
38
+ 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.
39
+
40
+ ```ruby
41
+ class MyServiceConfiguration < UltraSettings::Configuration
42
+ field :host, type: :string
43
+
44
+ field :port, type: :integer, default: 80
45
+
46
+ field :protocol, type: :string, default: "https"
47
+
48
+ field :timeout, type: :float, default: 1.0, default_if: ->(val) { val <= 0 }
49
+
50
+ field :auth_token,
51
+ type: :string,
52
+ env_var: "MY_SERVICE_TOKEN",
53
+ runtime_setting: false,
54
+ yaml_key: false,
55
+ description: "Bearer token for accessing the service"
56
+
57
+ # You aren't limited to just defining fields, you can define other
58
+ # helper methods to make using the configuration easier.
59
+ def uri
60
+ URI("#{protocol}://#{host}:#{port}")
61
+ end
62
+ end
63
+ ```
64
+
65
+ - You can specify a return type with the `:type` option. The value of the setting will be cast to this type. Valid types are:
66
+
67
+ - `:string` (the default)
68
+ - `:integer`
69
+ - `:float`
70
+ - `:boolean`
71
+ - `:datetime`
72
+ - `:symbol`
73
+ - `:array` (of strings)
74
+
75
+ - You can specify a default value with the `:default` option. Note that this value will still be cast to the defined type.
76
+
77
+ - 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.
78
+
79
+ - You can describe what your setting does with the `:description` option. This value is only for documentation purposes.
80
+
81
+ - 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.
82
+
83
+ - 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.
84
+
85
+ - 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.
86
+
87
+ - 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.
88
+
89
+ ### Environment Variables
90
+
91
+ Settings will first try to load values from environment variables. Environment variables are a good place to define environment specific values.
92
+
93
+ 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.
94
+
95
+ You can use lowercase environment variable names by setting `env_var_upcase` to `false` on your configuration class.
96
+
97
+ 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".
98
+
99
+ You can set an explicit prefix by setting `env_var_prefix` on your configuration class.
100
+
101
+ You can disable environment variables as a default source on your fields by setting `environment_variables_disabled` to `true` on your configuration class.
102
+
103
+ If a setting value cannot be loaded from an environment variable, then it's value will attempt to be loaded from a runtime setting.
104
+
105
+ ### Runtime Settings
106
+
107
+ 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.
108
+
109
+ 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.
110
+
111
+ ```ruby
112
+ class RedisRuntimeSettings
113
+ def initialize
114
+ @redis = Redis.new
115
+ end
116
+
117
+ def [](name)
118
+ @redis.get(name)
119
+ end
120
+ end
121
+
122
+ UltraSettings.runtime_settings = RedisRuntimeSettings.new
123
+ ```
124
+
125
+ 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.
126
+
127
+ ```ruby
128
+ UltraSettings.runtime_settings = SuperSettings
129
+ ```
130
+
131
+ 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.
132
+
133
+ You can use uppercase runtime setting names by setting `runtime_setting_upcase` to `true` on your configuration class.
134
+
135
+ 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".
136
+
137
+ You can set an explicit prefix by setting `runtime_setting_prefix` on your configuration class.
138
+
139
+ You can disable runtime settings as a default source on your fields by setting `runtime_settings_disabled` to `true` on your configuration class.
140
+
141
+ If a setting value cannot be loaded from the runtime settings, then it's value will attempt to be loaded from a YAML file.
142
+
143
+ ### YAML Files
144
+
145
+ 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.
146
+
147
+ 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.
148
+
149
+ You can specify an explicit YAML file to use by setting `configuration_file` on your configuration class to the path to the file.
150
+
151
+ You can disable YAML files as a default source on your fields by setting `yaml_config_disabled` to `true` on your configuration class.
152
+
153
+ 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.
154
+
155
+ 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.
156
+
157
+ So, for this YAML file:
158
+
159
+ ```yaml
160
+ shared:
161
+ timeout: 5
162
+ port: 8000
163
+
164
+ development:
165
+ timeout: 10
166
+ host: localhost
167
+
168
+ production:
169
+ host: prod.example.com
170
+ ```
171
+
172
+ The values for the "development" environment would be the combination of development and shared:
173
+
174
+ ```ruby
175
+ {
176
+ timeout: 10,
177
+ port: 8000,
178
+ host: "localhost"
179
+ }
180
+ ```
181
+
182
+ While for "production", the values would be the combination of production and shared:
183
+
184
+ ```ruby
185
+ {
186
+ timeout: 5,
187
+ port: 8000,
188
+ host: "prod.example.com"
189
+ }
190
+ ```
191
+
192
+ Values for the environment will always overwrite values from the shared hash.
193
+
194
+ 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.
195
+
196
+ ### Removing The Hierarchy
197
+
198
+ 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.
199
+
200
+ ```ruby
201
+ class MyServiceConfiguration < UtraSettings::Configuration
202
+ self.environment_variables_disabled = false
203
+ self.runtime_settings_disabled = false
204
+ self.yaml_config_disabled = false
205
+
206
+ field :host, yaml_key: "host"
207
+ field :token, env_var: "MY_SERVICE_TOKEN"
208
+ field :timeout, runtime_setting: "my_service.timeout", default: 5
209
+ end
210
+ ```
211
+
212
+ If you don't want the hierarchy in any configuration, then you can disable it globally.
213
+
214
+ ```ruby
215
+ UltraSettings.environment_variables_disabled = true
216
+ UltraSettings.runtime_settings_disabled = true
217
+ UltraSettings.yaml_config_disabled = true
218
+ ```
219
+
220
+ ### Accessing settings
221
+
222
+ Configurations are singleton objects. Settings are accessed by calling methods.
223
+
10
224
  ```ruby
225
+ MyServiceConfiguration.instance.host
226
+ ```
227
+
228
+ 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`).
229
+ ```ruby
230
+ UltraSettings.add(:my_service)
11
231
  UltraSettings.my_service.host
232
+ ```
233
+
234
+ You can also specify the class name to map to a different method name.
235
+ ```ruby
236
+ UltraSettings.add(:my, "MyServiceConfiguration")
237
+ UltraSettings.my.host
238
+ ```
239
+
240
+ In a Rails application, you could add some syntactic sugar and expose the `UltraSettings` object as a helper method in application.rb.
241
+
242
+ ```ruby
243
+ module MyApp
244
+ class Application < Rails::Application
245
+ def settings
246
+ UltraSettings
247
+ end
248
+ end
249
+ end
12
250
 
13
251
  Rails.application.settings.my_service.host
14
252
  ```
15
253
 
254
+ You can also keep things clean if your configuration is mostly accessed from within another class.
255
+
256
+ ```ruby
257
+ class MyService
258
+
259
+ # Reference the value as `settings.host`
260
+
261
+ private
262
+
263
+ def settings
264
+ MyServiceConfiguration.instance
265
+ end
266
+ end
267
+ ```
268
+
269
+ ### Web UI
270
+
271
+ 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.
272
+
273
+ ![Web UI](assets/web_ui.png)
274
+
275
+ Here is a simple example of how to mount in a Rails application behind HTTP Basic authentication with hard coded credentials.
276
+
277
+ ```ruby
278
+ # config/routes.rb
279
+
280
+ mount Rack::Builder.new do
281
+ use Rack::Auth::Basic do |username, password|
282
+ username == ENV.fetch("AUTH_USER") && password == ENV.fetch("AUTH_PASSWORD")
283
+ end
284
+ run UltraSettings::RackApp
285
+ end, at: "/ultra_settings"
286
+ ```
287
+
288
+ ### Testing
289
+
290
+ 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:
291
+
292
+ ```ruby
293
+ # Override a configuration added on the global namespace
294
+
295
+ UltraSettings.override!(test: {foo: "bar"}) do
296
+ expect(TestConfiguration.instance.foo).to eq "bar"
297
+ end
298
+
299
+ # or directly on the configuration class
300
+
301
+ TestConfiguration.override!(foo: "bar") do
302
+ expect(TestConfiguration.instance.foo).to eq "bar"
303
+ end
304
+
305
+ # or on the instance itself
306
+
307
+ TestConfiguration.instance.override!(foo: "bar") do
308
+ expect(TestConfiguration.instance.foo).to eq "bar"
309
+ end
310
+ ```
311
+
312
+ If you are using RSpec, you can set up a global before handler to make it easier to specify settings within your test blocks.
313
+
314
+ ```ruby
315
+ # RSpec setup
316
+ RSpec.configure do |config|
317
+ config.around do |example|
318
+ if example.metadata[:ultra_settings].is_a?(Hash)
319
+ UltraSettings.override!(example.metadata[:ultra_settings]) do
320
+ example.run
321
+ end
322
+ else
323
+ example.run
324
+ end
325
+ end
326
+ end
327
+
328
+ # In a test
329
+ it 'has the settings I want', ultra_settings: {test: {foo: "bar"}} do
330
+ expect(UltraSettings.test.foo).to eq("bar")
331
+ end
332
+ ```
333
+
16
334
  ## Installation
17
335
 
18
336
  Add this line to your application's Gemfile:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1.rc1
1
+ 1.0.0
@@ -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>