system_settings 0.1.0.pre → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +108 -9
  3. data/app/controllers/system_settings/root_controller.rb +1 -1
  4. data/app/models/system_settings/configurator.rb +18 -12
  5. data/app/models/system_settings/errors/settings_read_error.rb +6 -0
  6. data/app/models/system_settings/integer_list_setting.rb +1 -1
  7. data/app/models/system_settings/list_of_integers_validator.rb +6 -13
  8. data/app/models/system_settings/list_of_strings_validator.rb +36 -0
  9. data/app/models/system_settings/setting.rb +1 -1
  10. data/app/models/system_settings/string_list_setting.rb +2 -1
  11. data/app/models/system_settings/string_setting.rb +1 -0
  12. data/app/models/system_settings/type/integer_list.rb +10 -4
  13. data/app/models/system_settings/type/string_list.rb +2 -1
  14. data/config/locales/system_settings.en.yml +2 -1
  15. data/db/migrate/20181007125347_create_system_settings_settings.rb +1 -1
  16. data/frontend/build/asset-manifest.json +4 -4
  17. data/frontend/build/index.html +1 -1
  18. data/frontend/build/precache-manifest.2450e38293bed26dfe321bf03ff5acbb.js +22 -0
  19. data/frontend/build/service-worker.js +1 -1
  20. data/frontend/build/static/css/main.493112c7.chunk.css +1 -0
  21. data/frontend/build/static/js/2.17170219.chunk.js +1 -0
  22. data/frontend/build/static/js/main.dbe11b19.chunk.js +1 -0
  23. data/lib/system_settings/engine.rb +5 -0
  24. data/lib/system_settings/version.rb +1 -1
  25. data/lib/system_settings.rb +16 -0
  26. data/lib/tasks/system_settings_tasks.rake +18 -4
  27. metadata +17 -55
  28. data/frontend/build/precache-manifest.130ca067521371497dba783c23aa7e16.js +0 -22
  29. data/frontend/build/static/css/main.e262560e.chunk.css +0 -1
  30. data/frontend/build/static/js/2.83a5f4da.chunk.js +0 -1
  31. data/frontend/build/static/js/main.afaa2372.chunk.js +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 856385bd0d4333e0d4044a3afaca3dfad12db9cd3ade70940ab588cc8162ac0e
4
- data.tar.gz: 2ccd7b81d43f94bc33e473eef401ae2cdaa0b6ae212e64e8b0d4826717be16c1
3
+ metadata.gz: 9b0d4b4f84f7a19cd6befcc89649419e39607bb9c0e48a6b201d9c4eb8f07ba0
4
+ data.tar.gz: abc9fae1214a54c4fd6f5baeaec24a3879efa4a895c413a31b7966040a7ca274
5
5
  SHA512:
6
- metadata.gz: 31b69d9e2d3663a773cc6539211a2a53644f33a2aec0a40c1c8da1791a9dd81941dfb38d3e0615a4fcd0090e5b45b080f5ed2891303fc05fb8bae7ed0b804fd4
7
- data.tar.gz: 415a9d5a5968a4ed30be94b6ba4812d803be1c5a2d5ebbe15fbb8d716beacd9caa378669e471b3284ca31bcb42ab7ac90226fd3cd4ad8a639ce4f5dcf2c2f9f0
6
+ metadata.gz: f83ef8531ffbaf9f01c407baa8afd93848d9e1a4ff7671e0c656298f76d1e2d71449b30fa78847f8333efdf791cfb55e93e9d7b96eafafb9e27bd3e4e5fe8264
7
+ data.tar.gz: 7a9c5493d48d403de25deac639c1055cc36cbd3a6129457bdccd99b220883ac808492b3eb39f5c3029fb04e1b687307ff9a7c0a8b1bbb12bb06fd74731452db8
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # SystemSettings
2
- Short description and motivation.
1
+ # System Settings
2
+ System Settings is a Rails engine that adds settings functionality.
3
3
 
4
- ## Usage
5
- How to use my plugin.
4
+ Initial setting values can be loaded from file and later edited in a System Settings provided admin panel.
6
5
 
7
- ## Installation
6
+
7
+ ## Getting started
8
8
  Add this line to your application's Gemfile:
9
9
 
10
10
  ```ruby
@@ -16,13 +16,112 @@ And then execute:
16
16
  $ bundle
17
17
  ```
18
18
 
