super_settings 0.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +313 -0
  5. data/VERSION +1 -0
  6. data/app/helpers/super_settings/settings_helper.rb +32 -0
  7. data/app/views/layouts/super_settings/settings.html.erb +20 -0
  8. data/config/routes.rb +13 -0
  9. data/db/migrate/20210414004553_create_super_settings.rb +34 -0
  10. data/lib/super_settings/application/api.js +88 -0
  11. data/lib/super_settings/application/helper.rb +119 -0
  12. data/lib/super_settings/application/images/edit.svg +1 -0
  13. data/lib/super_settings/application/images/info.svg +1 -0
  14. data/lib/super_settings/application/images/plus.svg +1 -0
  15. data/lib/super_settings/application/images/slash.svg +1 -0
  16. data/lib/super_settings/application/images/trash.svg +1 -0
  17. data/lib/super_settings/application/index.html.erb +169 -0
  18. data/lib/super_settings/application/layout.html.erb +22 -0
  19. data/lib/super_settings/application/layout_styles.css +193 -0
  20. data/lib/super_settings/application/scripts.js +718 -0
  21. data/lib/super_settings/application/styles.css +122 -0
  22. data/lib/super_settings/application.rb +38 -0
  23. data/lib/super_settings/attributes.rb +24 -0
  24. data/lib/super_settings/coerce.rb +66 -0
  25. data/lib/super_settings/configuration.rb +144 -0
  26. data/lib/super_settings/controller_actions.rb +81 -0
  27. data/lib/super_settings/encryption.rb +76 -0
  28. data/lib/super_settings/engine.rb +70 -0
  29. data/lib/super_settings/history_item.rb +26 -0
  30. data/lib/super_settings/local_cache.rb +306 -0
  31. data/lib/super_settings/rack_middleware.rb +210 -0
  32. data/lib/super_settings/rest_api.rb +195 -0
  33. data/lib/super_settings/setting.rb +599 -0
  34. data/lib/super_settings/storage/active_record_storage.rb +123 -0
  35. data/lib/super_settings/storage/http_storage.rb +279 -0
  36. data/lib/super_settings/storage/redis_storage.rb +293 -0
  37. data/lib/super_settings/storage/test_storage.rb +158 -0
  38. data/lib/super_settings/storage.rb +254 -0
  39. data/lib/super_settings/version.rb +5 -0
  40. data/lib/super_settings.rb +213 -0
  41. data/lib/tasks/super_settings.rake +9 -0
  42. data/super_settings.gemspec +35 -0
  43. metadata +113 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: db0bf91c463e76358f1c6b9721960e4dc2b08a25959c71ee1ffce819ff859874
