super_settings 0.0.0.rc1 → 0.0.1.rc2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2 -1
- data/README.md +65 -62
- data/VERSION +1 -1
- data/config/routes.rb +3 -1
- data/lib/super_settings/application/api.js +8 -1
- data/lib/super_settings/application/helper.rb +2 -1
- data/lib/super_settings/application/index.html.erb +3 -3
- data/lib/super_settings/application/scripts.js +50 -12
- data/lib/super_settings/application/styles.css +5 -0
- data/lib/super_settings/application.rb +3 -1
- data/lib/super_settings/coerce.rb +6 -2
- data/lib/super_settings/configuration.rb +34 -15
- data/lib/super_settings/engine.rb +0 -5
- data/lib/super_settings/history_item.rb +10 -2
- data/lib/super_settings/local_cache.rb +14 -3
- data/lib/super_settings/{rack_middleware.rb → rack_application.rb} +66 -36
- data/lib/super_settings/rest_api.rb +98 -97
- data/lib/super_settings/setting.rb +96 -73
- data/lib/super_settings/storage/active_record_storage.rb +28 -10
- data/lib/super_settings/storage/http_storage.rb +7 -17
- data/lib/super_settings/storage/redis_storage.rb +20 -33
- data/lib/super_settings/storage/test_storage.rb +3 -10
- data/lib/super_settings/storage.rb +49 -24
- data/lib/super_settings.rb +41 -20
- data/super_settings.gemspec +1 -3
- metadata +8 -24
- data/lib/super_settings/encryption.rb +0 -76
- data/lib/tasks/super_settings.rake +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a9a76ec61662ea71965541177f828db81a6db13a2808bbdaca9d823053dd33a0
|
4
|
+
data.tar.gz: b6980aa31511e88dae2e9034a82cb6d514e8cf5b1d5b8b54439819dd5d5e0a62
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd24a62c276574dd407f32534964ec019d00813570abaf8b799a57f7b82a53828ac9ee572e1efeceb5b2f4a4ca83cfaa74b6da2f5f0bb3317fe3ee785ddc00d8
|
7
|
+
data.tar.gz: 3b66ffe10504f1de430aea5fe7a5b0c3d529e7374e974c3f61e92b660de4896466cb33531a7d0acf7d97881917e8ea68be186955c88470c4e1a2e17ae8404ecc
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,7 @@ 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
|
-
## [
|
7
|
+
## [1.0.0]
|
8
|
+
|
8
9
|
### Added
|
9
10
|
- Everything!
|
data/README.md
CHANGED
@@ -3,39 +3,39 @@
|
|
3
3
|
[![Continuous Integration](https://github.com/bdurand/super_settings/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/super_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
|
-
This gem provides a framework for maintaining runtime application settings. Settings are persisted in a database
|
6
|
+
This gem provides a framework for maintaining runtime application settings. Settings are persisted in a database but cached in memory for quick, efficient access. The settings are designed so they can be updated dynamically without requiring code deployment or restarting processes. The code scales very well and can easily handle very high throughput environments.
|
7
7
|
|
8
|
-
As applications grow, they tend to accumulate
|
8
|
+
As applications grow, they tend to accumulate many configuration options. Often these end up in environment variables, hard coded in YAML files, or sprinkled through various data models as additional columns. All of these methods of configuration have their place and are completely appropriate for different purposes (i.e. for storing application secrets, configuration required during application startup, etc.).
|
9
9
|
|
10
10
|
However, these methods don't work as well for runtime settings that you may want to change while your application is running.
|
11
11
|
|
12
|
-
|
12
|
+
- **Environment variables** - These are great for environment-specific configuration and they can be a good place to store sensitive data. However, they can be difficult to manage. All values must be stored as strings, and application processes need to be restarted for changes to take effect.
|
13
13
|
|
14
|
-
|
14
|
+
- **YAML files** - These are great for more complex configurations because they support data structures and they can be shipped with your application code. However, changing them usually requires a new release of the application.
|
15
15
|
|
16
|
-
|
16
|
+
- **Database columns** - These are great for settings tied to data models, however, they don't apply very well outside the data model, and you need to build the tools for managing them into your application.
|
17
17
|
|
18
|
-
SuperSettings provides a simple interface for accessing settings backed by a thread
|
18
|
+
SuperSettings provides a simple interface for accessing settings backed by a thread-safe caching mechanism, which provides in-memory performance while significantly limiting any load on the database. You can tune how frequently the cache is refreshed and each refresh call is tuned to be highly efficient.
|
19
19
|
|
20
|
-
There is also an out of the box
|
20
|
+
There is also an out of the box Web UI and REST API for managing settings. You can specify data types for your settings (string, integer, float, boolean, datetime, or array) to ensure that values will be valid. You can also supply documentation for each setting so that it's clear what each one does and how it is used.
|
21
21
|
|
22
22
|
## Usage
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
24
|
+
- [Getting Value](#getting-values)
|
25
|
+
- [Hashes](#hashes)
|
26
|
+
- [Defaults](#defaults)
|
27
|
+
- [Caching](#caching)
|
28
|
+
- [Data Model](#data-model)
|
29
|
+
- [Storage Engines](#storage-engines)
|
30
|
+
- [Web UI](#web-ui)
|
31
|
+
- [REST API](#rest-api)
|
32
|
+
- [Authentication](#authentication)
|
33
|
+
- [Rails Engine](#rails-engine)
|
34
|
+
- [Configuration](#configuration)
|
35
35
|
|
36
36
|
### Getting Values
|
37
37
|
|
38
|
-
This gem is in essence a key/value store. Settings are identified by unique keys and contain a typed value. You can access setting values using methods on the `SuperSettings` object.
|
38
|
+
This gem is, in essence, a key/value store. Settings are identified by unique keys and contain a typed value. You can access setting values using methods on the `SuperSettings` object.
|
39
39
|
|
40
40
|
```ruby
|
41
41
|
SuperSettings.get("key") # -> returns a string
|
@@ -52,7 +52,7 @@ SuperSettings.array("key") # -> returns an array of strings
|
|
52
52
|
```
|
53
53
|
|
54
54
|
#### Hashes
|
55
|
-
There is also a method to get multiple settings at once structured as a
|
55
|
+
There is also a method to get multiple settings at once structured as a hash.
|
56
56
|
|
57
57
|
```ruby
|
58
58
|
SuperSettings.structured("parent") # -> returns an hash
|
@@ -101,7 +101,7 @@ SuperSettings.structured
|
|
101
101
|
# "page_size" => 20
|
102
102
|
# }
|
103
103
|
|
104
|
-
# Limit the depth of the returned
|
104
|
+
# Limit the nesting depth of the returned hash to one level
|
105
105
|
SuperSettings.structured(max_depth: 1)
|
106
106
|
# {
|
107
107
|
# "vendors.company_1.path => "/co1",
|
@@ -122,9 +122,9 @@ SuperSettings.integer("key", 4)
|
|
122
122
|
|
123
123
|
#### Caching
|
124
124
|
|
125
|
-
When you read a setting using these methods, you are actually reading from an in
|
125
|
+
When you read a setting using these methods, you are actually reading from an in-memory cache. All of the settings are read into this local cache and checked periodically to see if the cache needs to be refreshed (defaults to every five seconds, but can be customized with `SuperSettings.refresh_interval`). When the cache needs to be refreshed, only the delta of updated records are re-read from the data store by a single background thread to minimize any load on the server.
|
126
126
|
|
127
|
-
Cache misses are also cached so
|
127
|
+
Cache misses are also cached so they don't add any overhead. Because of this, you should avoid using dynamically generated values as keys since this can lead to memory bloat.
|
128
128
|
|
129
129
|
```ruby
|
130
130
|
# BAD: this will create an entry in the cache for every id
|
@@ -137,17 +137,17 @@ SuperSettings.array("enabled_users", []).include?(id)
|
|
137
137
|
SuperSettings.structured("enabled_users", {})["id"]
|
138
138
|
```
|
139
139
|
|
140
|
-
|
140
|
+
The cache will scale without issue to handle hundreds of settings. However, you should avoid creating thousands of settings. Because all settings are read into memory, having too many settings records can lead to performance or memory issues.
|
141
141
|
|
142
142
|
### Data Model
|
143
143
|
|
144
|
-
Each setting has a
|
144
|
+
Each setting has a key, value, value type, and optional description. The key must be unique. The value type can be one of "string", "integer", "float", "boolean", "datetime", or "array". The array value type will always return an array of strings.
|
145
145
|
|
146
|
-
|
146
|
+
You can request a setting using one of the accessor methods on `SuperSettings` regardless of its defined value type. For instance, you can call `SuperSettings.get("integer_key")` on an integer setting and it will return the value as a string. The value type of a setting is only used for validating input values and does not limit how you can request the value at runtime.
|
147
147
|
|
148
|
-
It is not possible to store an empty string in a setting; empty strings will be always
|
148
|
+
It is not possible to store an empty string in a setting; empty strings will be always be returned as `nil`.
|
149
149
|
|
150
|
-
A history of all settings changes is
|
150
|
+
A history of all settings changes is updated every time the value is changed in the `histories` association. You can also record who made the changes.
|
151
151
|
|
152
152
|
#### Storage Engines
|
153
153
|
|
@@ -155,35 +155,21 @@ This gem abstracts out the storage engine and can support multiple storage mecha
|
|
155
155
|
|
156
156
|
* `SuperSettings::Storage::ActiveRecordStorage` - Stores the settings in a relational database using ActiveRecord. This is the default storage engine for Rails applications.
|
157
157
|
* `SuperSettings::Storage::RedisStorage` - Stores the settings in a Redis database using the [redis](https://github.com/redis/redis-rb) gem.
|
158
|
-
* `SuperSettings::Storage::HttpStorage` - Uses the SuperSettings REST API running on another server. This is useful in a
|
158
|
+
* `SuperSettings::Storage::HttpStorage` - Uses the SuperSettings REST API running on another server. This is useful in a microservices architecture so you can have a central settings server used by all the services.
|
159
159
|
|
160
|
-
Additional storage engines can be built by creating a class that includes `SuperSettings::Storage` and
|
160
|
+
Additional storage engines can be built by creating a class that includes `SuperSettings::Storage` and implements the unimplemented methods in that module.
|
161
161
|
|
162
|
-
The storage engine is defined by setting `SuperSettings::Setting.storage` to
|
163
|
-
|
164
|
-
#### Encrypted Secrets
|
165
|
-
|
166
|
-
You can specify that a setting is a secret by setting the value type to "secret". This will obscure the value in the UI (thoough it can still be seen when editing) as well as avoid recording the values in the setting history.
|
167
|
-
|
168
|
-
You can also specify an encryption secret that is used to encrypt these settings in the database. It is highly recommended that if you store secrets in your settings that you enable this feature. The enryption secret can either be set by setting `SuperSettings.secret` or by setting the `SUPER_SETTINGS_SECRET` environment variable.
|
169
|
-
|
170
|
-
If you need to roll your secret key, you can set the `SuperSettings.secret` value as an array (or as a space delmited list in the environment variable). The first secret will be the one used to encrypt values. However, all the secrets will be tried when decrypting values. This allows you to change the secret without raising decryption errors. If you do change your secret, you can run this rake task to re-encrypt all values using the new secret:
|
171
|
-
|
172
|
-
```bash
|
173
|
-
rake super_settings:encrypt_secrets
|
174
|
-
```
|
175
|
-
|
176
|
-
Encryption only changes how values are stored in the data store. Encrypted secrets are protected from someone gaining direct access to your database or a database backup and should be used if you are storing sensitive values. However, the values are not encrypted in the REST API or web UI. You must take appropriate measures to secure these if you choose to use them.
|
162
|
+
The storage engine is defined by setting `SuperSettings::Setting.storage` to a storage class. Note that each storage class may also require additional configuration. For instance, the Redis storage class requires you to provide a connection to a Redis database. If you are running a Rails application, then the storage engine will be set to ActiveRecord by default. Otherwise, you will need to define the storage class somewhere in your application's initialization.
|
177
163
|
|
178
164
|
### Web UI
|
179
165
|
|
180
166
|
The Web UI provides all the functionality to add, update, and delete settings.
|
181
167
|
|
182
|
-
![Web UI](web_ui.png)
|
168
|
+
![Web UI](assets/web_ui.png)
|
183
169
|
|
184
|
-
You can save multiple settings at once. If you have settings that need to be changed together, you can
|
170
|
+
You can save multiple settings at once. If you have settings that need to be changed together, you can ensure they will all be saved in a single transaction.
|
185
171
|
|
186
|
-
The Web UI is fully self
|
172
|
+
The Web UI is fully self-contained and has no external dependencies. There are configuration settings for tweaking the layout. See the `SuperSettings::Configuration` class for details if you are using Rails or `SuperSettings::RackApplication` if you are not.
|
187
173
|
|
188
174
|
You can see the Web UI in action if you clone this repository and then run:
|
189
175
|
|
@@ -193,17 +179,39 @@ bin/start_rails
|
|
193
179
|
|
194
180
|
Then go to http://localhost:3000/settings in your browser.
|
195
181
|
|
196
|
-
You can change the layout used by the
|
182
|
+
You can change the layout used by the Web UI. However, if you do this, you will be responsible for providing the CSS styles for the buttons, table rows, and form controls. The CSS class names used by the default layout are compatible with the class names defined in the [Bootstrap library](https://getbootstrap.com/).
|
197
183
|
|
198
|
-
It is not required to use the bundled
|
184
|
+
It is not required to use the bundled Web UI. You can implement your own UI using the `SuperSettings::Setting` model.
|
199
185
|
|
200
186
|
#### REST API
|
201
187
|
|
202
|
-
You can mount a REST API for
|
188
|
+
You can mount a REST API for exposing and managing the settings. This API is required for the Web UI and is mounted along with the Web UI. The REST interface is documented in the `SuperSettings::RestAPI` class.
|
189
|
+
|
190
|
+
If you are running a Rails application, you can mount the API as a controller via the bundled Rails engine. If you are not using Rails, then you can add a class that extends `SuperSettings::RackApplication` to your Rack middleware stack. The web UI can be disabled and only the REST API exposed. See `SuperSettings::Configuration` if you are using Rails or `SuperSettings::RackApplication` if you are not.
|
203
191
|
|
204
|
-
|
192
|
+
#### Authentication
|
205
193
|
|
206
|
-
|
194
|
+
You are responsible for implementing authentication on the Web UI and REST API endpoints. In a Rack application, you would do this by putting the Supersetting application behind Rack middleware the performs your authentication checks. In a Rails application, you can add a `before_action` filter to hook into your authentication checks.
|
195
|
+
|
196
|
+
If you are using access token authentication from a single-page application (as opposed to cookie-based authentication), you will need to pass the access token from the browser to the backend. There are a couple of built-in ways to do this.
|
197
|
+
|
198
|
+
You can pass the access token in either the `access_token` query parameter to the Web UI or as the URL hash. Both of these are equivalent:
|
199
|
+
|
200
|
+
```
|
201
|
+
https://myapp.example.com/settings?access_token=secrettokenstring
|
202
|
+
|
203
|
+
https://myapp.example.com/settings#access_token=secrettokenstring
|
204
|
+
```
|
205
|
+
|
206
|
+
If you use the above method, you would construct these URL's from a part of your application that already has the access token. The access token will be removed from the URL in the browser history and stored in the window's session storage so that it can be sent with each API request.
|
207
|
+
|
208
|
+
Alternatively, you can specify a piece of Javascript in `SuperSettings.web_ui_javascript` that will be injected into the Web UI. You can use this to set whatever authentication header you need to on the API requests in the `SuperSettingsAPI.headers` Javascript object.
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
SuperSettings.web_ui_javascript = "SuperSettingsAPI.headers['Authorization'] = window.localStorage.getItem('access_token')"
|
212
|
+
```
|
213
|
+
|
214
|
+
You can also specify the URL for a login page with `SuperSettings.authentication_url`. Browsers will be redirected to this URL if a request requiring authentication is received.
|
207
215
|
|
208
216
|
### Rails Engine
|
209
217
|
|
@@ -231,18 +239,15 @@ You can configure various aspects of the Rails engine using by calling `SuperSet
|
|
231
239
|
# config/initializers/super_settings.rb
|
232
240
|
|
233
241
|
SuperSettings.configure do |config|
|
234
|
-
# These options can be used to customize the header in the
|
242
|
+
# These options can be used to customize the header in the Web UI.
|
235
243
|
config.controller.application_name = "My Application"
|
236
244
|
config.controller.application_link = "/"
|
237
245
|
config.controller.application_logo = "/images/app_logo.png"
|
238
246
|
|
239
247
|
# Set a custom refresh interval for the cache (default is 5 seconds)
|
240
|
-
config.refresh_interval =
|
248
|
+
config.refresh_interval = 10
|
241
249
|
|
242
|
-
# Set
|
243
|
-
config.secret = "ad962cc27e02657795a61b8d48a31ce4"
|
244
|
-
|
245
|
-
# Set the superclass to use for the controll. Defaults to using `ApplicationController`.
|
250
|
+
# Set the superclass to use for the controller. Defaults to using `ApplicationController`.
|
246
251
|
config.controller.superclass = Admin::BaseController
|
247
252
|
|
248
253
|
# Add additional code to the controller. In this case we are adding code to ensure only
|
@@ -259,7 +264,7 @@ SuperSettings.configure do |config|
|
|
259
264
|
def require_admin
|
260
265
|
if current_user.nil?
|
261
266
|
redirect_to login_url, status: 401
|
262
|
-
|
267
|
+
elsif !current_user.admin?
|
263
268
|
redirect_to access_denied_url, status: 403
|
264
269
|
end
|
265
270
|
end
|
@@ -282,8 +287,6 @@ SuperSettings.configure do |config|
|
|
282
287
|
end
|
283
288
|
```
|
284
289
|
|
285
|
-
One configuration you will probably want to set is the superclass for the controller. By default, the base `ApplicationController` defined for your application will be used. However, if you want to provide Your application probably already has a
|
286
|
-
|
287
290
|
## Installation
|
288
291
|
|
289
292
|
Add this line to your application's Gemfile:
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.1.rc2
|
data/config/routes.rb
CHANGED
@@ -2,7 +2,9 @@
|
|
2
2
|
|
3
3
|
SuperSettings::Engine.routes.draw do
|
4
4
|
controller :settings do
|
5
|
-
|
5
|
+
if SuperSettings::Configuration.instance.controller.web_ui_enabled?
|
6
|
+
get "/", action: :root, as: :root
|
7
|
+
end
|
6
8
|
get "/settings", action: :index
|
7
9
|
post "/settings", action: :update
|
8
10
|
get "/setting", action: :show
|
@@ -36,7 +36,12 @@
|
|
36
36
|
params = options.params
|
37
37
|
let queryParams = null;
|
38
38
|
const fetchOptions = {credentials: "same-origin"};
|
39
|
-
const
|
39
|
+
const accessToken = window.sessionStorage.getItem("super_settings_access_token");
|
40
|
+
const headers = {"Accept": "application/json"};
|
41
|
+
if (accessToken) {
|
42
|
+
headers["Authorization"] = "Bearer " + accessToken;
|
43
|
+
}
|
44
|
+
Object.assign(headers, SuperSettingsAPI.headers);
|
40
45
|
if (method === "POST") {
|
41
46
|
queryParams = Object.assign({}, SuperSettingsAPI.queryParams);
|
42
47
|
csrfParam = document.querySelector("meta[name=csrf-param]");
|
@@ -59,6 +64,8 @@
|
|
59
64
|
function(response) {
|
60
65
|
if (response.ok) {
|
61
66
|
return response.json();
|
67
|
+
} else if ((response.status === 401 || response.status === 403) && SuperSettingsAPI.authenticationUrl) {
|
68
|
+
window.location = SuperSettingsAPI.authenticationUrl;
|
62
69
|
} else {
|
63
70
|
throw( response.status + response.statusText)
|
64
71
|
}
|
@@ -31,7 +31,8 @@ module SuperSettings
|
|
31
31
|
<script>
|
32
32
|
#{File.read(File.join(__dir__, "scripts.js"))}
|
33
33
|
#{File.read(File.join(__dir__, "api.js"))}
|
34
|
-
#{
|
34
|
+
#{"SuperSettingsAPI.authenticationUrl = '#{SuperSettings.authentication_url.gsub("'", "\\'")}';" if SuperSettings.authentication_url}
|
35
|
+
#{SuperSettings.web_ui_javascript}
|
35
36
|
</script>
|
36
37
|
HTML
|
37
38
|
end
|
@@ -54,7 +54,7 @@
|
|
54
54
|
</td>
|
55
55
|
|
56
56
|
<td class="super-settings-value">
|
57
|
-
<div class="js-value-placeholder"></div>
|
57
|
+
<div class="js-value-placeholder super-settings-max-height-text"></div>
|
58
58
|
</td>
|
59
59
|
|
60
60
|
<td class="super-settings-value-type">
|
@@ -62,7 +62,7 @@
|
|
62
62
|
</td>
|
63
63
|
|
64
64
|
<td class="super-settings-description">
|
65
|
-
<div class="js-value-placeholder"></div>
|
65
|
+
<div class="js-value-placeholder super-settings-max-height-text"></div>
|
66
66
|
</td>
|
67
67
|
|
68
68
|
<td class="super-settings-controls">
|
@@ -136,7 +136,7 @@
|
|
136
136
|
<label for="settings_{{id}}_value_time" class="super-settings-sr-only">Time</label>
|
137
137
|
<input type="time" id="settings_{{id}}_value_time" name="_settings[{{id}}][time]" value="" class="form-control js-time-input" aria-label="Time">
|
138
138
|
<input type="hidden" name="settings[{{id}}][value]" value="" class="js-setting-value">
|
139
|
-
<small class="text-muted">Time
|
139
|
+
<small class="text-muted">Time Zone: <span class="timezone"></span></small>
|
140
140
|
</span>
|
141
141
|
</template>
|
142
142
|
|
@@ -45,16 +45,10 @@
|
|
45
45
|
} else if (setting.value_type === "datetime") {
|
46
46
|
try {
|
47
47
|
const datetime = new Date(Date.parse(setting.value));
|
48
|
-
element.innerText =
|
48
|
+
element.innerText = dateFormatter().format(datetime);
|
49
49
|
} catch (e) {
|
50
50
|
element.innerText = "" + setting.value
|
51
51
|
}
|
52
|
-
} else if (setting.value_type === "secret") {
|
53
|
-
let placeholder = "••••••••••••••••••••••••";
|
54
|
-
if (!setting.encrypted) {
|
55
|
-
placeholder += '<br><span class="text-danger">not encrypted</span>'
|
56
|
-
}
|
57
|
-
element.innerHTML = placeholder;
|
58
52
|
} else {
|
59
53
|
element.innerText = "" + setting.value
|
60
54
|
}
|
@@ -165,8 +159,8 @@
|
|
165
159
|
} else if (setting.value_type === "datetime") {
|
166
160
|
try {
|
167
161
|
const datetime = new Date(Date.parse(setting.value));
|
168
|
-
const isoDate = `${datetime.
|
169
|
-
const isoTime = `${padTimeVal(datetime.
|
162
|
+
const isoDate = `${datetime.getFullYear()}-${padTimeVal(datetime.getMonth() + 1)}-${padTimeVal(datetime.getDate())}`;
|
163
|
+
const isoTime = `${padTimeVal(datetime.getHours())}:${padTimeVal(datetime.getMinutes())}:${padTimeVal(datetime.getSeconds())}`;
|
170
164
|
element.querySelector('input[type="date"]').value = isoDate;
|
171
165
|
element.querySelector('input[type="time"]').value = isoTime;
|
172
166
|
element.querySelector(".js-setting-value").value = datetime.toUTCString().replace("GMT", "UTC");
|
@@ -219,6 +213,12 @@
|
|
219
213
|
row.dataset.newrecord = "true";
|
220
214
|
}
|
221
215
|
|
216
|
+
const timezone = row.querySelector(".timezone");
|
217
|
+
if (timezone) {
|
218
|
+
tzName = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
219
|
+
timezone.innerText = tzName;
|
220
|
+
}
|
221
|
+
|
222
222
|
return row
|
223
223
|
}
|
224
224
|
|
@@ -352,6 +352,17 @@
|
|
352
352
|
}
|
353
353
|
}
|
354
354
|
|
355
|
+
function dateFormatter() {
|
356
|
+
return new Intl.DateTimeFormat(navigator.language, {
|
357
|
+
month: "short",
|
358
|
+
day: "numeric",
|
359
|
+
year: "numeric",
|
360
|
+
hour: "numeric",
|
361
|
+
minute: "numeric",
|
362
|
+
second: "numeric",
|
363
|
+
timeZoneName: "short"
|
364
|
+
});
|
365
|
+
}
|
355
366
|
// Render a setting's history in a table.
|
356
367
|
function renderHistoryTable(parent, payload) {
|
357
368
|
parent.innerHTML = document.querySelector("#setting-history-table").innerHTML.trim();
|
@@ -359,9 +370,10 @@
|
|
359
370
|
const tbody = parent.querySelector("tbody");
|
360
371
|
let rowsHTML = "";
|
361
372
|
payload.histories.forEach(function(history) {
|
362
|
-
const date =
|
373
|
+
const date = new Date(Date.parse(history.created_at));
|
374
|
+
const dateString = dateFormatter().format(date);
|
363
375
|
const value = (payload.encrypted ? "<em>n/a</em>" : escapeHTML(history.value));
|
364
|
-
rowsHTML += `<tr><td class="super-settings-text-nowrap">${escapeHTML(
|
376
|
+
rowsHTML += `<tr><td class="super-settings-text-nowrap">${escapeHTML(dateString)}</td><td>${escapeHTML(history.changed_by)}</td><td>${value}</td></tr>`;
|
365
377
|
});
|
366
378
|
tbody.insertAdjacentHTML("beforeend", rowsHTML);
|
367
379
|
|
@@ -516,7 +528,8 @@
|
|
516
528
|
if (timeValue === "") {
|
517
529
|
timeValue = "00:00:00";
|
518
530
|
}
|
519
|
-
|
531
|
+
const date = new Date(Date.parse(dateValue + "T" + timeValue));
|
532
|
+
parentNode.querySelector(".js-setting-value").value = date.toISOString();
|
520
533
|
}
|
521
534
|
|
522
535
|
// Listener for the add setting button.
|
@@ -629,6 +642,29 @@
|
|
629
642
|
window.location = url;
|
630
643
|
}
|
631
644
|
|
645
|
+
// Support integration into single page applications where OAuth2 access tokens are used.
|
646
|
+
// The access token can be passed either in the access_token query parameter per the
|
647
|
+
// OAuth2 standard, or in the URL hash. Passing it in the hash will prevent it from ever
|
648
|
+
// being sent to the backend and is a bit more secure since there's no chance a web server
|
649
|
+
// will accidentally log it with the request URL.
|
650
|
+
function storeAccessToken() {
|
651
|
+
let accessToken = null;
|
652
|
+
const params = new URLSearchParams(window.location.search);
|
653
|
+
if (params.get("access_token")) {
|
654
|
+
accessToken = params.get("access_token");
|
655
|
+
}
|
656
|
+
if (window.location.hash.startsWith("#access_token=")) {
|
657
|
+
accessToken = window.location.hash.replace("#access_token=", "");
|
658
|
+
}
|
659
|
+
if (accessToken) {
|
660
|
+
window.sessionStorage.setItem("super_settings_access_token", accessToken);
|
661
|
+
const params = new URLSearchParams(window.location.search);
|
662
|
+
params.delete("access_token");
|
663
|
+
window.location.hash = null;
|
664
|
+
window.history.replaceState("", document.title, window.location.pathname + "?" + params.toString());
|
665
|
+
}
|
666
|
+
}
|
667
|
+
|
632
668
|
// Attach event listener to one or more elements.
|
633
669
|
function addListener(elements, event, handler) {
|
634
670
|
if (elements.addEventListener) {
|
@@ -702,6 +738,8 @@
|
|
702
738
|
let activeSettings = [];
|
703
739
|
|
704
740
|
docReady(function() {
|
741
|
+
storeAccessToken();
|
742
|
+
|
705
743
|
addListener(document.querySelector("#filter"), "input", filterListener);
|
706
744
|
addListener(document.querySelector("#add-setting"), "click", addSetting);
|
707
745
|
addListener(document.querySelector("#discard-changes"), "click", refreshPage);
|
@@ -8,7 +8,7 @@ module SuperSettings
|
|
8
8
|
include Helper
|
9
9
|
|
10
10
|
# @param layout [String, Symbol] path to an ERB template to use as the layout around the application UI. You can
|
11
|
-
# pass the symbol
|
11
|
+
# pass the symbol +:default+ to use the default layout that ships with the gem.
|
12
12
|
# @param add_to_head [String] HTML code to add to the <head> element on the page.
|
13
13
|
def initialize(layout = nil, add_to_head = nil)
|
14
14
|
if layout
|
@@ -19,6 +19,8 @@ module SuperSettings
|
|
19
19
|
end
|
20
20
|
|
21
21
|
# Render the specified ERB file in the lib/application directory distributed with the gem.
|
22
|
+
#
|
23
|
+
# @return [void]
|
22
24
|
def render(erb_file)
|
23
25
|
template = ERB.new(File.read(File.expand_path(File.join("application", erb_file), __dir__)))
|
24
26
|
html = template.result(binding)
|
@@ -18,6 +18,7 @@ module SuperSettings
|
|
18
18
|
|
19
19
|
class << self
|
20
20
|
# Cast variations of booleans (i.e. "true", "false", 1, 0, etc.) to actual boolean objects.
|
21
|
+
#
|
21
22
|
# @param value [Object]
|
22
23
|
# @return [Boolean]
|
23
24
|
def boolean(value)
|
@@ -31,6 +32,9 @@ module SuperSettings
|
|
31
32
|
end
|
32
33
|
|
33
34
|
# Cast a value to a Time object.
|
35
|
+
#
|
36
|
+
# @param value [Object]
|
37
|
+
# @return [Time]
|
34
38
|
def time(value)
|
35
39
|
value = nil if value.nil? || value.to_s.empty?
|
36
40
|
return nil if value.nil?
|
@@ -47,7 +51,7 @@ module SuperSettings
|
|
47
51
|
time
|
48
52
|
end
|
49
53
|
|
50
|
-
# @return true if the value is nil or empty.
|
54
|
+
# @return [Boolean] true if the value is nil or empty.
|
51
55
|
def blank?(value)
|
52
56
|
return true if value.nil?
|
53
57
|
if value.respond_to?(:empty?)
|
@@ -57,7 +61,7 @@ module SuperSettings
|
|
57
61
|
end
|
58
62
|
end
|
59
63
|
|
60
|
-
# @return true if the value is not nil and not empty.
|
64
|
+
# @return [Boolean] true if the value is not nil and not empty.
|
61
65
|
def present?(value)
|
62
66
|
!blank?(value)
|
63
67
|
end
|
@@ -17,13 +17,13 @@ module SuperSettings
|
|
17
17
|
|
18
18
|
# Superclass for the controller. This should normally be set to one of your existing
|
19
19
|
# base controller classes since these probably have authentication methods, etc. defined
|
20
|
-
# on them. If this is not defined, the superclass will be
|
20
|
+
# on them. If this is not defined, the superclass will be SuperSettings::ApplicationController.
|
21
21
|
# It can be set to either a class or a class name. Setting to a class name is preferrable
|
22
22
|
# since it will be compatible with class reloading in a development environment.
|
23
23
|
attr_writer :superclass
|
24
24
|
|
25
25
|
def superclass
|
26
|
-
if @superclass.is_a?(String)
|
26
|
+
if defined?(@superclass) && @superclass.is_a?(String)
|
27
27
|
@superclass.constantize
|
28
28
|
else
|
29
29
|
@superclass
|
@@ -39,27 +39,51 @@ module SuperSettings
|
|
39
39
|
# Optional URL for a link back to the rest of the application.
|
40
40
|
attr_accessor :application_link
|
41
41
|
|
42
|
+
# Optional URL for a link to the login page for the application.
|
43
|
+
def authentication_url=(value)
|
44
|
+
SuperSettings.authentication_url = value
|
45
|
+
end
|
46
|
+
|
42
47
|
# Javascript to inject into the settings application HTML page. This can be used, for example,
|
43
48
|
# to set authorization credentials stored client side to access the settings API.
|
44
|
-
|
49
|
+
def web_ui_javascript=(script)
|
50
|
+
SuperSettings.web_ui_javascript = script
|
51
|
+
end
|
52
|
+
|
53
|
+
# Enable or disable the web UI (the REST API will always be enabled).
|
54
|
+
attr_writer :web_ui_enabled
|
55
|
+
|
56
|
+
def web_ui_enabled?
|
57
|
+
unless defined?(@web_ui_enabled)
|
58
|
+
@web_ui_enabled = true
|
59
|
+
end
|
60
|
+
!!@web_ui_enabled
|
61
|
+
end
|
45
62
|
|
46
63
|
# Enhance the controller. You can define methods or call controller class methods like
|
47
|
-
#
|
64
|
+
# +before_action+, etc. in the block. These will be applied to the engine controller.
|
48
65
|
# This is essentially the same a monkeypatching the controller class.
|
66
|
+
#
|
67
|
+
# @yield Block of code to inject into the controller class.
|
49
68
|
def enhance(&block)
|
50
69
|
@enhancement = block
|
51
70
|
end
|
52
71
|
|
53
|
-
# Define how the
|
72
|
+
# Define how the +changed_by+ attibute on the setting history will be filled from the controller.
|
54
73
|
# The block will be evaluated in the context of the controller when the settings are changed.
|
55
74
|
# The value returned by the block will be stored in the changed_by attribute. For example, if
|
56
|
-
# your base controller class defines a method
|
57
|
-
# in the history, you could call
|
75
|
+
# your base controller class defines a method +current_user+ and you'd like the name to be stored
|
76
|
+
# in the history, you could call
|
77
|
+
#
|
78
|
+
# @example
|
79
|
+
# define_changed_by { current_user.name }
|
80
|
+
#
|
81
|
+
# @yield Block of code to call on the controller at request time
|
58
82
|
def define_changed_by(&block)
|
59
83
|
@changed_by_block = block
|
60
84
|
end
|
61
85
|
|
62
|
-
# Return the value of
|
86
|
+
# Return the value of +define_changed_by+ block.
|
63
87
|
#
|
64
88
|
# @api private
|
65
89
|
def changed_by(controller)
|
@@ -72,14 +96,14 @@ module SuperSettings
|
|
72
96
|
# Configuration for the models.
|
73
97
|
class Model
|
74
98
|
# Specify the cache implementation to use for caching the last updated timestamp for reloading
|
75
|
-
# changed records. Defaults to
|
99
|
+
# changed records. Defaults to Rails.cache
|
76
100
|
attr_accessor :cache
|
77
101
|
|
78
102
|
attr_writer :storage
|
79
103
|
|
80
104
|
# Specify the storage engine to use for persisting settings. The value can either be specified
|
81
105
|
# as a full class name or an underscored class name for a storage classed defined in the
|
82
|
-
#
|
106
|
+
# SuperSettings::Storage namespace. The default storage engine is +SuperSettings::Storage::ActiveRecord+.
|
83
107
|
def storage
|
84
108
|
if defined?(@storage) && @storage
|
85
109
|
@storage
|
@@ -110,11 +134,6 @@ module SuperSettings
|
|
110
134
|
# Return the controller specific configuration object.
|
111
135
|
attr_reader :controller
|
112
136
|
|
113
|
-
# Set the secret used for encrypting secret settings. Defaults to the value of the
|
114
|
-
# SUPER_SETTINGS_SECRET environment variable. An array can be provided if you need to
|
115
|
-
# roll the secret with the first value being the current one.
|
116
|
-
attr_accessor :secret
|
117
|
-
|
118
137
|
# Set the number of seconds that settings will be cached locally before the database
|
119
138
|
# is checked for updates. Defaults to 5 seconds.
|
120
139
|
attr_accessor :refresh_interval
|