19
- Or install it yourself as:
19
+ Copy migrations:
20
+ ```bash
21
+ $ bin/rails system_settings:install:migrations
22
+ ```
23
+
24
+ And then run the migrations:
20
25
  ```bash
21
- $ gem install system_settings
26
+ $ bin/rails db:migrate
27
+ ```
28
+
29
+ Create settings file where all settings will be defined:
30
+ ```bash
31
+ $ touch config/system_settings.rb
32
+ ```
33
+
34
+ Add your first setting to `config/system_settings.rb`:
35
+ ```ruby
36
+ # String type values
37
+ string :default_mail_from, value: "Example Company <noreply@example.com>", description: "This email will be used for all outgoing emails"
38
+ string :date_format, value: "%Y-%m-%d"
39
+ string :default_locale, value: "en"
40
+
41
+ # Integer type values
42
+ integer :default_records_per_page, value: 25
43
+ integer :remainder_interval_in_hours, value: 48
44
+
45
+ # Array type strings and integers
46
+ string_list :admin_emails, description: "Will receive alerts"
47
+ integer_list :lucky_numbers, description: "Prime numbers are more effective", value: [2, 3, 5, 11]
48
+ ```
49
+
50
+ Load values from `config/system_settings.rb` into database:
51
+ ```bash
52
+ $ ./bin/rails system_settings:load
53
+ ```
54
+
55
+ Add System Settings admin panel to Rails routes:
56
+ ```ruby
57
+ Rails.application.routes.draw do
58
+ mount SystemSettings::Engine, at: "/system_settings"
59
+ # rest of your routes..
60
+ end
61
+ ```
62
+
63
+ Final step. Access settings values anywhere in your code:
64
+ ```ruby
65
+ SystemSettings[:date_format] # => "%Y-%m-%d"
66
+ SystemSettings[:lucky_numbers] # => [2, 3, 5, 11]
67
+
68
+ # You can change setting's value like any other Rails model.
69
+ SystemSettings::Setting.find_by(name: "default_mail_from").update({value: "No-Reply <noreply@example.com>"})
70
+ ```
71
+
72
+
73
+ ## Do not forget!
74
+ Before using System settings in production please protect the `/system_settings` endpoint with routing constraint. You can read more about it in [Rails Guides: Rails Routing from the Outside In](https://guides.rubyonrails.org/routing.html#advanced-constraints)
75
+
76
+ ```ruby
77
+ Rails.application.routes.draw do
78
+ mount SystemSettings::Engine, at: "/system_settings", constraints: AdminRoutingConstraint.new
79
+ # rest of your routes..
80
+ end
22
81
  ```
23
82
 
24
- ## Contributing
25
- Contribution directions go here.
83
+
84
+ ## Few more things
85
+
86
+ When you run `./bin/rails system_settings:load` task it will read `config/system_settings.rb` file and add new entries to the database. If you would like to replace all values with the ones from the file then run `./bin/rails system_settings:reset`
87
+
88
+ System Settings admin panel is precompiled at gem's build time. So it does not require any Javascript runtime and can be used with api-only Rails applications.
89
+
90
+ If you would like to store your settings somewhere else than `config/system_settings.rb` you can use ENV variable `SYSTEM_SETTINGS_PATH` to specify custom path.
91
+
92
+ ## Development
93
+
94
+ Required development dependencies:
95
+ * [Node.js](https://nodejs.org/) - JavaScript runtime
96
+ * [Yarn](https://yarnpkg.com/) - package manager
97
+
98
+ Optional development tools:
99
+ * [overmind](https://github.com/DarthSim/overmind) - Process manager for Procfile-based applications and tmux
100
+ * [direnv](https://direnv.net/) - Unclutter your .profile
101
+
102
+ Required environment variables:
103
+ * `RAILS_VERSION`
104
+ * `SQLITE3_VERSION`
105
+
106
+ As System Settings gem is being developed to be compatible with multiple [Rails](https://github.com/rails/rails) versions,
107
+ you need to set `RAILS_VERSION` and `SQLITE3_VERSION` environment variables when running `bundle install` or any `./bin/rails` command.
108
+ It is recommended to set these up using [direnv](https://direnv.net/) and `.envrc` file.
109
+
110
+
111
+ Getting started with development:
112
+ 1) `RAILS_VERSION=5.2.3 SQLITE3_VERSION=1.4.1 bundle`
113
+ 2) `RAILS_VERSION=5.2.3 SQLITE3_VERSION=1.4.1 ./bin/rails db:create db:migrate`
114
+ 3) `RAILS_VERSION=5.2.3 SQLITE3_VERSION=1.4.1 ./bin/rails test`
115
+ 4) `RAILS_VERSION=5.2.3 SQLITE3_VERSION=1.4.1 ./bin/rails frontend:install`
116
+ 4) `RAILS_VERSION=5.2.3 SQLITE3_VERSION=1.4.1 ./bin/rails app:system_settings:load`
117
+ 4) `RAILS_VERSION=5.2.3 SQLITE3_VERSION=1.4.1 overmind start`
118
+
119
+
120
+ ## Build status
121
+ System Settings is being tested with Rails versions - `5.0`, `5.1`, `5.2`, `6.0`, `rails repo master branch`
122
+
123
+ [![Build Status](https://dev.azure.com/kristsozols/System%20Settings/_apis/build/status/krists.system_settings?branchName=master)](https://dev.azure.com/kristsozols/System%20Settings/_build/latest?definitionId=1&branchName=master)
124
+
26
125
 
27
126
  ## License
28
127
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -3,7 +3,7 @@ require_relative "./application_controller"
3
3
  module SystemSettings
4
4
  class RootController < ApplicationController
5
5
  def index
6
- if File.exists?(SystemSettings::Engine.frontend_build_index_html_path)
6
+ if File.exist?(SystemSettings::Engine.frontend_build_index_html_path)
7
7
  render file: SystemSettings::Engine.frontend_build_index_html_path
8
8
  else
9
9
  render plain: "Frontend application has not been compiled", status: :not_implemented
@@ -1,25 +1,30 @@
1
1
  module SystemSettings
2
2
  class Configurator
3
-
4
3
  class << self
5
4
  def from_file(path)
6
- file_content = File.read(path)
5
+ path_str = path.to_s
6
+ raise SystemSettings::Errors::SettingsReadError, "#{path_str} file does not exist" unless File.exist?(path_str)
7
+ raise SystemSettings::Errors::SettingsReadError, "#{path_str} file not readable" unless File.readable?(path_str)
8
+ file_content = File.read(path_str)
7
9
  new.tap do |obj|
8
- obj.instance_eval(file_content, path, 1)
10
+ obj.instance_eval(file_content, path_str, 1)
9
11
  end
10
12
  end
13
+
14
+ def purge
15
+ new.purge
16
+ end
11
17
  end
12
18
 
13
19
  attr_reader :items
14
20
 
15
21
  def initialize(&block)
16
22
  @items = []
17
- if block_given?
18
- if block.arity == 1
19
- yield self
20
- else
21
- instance_exec(&block)
22
- end
23
+ return unless block_given?
24
+ if block.arity == 1
25
+ yield self
26
+ else
27
+ instance_exec(&block)
23
28
  end
24
29
  end
25
30
 
@@ -28,7 +33,7 @@ module SystemSettings
28
33
  end
29
34
 
30
35
  def string_list(name, value: nil, description: nil, &blk)
31
- add(name, SystemSettings::StringListSetting, value: value, description: description, &blk)
36
+ add(name, SystemSettings::StringListSetting, value: value || [], description: description, &blk)
32
37
  end
33
38
 
34
39
  def integer(name, value: nil, description: nil, &blk)
@@ -36,7 +41,7 @@ module SystemSettings
36
41
  end
37
42
 
38
43
  def integer_list(name, value: nil, description: nil, &blk)
39
- add(name, SystemSettings::IntegerListSetting, value: value, description: description, &blk)
44
+ add(name, SystemSettings::IntegerListSetting, value: value || [], description: description, &blk)
40
45
  end
41
46
 
42
47
  def persist
@@ -51,8 +56,9 @@ module SystemSettings
51
56
  end
52
57
  end
53
58
  end
59
+ true
54
60
  else
55
- $stderr.puts "SystemSettings: Settings table has not been created!"
61
+ warn "SystemSettings: Settings table has not been created!"
56
62
  false
57
63
  end
58
64
  end
@@ -0,0 +1,6 @@
1
+ module SystemSettings
2
+ module Errors
3
+ class SettingsReadError < StandardError
4
+ end
5
+ end
6
+ end
@@ -3,6 +3,6 @@ require_relative "./setting"
3
3
  module SystemSettings
4
4
  class IntegerListSetting < Setting
5
5
  attribute :value, SystemSettings::Type::IntegerList.new
6
- validates :value, list_of_integers: { allow_blank: true }
6
+ validates :value, "system_settings/list_of_integers": true
7
7
  end
8
8
  end
@@ -1,31 +1,24 @@
1
1
  module SystemSettings
2
2
  class ListOfIntegersValidator < ActiveModel::EachValidator
3
- LIST_REGEXP = /\A[+-]?\d+(?:; *[+-]?\d+)*\z/
4
- SINGLE_REGEXP = /\A[+-]?\d+\z/
3
+ LIST_REGEXP = /\A[+-]?\d+(?:; *[+-]?\d+)*\z/.freeze
4
+ SINGLE_REGEXP = /\A[+-]?\d+\z/.freeze
5
5
 
6
6
  def validate_each(record, attr_name, value)
7
7
  came_from_user = :"#{attr_name}_came_from_user?"
8
8
 
9
- if record.respond_to?(came_from_user) && record.public_send(came_from_user)
10
- raw_value = record.read_attribute_before_type_cast(attr_name)
11
- end
9
+ raw_value = record.read_attribute_before_type_cast(attr_name) if record.respond_to?(came_from_user) && record.public_send(came_from_user)
12
10
  raw_value ||= value
13
11
 
14
- if record_attribute_changed_in_place?(record, attr_name)
15
- raw_value = value
16
- end
12
+ raw_value = value if record_attribute_changed_in_place?(record, attr_name)
17
13
 
18
- unless matches_list_of_integers_regexp?(raw_value)
19
- record.errors.add(attr_name, :not_a_list_of_integers)
20
- return
21
- end
14
+ record.errors.add(attr_name, :not_a_list_of_integers) unless matches_list_of_integers_regexp?(raw_value)
22
15
  end
23
16
 
24
17
  private
25
18
 
26
19
  def record_attribute_changed_in_place?(record, attr_name)
27
20
  record.respond_to?(:attribute_changed_in_place?) &&
28
- record.attribute_changed_in_place?(attr_name.to_s)
21
+ record.attribute_changed_in_place?(attr_name.to_s)
29
22
  end
30
23
 
31
24
  def matches_list_of_integers_regexp?(raw_value)
@@ -0,0 +1,36 @@
1
+ module SystemSettings
2
+ class ListOfStringsValidator < ActiveModel::EachValidator
3
+ NON_WHITESPACE_REGEXP = /[^[:space:]]/.freeze
4
+
5
+ def validate_each(record, attr_name, value)
6
+ came_from_user = :"#{attr_name}_came_from_user?"
7
+
8
+ raw_value = record.read_attribute_before_type_cast(attr_name) if record.respond_to?(came_from_user) && record.public_send(came_from_user)
9
+ raw_value ||= value
10
+
11
+ raw_value = value if record_attribute_changed_in_place?(record, attr_name)
12
+
13
+ record.errors.add(attr_name, :not_a_list_of_strings) unless matches_list_of_strings_regexp?(raw_value)
14
+ end
15
+
16
+ private
17
+
18
+ def record_attribute_changed_in_place?(record, attr_name)
19
+ record.respond_to?(:attribute_changed_in_place?) &&
20
+ record.attribute_changed_in_place?(attr_name.to_s)
21
+ end
22
+
23
+ def matches_list_of_strings_regexp?(raw_value)
24
+ case raw_value
25
+ when String
26
+ raw_value.split(SystemSettings::Type::StringList::DELIMITER_REGEXP).all? do |value|
27
+ NON_WHITESPACE_REGEXP.match?(value)
28
+ end
29
+ when Array
30
+ raw_value.all? { |v| NON_WHITESPACE_REGEXP.match?(v.to_s) }
31
+ else
32
+ false
33
+ end
34
+ end
35
+ end
36
+ end
@@ -5,4 +5,4 @@ module SystemSettings
5
5
  validates :type, presence: true
6
6
  validates :name, presence: true, uniqueness: true
7
7
  end
8
- end
8
+ end
@@ -3,5 +3,6 @@ require_relative "./setting"
3
3
  module SystemSettings
4
4
  class StringListSetting < Setting
5
5
  attribute :value, SystemSettings::Type::StringList.new
6
+ validates :value, "system_settings/list_of_strings": true
6
7
  end
7
- end
8
+ end
@@ -3,5 +3,6 @@ require_relative "./setting"
3
3
  module SystemSettings
4
4
  class StringSetting < Setting
5
5
  attribute :value, :string
6
+ validates :value, presence: true
6
7
  end
7
8
  end
@@ -20,11 +20,17 @@ module SystemSettings
20
20
  def cast_value(value)
21
21
  case value
22
22
  when Array
23
- value.map { |v| v.to_i rescue nil }
23
+ value.map do |v|
24
+ v.to_i
25
+ rescue StandardError
26
+ nil
27
+ end
24
28
  when String
25
- value.split(SEPARATOR).map { |v| v.to_i rescue nil }
26
- else
27
- nil
29
+ value.split(SEPARATOR).map do |v|
30
+ v.to_i
31
+ rescue StandardError
32
+ nil
33
+ end
28
34
  end
29
35
  end
30
36
  end
@@ -1,6 +1,7 @@
1
1
  module SystemSettings
2
2
  module Type
3
3
  class StringList < ActiveModel::Type::Value
4
+ DELIMITER_REGEXP = /(?<=[^\\]);/.freeze
4
5
  def type
5
6
  :string_list
6
7
  end
@@ -20,7 +21,7 @@ module SystemSettings
20
21
  when Array
21
22
  value.map { |v| String(v).strip }
22
23
  when String
23
- value.split(/(?<=[^\\]);/).map(&:strip)
24
+ value.split(DELIMITER_REGEXP).map(&:strip).map { |str| str.gsub("\\;", ";") }
24
25
  end
25
26
  end
26
27
  end
@@ -29,4 +29,5 @@ en:
29
29
  system_settings/setting:
30
30
  attributes:
31
31
  value:
32
- not_a_list_of_integers: "not a list of integers"
32
+ not_a_list_of_integers: "not a list of integers"
33
+ not_a_list_of_strings: "not a list of strings"
@@ -1,4 +1,4 @@
1
- class CreateSystemSettingsSettings < ActiveRecord::Migration[5.2]
1
+ class CreateSystemSettingsSettings < ActiveRecord::Migration[5.0]
2
2
  def change
3
3
  create_table :system_settings_settings do |t|
4
4
  t.string :name, null: false, index: { unique: true }
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "files": {
3
- "main.css": "/system_settings/static/css/main.e262560e.chunk.css",
4
- "main.js": "/system_settings/static/js/main.afaa2372.chunk.js",
3
+ "main.css": "/system_settings/static/css/main.493112c7.chunk.css",
4
+ "main.js": "/system_settings/static/js/main.dbe11b19.chunk.js",
5
5
  "runtime~main.js": "/system_settings/static/js/runtime~main.a09e9b82.js",
6
- "static/js/2.83a5f4da.chunk.js": "/system_settings/static/js/2.83a5f4da.chunk.js",
6
+ "static/js/2.17170219.chunk.js": "/system_settings/static/js/2.17170219.chunk.js",
7
7
  "index.html": "/system_settings/index.html",
8
- "precache-manifest.130ca067521371497dba783c23aa7e16.js": "/system_settings/precache-manifest.130ca067521371497dba783c23aa7e16.js",
8
+ "precache-manifest.2450e38293bed26dfe321bf03ff5acbb.js": "/system_settings/precache-manifest.2450e38293bed26dfe321bf03ff5acbb.js",
9
9
  "service-worker.js": "/system_settings/service-worker.js"
10
10
  }
11
11
  }
@@ -1 +1 @@
1
- <!doctype html><html lang="en"><head><meta charset="utf-8"><link rel="shortcut icon" href="/system_settings/favicon.ico"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><title>System Settings</title><link href="/system_settings/static/css/main.e262560e.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script src="/system_settings/static/js/runtime~main.a09e9b82.js"></script><script src="/system_settings/static/js/2.83a5f4da.chunk.js"></script><script src="/system_settings/static/js/main.afaa2372.chunk.js"></script></body></html>
1
+ <!doctype html><html lang="en"><head><meta charset="utf-8"><link rel="shortcut icon" href="/system_settings/favicon.ico"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><title>System Settings</title><link href="/system_settings/static/css/main.493112c7.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script src="/system_settings/static/js/runtime~main.a09e9b82.js"></script><script src="/system_settings/static/js/2.17170219.chunk.js"></script><script src="/system_settings/static/js/main.dbe11b19.chunk.js"></script></body></html>
@@ -0,0 +1,22 @@
1
+ self.__precacheManifest = (self.__precacheManifest || []).concat([
2
+ {
3
+ "revision": "0acf133c6751acd69af511a671c2fd1e",
4
+ "url": "/system_settings/index.html"
5
+ },
6
+ {
7
+ "revision": "e07baceb8e059f51d274",
8
+ "url": "/system_settings/static/css/main.493112c7.chunk.css"
9
+ },
10
+ {
11
+ "revision": "f336fd31562a5938e83a",
12
+ "url": "/system_settings/static/js/2.17170219.chunk.js"
13
+ },
14
+ {
15
+ "revision": "e07baceb8e059f51d274",
16
+ "url": "/system_settings/static/js/main.dbe11b19.chunk.js"
17
+ },
18
+ {
19
+ "revision": "1b52e032205e59a9e84f",
20
+ "url": "/system_settings/static/js/runtime~main.a09e9b82.js"
21
+ }
22
+ ]);
@@ -14,7 +14,7 @@
14
14
  importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.0/workbox-sw.js");
15
15
 
16
16
  importScripts(
17
- "/system_settings/precache-manifest.130ca067521371497dba783c23aa7e16.js"
17
+ "/system_settings/precache-manifest.2450e38293bed26dfe321bf03ff5acbb.js"
18
18
  );
19
19
 
20
20
  self.addEventListener('message', (event) => {
@@ -0,0 +1 @@
1
+ .App_container__BLGGc{min-height:100vh;display:flex;flex-direction:column;position:relative;z-index:auto}.App_header-wrap__3DnwX{position:fixed;width:100%;background:#fff;border-bottom:1px solid #e1e1e1;z-index:1}.App_header__1o2Y8{max-width:1020px;margin:0 auto;display:flex}.App_header-name__2XXxc{font-size:1.5rem;line-height:2rem;font-weight:700;padding:.5rem 0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-decoration:none}.App_header-name__2XXxc:active,.App_header-name__2XXxc:focus,.App_header-name__2XXxc:hover{text-decoration:none}.App_header-spacer__3F0KU{margin-top:calc(3rem + 1px);height:3rem}.App_content-wrap__1oPWs{flex-grow:5;box-sizing:border-box;display:flex}.App_content__3dh9P{max-width:1020px;min-width:0;margin:0 auto;display:flex;flex-grow:1;flex-direction:column}.NotFoundPage_container__35vs4{flex-grow:1;align-items:center;justify-content:center;display:flex}.ListPage_table__2qY_8{table-layout:fixed;white-space:nowrap;width:100%;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ListPage_table__2qY_8 td:empty:after{content:"-"}.ListPage_empty-tr__2VxYl{color:#a6a6a6}.ListPage_empty-tr__2VxYl td{text-align:center}.ListPage_name__1-Gs-{width:20%;overflow:hidden;text-overflow:ellipsis}.ListPage_table__2qY_8 tbody .ListPage_description__1Sci0,.ListPage_table__2qY_8 tbody .ListPage_name__1-Gs-,.ListPage_table__2qY_8 tbody .ListPage_value__3X3Vq{-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}.ListPage_description__1Sci0{width:50%}.ListPage_description__1Sci0,.ListPage_value__3X3Vq{overflow:hidden;text-overflow:ellipsis}.ListPage_actions__2iqTu{width:50px;text-align:right}.PageLoadError_wrap__mpF75{display:flex;align-items:flex-start;justify-content:center}.PageLoadError_container__kcD9o{display:flex;flex-direction:column;background:#ffe5e5;padding:.25rem .5rem;border-radius:4px;color:#b94343;text-align:center}.PageLoadError_error-small__14Fex{font-size:.85em}#nprogress{pointer-events:none}#nprogress .bar{background:#ffc25b;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}#nprogress .peg{display:block;position:absolute;right:0;width:100px;height:100%;box-shadow:0 0 10px #ffc25b,0 0 5px #ffc25b;opacity:1;-webkit-transform:rotate(3deg) translateY(-4px);transform:rotate(3deg) translateY(-4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:.9rem;right:.9rem}#nprogress .spinner-icon{width:1.2rem;height:1.2rem;box-sizing:border-box;border-color:#ffc25b transparent transparent #ffc25b;border-style:solid;border-width:2px;border-radius:50%;-webkit-animation:nprogress-spinner .4s linear infinite;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .bar,.nprogress-custom-parent #nprogress .spinner{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(1turn)}}@keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.Value_wrap__3J_8z{white-space:nowrap}.Value_section__cgQmL{max-width:100%;margin-right:.25em}.Value_section__cgQmL:last-child{margin-right:0}.Attribute_wrap__1Zy9J{display:flex;flex-direction:column;margin-bottom:1rem}.Attribute_name__3a-cD{font-size:.8em;color:#737373;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.Attribute_value__IxZCj:empty:after{content:"-"}.ButtonBar_container__21aEr{border-top:1px dotted #f2f2f2;padding-top:1rem;margin-bottom:1rem}.ButtonBar_container__21aEr>*{margin-right:.5rem}.LabeledInput_wrap__2KAyr{position:relative;margin-bottom:1em}.LabeledInput_label__1AyjZ{font-size:.8em;color:#737373;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;display:block}.LabeledInput_input-and-error-wrap__2iJKd{display:flex;align-items:baseline}.LabeledInput_error__2QBpW{flex-grow:0;display:block;color:#ff3656;margin-left:1rem;white-space:nowrap}.LabeledInput_hint__rAeG8{font-size:.8em;display:block}.SettingForm_error-wrap__3SdnI{display:flex;padding:.5rem 0}.SettingForm_error-message__uQK1Y{background:#ffe5e5;padding:.25rem .5rem;border-radius:4px;color:#b94343}.SettingForm_error-small__3owRp{font-size:.85em}.ClassicSpinner_wrap__1LF2_{display:block}html{box-sizing:border-box;overflow-y:scroll;font-size:14px}*,:after,:before{box-sizing:inherit}:root,body{background:#fff}body{color:#233656;font-family:Arial,Helvetica,sans-serif;font-weight:400;font-size:1rem;line-height:1.5rem;margin:0;padding:0;min-height:100vh;position:relative;display:flex;flex-direction:column}table{border-collapse:collapse;border-spacing:0}table td,table th{text-align:left}a{color:#233656;text-decoration:none}a:hover{text-decoration:underline}a.button,button,input[type=button],input[type=submit]{display:inline-block;cursor:pointer;border:none;border-radius:3px;padding:.375rem .75rem;font-size:1rem;line-height:1.5;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;white-space:nowrap}button,input[type=button],input[type=submit]{color:#4d4d4d;background-color:#d9d9d9;-webkit-transition:background-color .1s ease-in-out;transition:background-color .1s ease-in-out;outline:none}button:focus,button:hover,input[type=button]:focus,input[type=button]:hover,input[type=submit]:focus,input[type=submit]:hover{background-color:#b8b8b8}button:active,input[type=button]:active,input[type=submit]:active{background-color:#828282}a.button{color:#4d4d4d;background-color:initial;-webkit-transition:background-color .1s ease-in-out;transition:background-color .1s ease-in-out;outline:none;text-decoration:none}a.button:focus,a.button:hover{background-color:rgba(0,0,0,.15)}a.button:active{background-color:rgba(0,0,0,.4)}a.button.primary,button.primary,input[type=button].primary,input[type=submit].primary{color:#4d4d4d;background-color:#a6a6a6;-webkit-transition:background-color .1s ease-in-out;transition:background-color .1s ease-in-out;outline:none}a.button.primary:focus,a.button.primary:hover,button.primary:focus,button.primary:hover,input[type=button].primary:focus,input[type=button].primary:hover,input[type=submit].primary:focus,input[type=submit].primary:hover{background-color:#8d8d8d}a.button.primary:active,button.primary:active,input[type=button].primary:active,input[type=submit].primary:active{background-color:#646464}input[type=text]{border:none;border-radius:3px;padding:.5em;font-size:1rem;line-height:normal;color:#4d4d4d;background-color:#e6e6e6;-webkit-transition:background-color .1s ease-in-out;transition:background-color .1s ease-in-out;outline:none;box-sizing:initial;width:auto;min-width:12ch;max-width:calc(100% - 2em);will-change:width}input[type=text]:focus{background-color:#c4c4c4}input[type=text]:active{background-color:#bfbfbf}.button-wrap{display:flex;align-items:center}.svg-wrap{display:block}code{border-radius:2px;padding:0 .2rem}.sysname,code{background:#f2f2f2}.sysname{display:inline-block;border-radius:3px;padding:0 .5em;font-family:Courier New,Courier,monospace;font-size:.75em;max-width:100%}#root{z-index:1;position:relative}