4
+ data.tar.gz: 2c91970c4246df25e14f7f2928f15a449d5abb246442aff56d7fbbe5a98d27b9
5
+ SHA512:
6
+ metadata.gz: 7e18d007ff8754192e9165e4b2339e630123852b1d9a2e1be898af1f71a823f461977b7151e9935f4965691be73837ac07b5f5c66f96d15eff722e37283cd123
7
+ data.tar.gz: 7fd370930d84ab992824866d45220962baa51533f23101e847bf5a4a9a109a0b0dea0397a0c7e2692c57cd5d3bec9d5b4e1e3969d55f7ee1cd113fa8f1771587
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+ ### Added
9
+ - Everything!
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2021 Brian Durand
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,313 @@
1
+ # SuperSettings
2
+
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
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
5
+
6
+ This gem provides a framework for maintaining runtime application settings. Settings are persisted in a database and cached locally in memory for quick, efficient access. The settings are designed so they can be updated dynamically without requiring code deployment or restarting processes.
7
+
8
+ As applications grow, they tend to accumulate a lot of configuration over time. 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
+
10
+ However, these methods don't work as well for runtime settings that you may want to change while your application is running.
11
+
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
+
14
+ * YAML files - These are great for more complex configurations since they support data structures and they can be shipped with your application. However, changing them usually requires a new release of the application code.
15
+
16
+ * Database columns - These are great for settings tied to data models. However, they don't apply very well outside the data model, you need to build the tools for managing them into your application.
17
+
18
+ SuperSettings provides a simple interface for accessing settings backed by a thread safe caching mechanism that provides in-memory performance while significantly limiting database load. You can tune how frequently the cache is refreshed and each refresh call is tuned to be highly efficient.
19
+
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) and be assured that values will be valid. You can also supply documentation for each setting so that it's obvious what each one does and how it is used.
21
+
22
+ ## Usage
23
+
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
+ * [Encrypted Secrets](#encrypted_secrets)
31
+ * [Web UI](#web_ui)
32
+ * [REST API](#rest_api)
33
+ * [Rails Engine](#rails_engine)
34
+ * [Configuration](#configuration)
35
+
36
+ ### Getting Values
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.
39
+
40
+ ```ruby
41
+ SuperSettings.get("key") # -> returns a string
42
+
43
+ SuperSettings.integer("key") # -> returns an integer
44
+
45
+ SuperSettings.float("key") # -> returns a float
46
+
47
+ SuperSettings.enabled?("key") # -> returns a boolean
48
+
49
+ SuperSettings.datetime("key") # -> returns a `Time` object
50
+
51
+ SuperSettings.array("key") # -> returns an array of strings
52
+ ```
53
+
54
+ #### Hashes
55
+ There is also a method to get multiple settings at once structured as a Hash.
56
+
57
+ ```ruby
58
+ SuperSettings.structured("parent") # -> returns an hash
59
+ ```
60
+
61
+ The key provided to the `SuperSettings.structured` method indicates the key prefix and constructs the hash from settings that have keys beginning with that prefix. Keys are also broken down by a delimiter so you can create nested hashes. The delimiter defaults to `"."`, but you can specify a different one with the `delimiter` keyword argument.
62
+
63
+ You can also set a maximum depth to the returned hash with the `max_depth` keyword argument.
64
+
65
+ So, if you have the following settings:
66
+
67
+ ```
68
+ vendors.company_1.path = "/co1"
69
+ vendors.company_1.timeout = 5
70
+ vendors.company_2.path = "/co2"
71
+ page_size = 20
72
+ ```
73
+
74
+ You would get these results:
75
+
76
+ ```ruby
77
+ SuperSettings.structured("vendors")
78
+ # {
79
+ # "company_1" => {
80
+ # "path" => "/co1",
81
+ # "timeout" => 5
82
+ # },
83
+ # "company_2" => {
84
+ # "path" => "/co2"
85
+ # }
86
+ # }
87
+
88
+ SuperSettings.structured("vendors.company_1")
89
+ # {"path" => "/co1", "timeout" => 5}
90
+
91
+ SuperSettings.structured("vendors.company_2")
92
+ # {"path" => "/co2"}
93
+
94
+ # Get all the settings by omitting the key
95
+ SuperSettings.structured
96
+ # {
97
+ # "vendors" => {
98
+ # "company_1" => {"path" => "/co1", "timeout" => 5},
99
+ # "company_2" => {"path" => "/co2"}
100
+ # },
101
+ # "page_size" => 20
102
+ # }
103
+
104
+ # Limit the depth of the returned has to one level
105
+ SuperSettings.structured(max_depth: 1)
106
+ # {
107
+ # "vendors.company_1.path => "/co1",
108
+ # "vendors.company_1.timeout" => 5,
109
+ # "vendors.company_2.path" => "/co2",
110
+ # "page_size" => 20
111
+ # }
112
+ ```
113
+
114
+ #### Defaults
115
+
116
+ When you request a setting, you can also specify a default value to use if the setting does not have a value.
117
+
118
+ ```ruby
119
+ SuperSettings.integer("key", 4)
120
+ # return 4 if the "key" setting has not been set
121
+ ```
122
+
123
+ #### Caching
124
+
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 the cache is checked periodically to see if it needs to be refreshed (defaults to every five seconds, but can be customized with `SuperSettings.refresh_interval`). When the cache does need to be refreshed, only updated records are re-read from the data store by a single background thread. Thus, you don't have to worry about overloading your database by reading settings values.
126
+
127
+ Cache misses are also cached so that they don't add any overhead. You should avoid querying for dynamically generated values as a setting key since this can lead to memory bloat.
128
+
129
+ ```ruby
130
+ # BAD: this will create an entry in the cache for every id
131
+ SuperSettings.enabled?("enabled_users.#{id}")
132
+
133
+ # GOOD: use an array if there are a limited number of values
134
+ SuperSettings.array("enabled_users", []).include?(id)
135
+
136
+ # GOOD: use a hash if you need to scale to any number of values
137
+ SuperSettings.structured("enabled_users", {})["id"]
138
+ ```
139
+
140
+ Because all settings must be read into memory, you should avoid creating thousands of settings since this could lead to performance or memory issues loading the cache.
141
+
142
+ ### Data Model
143
+
144
+ Each setting has a unique key, a value, a value type, and an optional description. The value type can be one of "string", "integer", "float", "boolean", "datetime", "array", or "secret". The array value type will always return an array of strings. The secret value type also returns a string and is used to indicate that the value contains sensitive data that should not be exposed. Secret values can be encrypted in the database as well (see below).
145
+
146
+ The value type on a setting does not limit how it can be cast when request using one of the accessor methods on `SuperSettings`, though. For instance, you can call `SuperSettings.get("integer_key")` on an integer setting and it will return a string. The value type does ensure that the value is validated to be sure it can be cast to the specified type so you can avoid inputing invalid data.
147
+
148
+ It is not possible to store an empty string in a setting; empty strings will be always cast to `nil`.
149
+
150
+ A history of all settings changes is kept every time the value is changed in the `histories` association. You can use this information to see what values were in effect at what time. You can optionally alse record who made the changes.
151
+
152
+ #### Storage Engines
153
+
154
+ This gem abstracts out the storage engine and can support multiple storage mechanisms. It has built in support for ActiveRecord, Redis, and HTTP storage.
155
+
156
+ * `SuperSettings::Storage::ActiveRecordStorage` - Stores the settings in a relational database using ActiveRecord. This is the default storage engine for Rails applications.
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 micro services architecture so you can have a central settings server used by all the services.
159
+
160
+ Additional storage engines can be built by creating a class that includes `SuperSettings::Storage` and implementing the unimplemented methods in that module.
161
+
162
+ The storage engine is defined by setting `SuperSettings::Setting.storage` to the storage class to use. 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 it somewhere in your application's initialization.
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.
177
+
178
+ ### Web UI
179
+
180
+ The Web UI provides all the functionality to add, update, and delete settings.
181
+
182
+ ![Web UI](web_ui.png)
183
+
184
+ You can save multiple settings at once. If you have settings that need to be changed together, you can be assured they will all be saved in a single transaction.
185
+
186
+ 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
187
+
188
+ You can see the Web UI in action if you clone this repository and then run:
189
+
190
+ ```bash
191
+ bin/start_rails
192
+ ```
193
+
194
+ Then go to http://localhost:3000/settings in your browser.
195
+
196
+ 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 the 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
+
198
+ It is not required to use the bundled web UI. You can implement your own UI if you need to using the `SuperSettings::Setting` model.
199
+
200
+ #### REST API
201
+
202
+ You can mount a REST API for the exposing and managing the settings. This API is required for the web UI. The REST interface is documented in the `SuperSettings::RestAPI` class.
203
+
204
+ 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::RackMiddleware` to your Rack middleware stack.
205
+
206
+ In either case, you are responsible for implementing authentication and authorization for the HTTP requests. This allows you to seamlessly integrate with existing authentication and authorization in your application.
207
+
208
+ ### Rails Engine
209
+
210
+ The gem ships with a Rails engine that provides easy integration with a Rails application.
211
+
212
+ The default storage engine for a Rails application will be the ActiveRecord storage. You need to install the database migrations first with:
213
+
214
+ ```bash
215
+ rails app:super_settings:install:migrations
216
+ ```
217
+
218
+ You also need to mount the engine routes in your application's `config/routes.rb` file. The routes can be mounted under any prefix you'd like.
219
+
220
+ ```ruby
221
+ mount SuperSettings::Engine => "/settings"
222
+ ```
223
+
224
+ See the configuration section below for information about how to secure the controller endpoints. The engine provides no mechanism for security out of the box, but it is designed to seamlessly integrate with your application's existing authentication and authorization mechanism.
225
+
226
+ #### Configuration
227
+
228
+ You can configure various aspects of the Rails engine using by calling `SuperSettings.configure` in an initializer.
229
+
230
+ ```ruby
231
+ # config/initializers/super_settings.rb
232
+
233
+ SuperSettings.configure do |config|
234
+ # These options can be used to customize the header in the web UI.
235
+ config.controller.application_name = "My Application"
236
+ config.controller.application_link = "/"
237
+ config.controller.application_logo = "/images/app_logo.png"
238
+
239
+ # Set a custom refresh interval for the cache (default is 5 seconds)
240
+ config.refresh_interval = 2
241
+
242
+ # Set a secret used for encrypting settings with the "secret" value type.
243
+ config.secret = "ad962cc27e02657795a61b8d48a31ce4"
244
+
245
+ # Set the superclass to use for the controll. Defaults to using `ApplicationController`.
246
+ config.controller.superclass = Admin::BaseController
247
+
248
+ # Add additional code to the controller. In this case we are adding code to ensure only
249
+ # admins can access the functionality and changing the layout to use one defined by the application.
250
+ config.controller.enhance do
251
+ self.layout = "admin"
252
+
253
+ before_action do
254
+ require_admin
255
+ end
256
+
257
+ private
258
+
259
+ def require_admin
260
+ if current_user.nil?
261
+ redirect_to login_url, status: 401
262
+ else
263
+ redirect_to access_denied_url, status: 403
264
+ end
265
+ end
266
+ end
267
+
268
+ # Define a method that returns the value that will be stored in the settings history in
269
+ # the `changed_by` column.
270
+ config.controller.define_changed_by do
271
+ current_user.name
272
+ end
273
+
274
+ # You can define the storage engine for the model. This can be either done either with a Class
275
+ # object or with a symbol matching the underscored class name of a storage class defined under
276
+ # the SuperSettings::Storage namespace.
277
+ # config.model.storage = :active_record
278
+
279
+ # You can also specify a cache implementation to use to cache the last updated timestamp
280
+ # for model changes. By default this will use `Rails.cache`.
281
+ # config.model.cache = Rails.cache
282
+ end
283
+ ```
284
+
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
+ ## Installation
288
+
289
+ Add this line to your application's Gemfile:
290
+
291
+ ```ruby
292
+ gem 'super_settings'
293
+ ```
294
+
295
+ And then execute:
296
+ ```bash
297
+ $ bundle
298
+ ```
299
+
300
+ Or install it yourself as:
301
+ ```bash
302
+ $ gem install super_settings
303
+ ```
304
+
305
+ ## Contributing
306
+
307
+ Open a pull request on GitHub.
308
+
309
+ Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting.
310
+
311
+ ## License
312
+
313
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0.rc1
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperSettings
4
+ module SettingsHelper
5
+ # Render the styles.css as an inline <style> tag.
6
+ def super_settings_layout_style_tag
7
+ application_dir = File.expand_path(File.join("..", "..", "..", "lib", "super_settings", "application"), __dir__)
8
+ content_tag(:style, type: "text/css") do
9
+ render(file: File.join(application_dir, "layout_styles.css")).html_safe
10
+ end
11
+ end
12
+
13
+ # Return the application name set by the configuration or a default value.
14
+ def super_settings_application_name
15
+ Configuration.instance.controller.application_name || "Application"
16
+ end
17
+
18
+ # Render the header for the web pages using values set in the configuration.
19
+ def super_settings_application_header
20
+ config = Configuration.instance.controller
21
+ content = "#{super_settings_application_name} Settings"
22
+ if config.application_logo.present?
23
+ content = image_tag(config.application_logo, alt: "").concat(content)
24
+ end
25
+ if config.application_link
26
+ link_to(content, config.application_link)
27
+ else
28
+ content
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <%= content_tag :title, "#{super_settings_application_name} Settings"%>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag if defined?(csp_meta_tag) %>
7
+ <%= super_settings_layout_style_tag %>
8
+ </head>
9
+
10
+ <body class="super_settings">
11
+ <header>
12
+ <h1 class="logo">
13
+ <%= super_settings_application_header %>
14
+ </h1>
15
+ </header>
16
+ <div class="container">
17
+ <%= yield %>
18
+ </div>
19
+ </body>
20
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ SuperSettings::Engine.routes.draw do
4
+ controller :settings do
5
+ get "/", action: :root, as: :root
6
+ get "/settings", action: :index
7
+ post "/settings", action: :update
8
+ get "/setting", action: :show
9
+ get "/setting/history", action: :history
10
+ get "/settings/last_updated_at", action: :last_updated_at
11
+ get "/settings/updated_since", action: :updated_since
12
+ end
13
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Needed for the super_settings gem to maintain backward compatibility with Rails 4.2
4
+ migration_class = ActiveRecord::Migration
5
+ if migration_class.respond_to?(:[])
6
+ migration_class = migration_class[4.2]
7
+ end
8
+
9
+ class CreateSuperSettings < migration_class
10
+ def up
11
+ create_table :super_settings do |t|
12
+ t.string :key, null: false, limit: 190, index: {unique: true}
13
+ t.string :value_type, limit: 30, null: false, default: "string"
14
+ t.string :raw_value, limit: 4096, null: true
15
+ t.string :description, limit: 4096, null: true
16
+ t.datetime :updated_at, null: false, index: true
17
+ t.datetime :created_at, null: false
18
+ t.boolean :deleted, default: false
19
+ end
20
+
21
+ create_table :super_settings_histories do |t|
22
+ t.string :key, null: false, limit: 190, index: true
23
+ t.string :changed_by, limit: 150, null: true, index: true
24
+ t.string :value, limit: 4096, null: true
25
+ t.boolean :deleted, default: false
26
+ t.datetime :created_at, null: false
27
+ end
28
+ end
29
+
30
+ def down
31
+ drop_table :super_settings
32
+ drop_table :super_settings_histories
33
+ end
34
+ end
@@ -0,0 +1,88 @@
1
+ // Functions for using the Super Settngs REST API.
2
+ //
3
+ // The functions are exposed through the `window.SuperSettingsAPI` object.
4
+ //
5
+ // You can add custom headers or query string parameters to the API requests
6
+ // by adding key/values to the `headers` and `queryParams` hashes on this object.
7
+ // You can use these to add authorization credentials or access tokens to the
8
+ // requests so they will be accepted by the back end.
9
+ (function() {
10
+ // Get the URL for making an API call to the specified action and id.
11
+ function apiURL(action, params) {
12
+ let url = window.location.pathname;
13
+ if (url.endsWith("/")) {
14
+ url = url.substring(0, url.length - 1);
15
+ }
16
+ if (action) {
17
+ url += action;
18
+ }
19
+ if (params) {
20
+ const queryString = Object.keys(params).map(function(key) {
21
+ return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
22
+ }).join('&');
23
+ if (queryString.length > 0) {
24
+ url += "?" + queryString
25
+ }
26
+ }
27
+ return url;
28
+ }
29
+
30
+ function callAPI(path, options, callback) {
31
+ if (!options) {
32
+ options = {};
33
+ }
34
+ method = (options.method || "get");
35
+
36
+ params = options.params
37
+ let queryParams = null;
38
+ const fetchOptions = {credentials: "same-origin"};
39
+ const headers = Object.assign({"Accept": "application/json"}, SuperSettingsAPI.headers);
40
+ if (method === "POST") {
41
+ queryParams = Object.assign({}, SuperSettingsAPI.queryParams);
42
+ csrfParam = document.querySelector("meta[name=csrf-param]");
43
+ csrfToken = document.querySelector("meta[name=csrf-token]");
44
+ if (csrfParam && csrfToken) {
45
+ params = Object.assign({}, params || {});
46
+ params[csrfParam.content] = csrfToken.content;
47
+ }
48
+ fetchOptions["method"] = "POST";
49
+ fetchOptions["body"] = JSON.stringify(params);
50
+ headers["Content-Type"] = "application/json";
51
+ } else {
52
+ queryParams = Object.assign({}, SuperSettingsAPI.queryParams, params);
53
+ }
54
+ fetchOptions["headers"] = new Headers(headers);
55
+ const url = apiURL(path, queryParams);
56
+
57
+ fetch(url, fetchOptions)
58
+ .then(
59
+ function(response) {
60
+ if (response.ok) {
61
+ return response.json();
62
+ } else {
63
+ throw( response.status + response.statusText)
64
+ }
65
+ }
66
+ ).then(
67
+ callback
68
+ ).catch(
69
+ function(error) {
70
+ showError(error);
71
+ }
72
+ );
73
+ }
74
+
75
+ // Show an error message in an alert.
76
+ function showError(error) {
77
+ console.error('Error:', error)
78
+ alert("Sorry, an error occurred. Refresh the page and try again.")
79
+ }
80
+
81
+ window.SuperSettingsAPI = {
82
+ queryParams: {},
83
+ headers: {},
84
+ fetchSettings: function(callback) { callAPI("/settings", {}, callback) },
85
+ fetchHistory: function(params, callback) { callAPI("/setting/history", {params: params}, callback) },
86
+ updateSettings: function(params, callback) { callAPI("/settings", {method: "POST", params: params}, callback)}
87
+ }
88
+ })();
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperSettings
4
+ # Helper functions used for rendering the Super Settings HTML application. These methods
5
+ # are mixed in to the Application class so they are accessible from the ERB templates.
6
+ module Helper
7
+ ICON_SVG = Dir.glob(File.join(__dir__, "images", "*.svg")).each_with_object({}) do |file, cache|
8
+ cache[File.basename(file, ".svg")] = File.read(file).chomp
9
+ end.freeze
10
+
11
+ ICON_BUTTON_STYLE = {
12
+ cursor: "pointer",
13
+ width: "1.5rem",
14
+ height: "1.5rem",
15
+ "min-width": "20px",
16
+ "min-height": "20px",
17
+ "margin-top": "0.25rem",
18
+ "margin-right": "0.5rem"
19
+ }.freeze
20
+
21
+ DEFAULT_ICON_STYLE = {
22
+ width: "1rem",
23
+ height: "1rem",
24
+ display: "inline-block",
25
+ "vertical-align": "middle"
26
+ }.freeze
27
+
28
+ # Render the scripts.js file as an inline <script> tag.
29
+ def javascript_tag
30
+ <<~HTML
31
+ <script>
32
+ #{File.read(File.join(__dir__, "scripts.js"))}
33
+ #{File.read(File.join(__dir__, "api.js"))}
34
+ #{Configuration.instance.controller.javascript}
35
+ </script>
36
+ HTML
37
+ end
38
+
39
+ # Render the styles.css as an inline <style> tag.
40
+ def style_tag
41
+ <<~HTML
42
+ <style type="text/css">
43
+ #{File.read(File.join(__dir__, "styles.css"))}
44
+ </style>
45
+ HTML
46
+ end
47
+
48
+ # Render the styles.css as an inline <style> tag.
49
+ def layout_style_tag
50
+ <<~HTML
51
+ <style type="text/css">
52
+ #{File.read(File.join(__dir__, "layout_styles.css"))}
53
+ </style>
54
+ HTML
55
+ end
56
+
57
+ # Render an image tag for one of the SVG images in the images directory. If the :color option
58
+ # is specified, it will be applied to the SVG image.
59
+ def icon_image(name, options = {})
60
+ svg = ICON_SVG[name.to_s]
61
+ if Coerce.present?(options[:color])
62
+ svg = svg.gsub("currentColor", options[:color])
63
+ end
64
+ css = DEFAULT_ICON_STYLE.merge(options[:style] || {}).map { |name, value| "#{name}:#{value}" }.join("; ")
65
+ options = {alt: ""}.merge(options).merge(src: "data:image/svg+xml;utf8,#{svg}", style: css)
66
+ tag(:img, options)
67
+ end
68
+
69
+ # Render an icon image as a link tag.
70
+ def icon_button(icon, title:, color:, js_class:, url: nil, disabled: false, style: {}, link_style: nil)
71
+ url = "#" if Coerce.blank?(url)
72
+ image = icon_image(icon, alt: title, color: color, style: ICON_BUTTON_STYLE.merge(style))
73
+ content_tag(:a, image, href: url, class: js_class, disabled: disabled, style: link_style)
74
+ end
75
+
76
+ # Return the application name set by the configuration or a default value.
77
+ def application_name
78
+ ERB::Util.html_escape(Configuration.instance.controller.application_name || "Application")
79
+ end
80
+
81
+ # Render the header for the web pages using values set in the configuration.
82
+ def application_header
83
+ config = Configuration.instance.controller
84
+ content = ERB::Util.html_escape("#{application_name} Settings")
85
+ if Coerce.present?(config.application_logo)
86
+ content = tag(:img, src: config.application_logo, alt: "") + content
87
+ end
88
+ if config.application_link
89
+ content_tag(:a, content, href: config.application_link)
90
+ else
91
+ content
92
+ end
93
+ end
94
+
95
+ # Render an HTML tag without any body content.
96
+ def tag(tag, options)
97
+ "<#{tag} #{html_attributes(options)}>"
98
+ end
99
+
100
+ # Render an HTML tag with body content.
101
+ def content_tag(tag, body, options)
102
+ "<#{tag} #{html_attributes(options)}>#{body}</#{tag}>"
103
+ end
104
+
105
+ # Format a hash into HTML attributes.
106
+ def html_attributes(options)
107
+ html_options = []
108
+ options.each do |name, value|
109
+ html_options << "#{name}=\"#{ERB::Util.html_escape(value.to_s)}\""
110
+ end
111
+ html_options.join(" ")
112
+ end
113
+
114
+ # Add additional HTML code to the <head> element on the page.
115
+ def add_to_head
116
+ @add_to_head if defined?(@add_to_head)
117
+ end
118
+ end
119
+ end
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-info"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>