ultra_settings 2.10.0 → 2.10.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +18 -0
- data/VERSION +1 -1
- data/app/_config_description.html.erb +10 -1
- data/app/_data_source.html.erb +2 -2
- data/app/application.css +116 -24
- data/app/application_vars.css.erb +30 -12
- data/app/configuration.html.erb +19 -1
- data/app/layout.css +5 -5
- data/app/layout_vars.css.erb +4 -4
- data/app/locales/ar.json +5 -2
- data/app/locales/cs.json +5 -2
- data/app/locales/da.json +5 -2
- data/app/locales/de.json +5 -2
- data/app/locales/el.json +5 -2
- data/app/locales/en.json +5 -2
- data/app/locales/es.json +5 -2
- data/app/locales/fa.json +5 -2
- data/app/locales/fr.json +5 -2
- data/app/locales/gd.json +5 -2
- data/app/locales/he.json +5 -2
- data/app/locales/hi.json +5 -2
- data/app/locales/it.json +5 -2
- data/app/locales/ja.json +5 -2
- data/app/locales/ko.json +5 -2
- data/app/locales/lt.json +5 -2
- data/app/locales/nb.json +5 -2
- data/app/locales/nl.json +5 -2
- data/app/locales/pl.json +5 -2
- data/app/locales/pt-br.json +5 -2
- data/app/locales/pt.json +5 -2
- data/app/locales/ru.json +5 -2
- data/app/locales/sv.json +5 -2
- data/app/locales/ta.json +5 -2
- data/app/locales/tr.json +5 -2
- data/app/locales/uk.json +5 -2
- data/app/locales/ur.json +5 -2
- data/app/locales/vi.json +5 -2
- data/app/locales/zh-cn.json +5 -2
- data/app/locales/zh-tw.json +5 -2
- data/lib/ultra_settings/application_view.rb +13 -4
- data/lib/ultra_settings/audit_data_sources.rb +1 -1
- data/lib/ultra_settings/coerce.rb +1 -1
- data/lib/ultra_settings/configuration.rb +36 -10
- data/lib/ultra_settings/configuration_view.rb +166 -9
- data/lib/ultra_settings/mini_i18n.rb +14 -10
- data/lib/ultra_settings/rack_app.rb +4 -4
- data/lib/ultra_settings/view_helper.rb +27 -5
- data/lib/ultra_settings/web_view.rb +17 -10
- data/lib/ultra_settings/yaml_config.rb +10 -2
- data/lib/ultra_settings.rb +57 -1
- data/ultra_settings.gemspec +1 -1
- metadata +2 -2
data/app/locales/vi.json
CHANGED
|
@@ -17,18 +17,21 @@
|
|
|
17
17
|
"field.nil": "nil",
|
|
18
18
|
"field.click_to_expand": "Bấm để mở rộng",
|
|
19
19
|
"field.not_set": "chưa đặt",
|
|
20
|
+
"field.copy": "Sao chép giá trị",
|
|
21
|
+
"field.copy_disabled": "Không thể sao chép giá trị bí mật",
|
|
20
22
|
"source.env": "ENV",
|
|
21
23
|
"source.setting": "CÀI ĐẶT",
|
|
22
24
|
"source.yaml": "YAML",
|
|
23
25
|
"source.default": "MẶC ĐỊNH",
|
|
24
|
-
"source.active": "
|
|
26
|
+
"source.active": "Hoạt động",
|
|
25
27
|
"source.overridden": "Bị ghi đè",
|
|
26
28
|
"detail.field_detail": "Chi tiết trường",
|
|
27
29
|
"detail.close_panel": "Đóng bảng",
|
|
28
30
|
"detail.type_label": "Loại:",
|
|
29
31
|
"detail.secret_badge": "BÍ MẬT",
|
|
30
32
|
"detail.secret_value": "•••••••• (bí mật)",
|
|
31
|
-
"config.file_not_found": "
|
|
33
|
+
"config.file_not_found": "tệp không tồn tại",
|
|
34
|
+
"config.show_yaml_keys": "hiện khóa",
|
|
32
35
|
"config.edit_tooltip": "Chỉnh sửa ${name}",
|
|
33
36
|
"edit.title": "Chỉnh sửa cài đặt runtime",
|
|
34
37
|
"edit.open_dashboard": "Mở trong bảng điều khiển cài đặt ↗",
|
data/app/locales/zh-cn.json
CHANGED
|
@@ -17,18 +17,21 @@
|
|
|
17
17
|
"field.nil": "nil",
|
|
18
18
|
"field.click_to_expand": "点击展开",
|
|
19
19
|
"field.not_set": "未设置",
|
|
20
|
+
"field.copy": "复制值",
|
|
21
|
+
"field.copy_disabled": "无法复制机密值",
|
|
20
22
|
"source.env": "ENV",
|
|
21
23
|
"source.setting": "设置",
|
|
22
24
|
"source.yaml": "YAML",
|
|
23
25
|
"source.default": "默认",
|
|
24
|
-
"source.active": "
|
|
26
|
+
"source.active": "当前生效",
|
|
25
27
|
"source.overridden": "已覆盖",
|
|
26
28
|
"detail.field_detail": "字段详情",
|
|
27
29
|
"detail.close_panel": "关闭面板",
|
|
28
30
|
"detail.type_label": "类型:",
|
|
29
31
|
"detail.secret_badge": "机密",
|
|
30
32
|
"detail.secret_value": "•••••••• (机密)",
|
|
31
|
-
"config.file_not_found": "
|
|
33
|
+
"config.file_not_found": "文件不存在",
|
|
34
|
+
"config.show_yaml_keys": "显示键",
|
|
32
35
|
"config.edit_tooltip": "编辑 ${name}",
|
|
33
36
|
"edit.title": "编辑运行时设置",
|
|
34
37
|
"edit.open_dashboard": "在设置仪表板中打开 ↗",
|
data/app/locales/zh-tw.json
CHANGED
|
@@ -17,18 +17,21 @@
|
|
|
17
17
|
"field.nil": "nil",
|
|
18
18
|
"field.click_to_expand": "點擊以展開",
|
|
19
19
|
"field.not_set": "未設定",
|
|
20
|
+
"field.copy": "複製值",
|
|
21
|
+
"field.copy_disabled": "無法複製機密值",
|
|
20
22
|
"source.env": "ENV",
|
|
21
23
|
"source.setting": "設定",
|
|
22
24
|
"source.yaml": "YAML",
|
|
23
25
|
"source.default": "預設",
|
|
24
|
-
"source.active": "
|
|
26
|
+
"source.active": "目前生效",
|
|
25
27
|
"source.overridden": "已覆寫",
|
|
26
28
|
"detail.field_detail": "欄位詳細資料",
|
|
27
29
|
"detail.close_panel": "關閉面板",
|
|
28
30
|
"detail.type_label": "類型:",
|
|
29
31
|
"detail.secret_badge": "機密",
|
|
30
32
|
"detail.secret_value": "•••••••• (機密)",
|
|
31
|
-
"config.file_not_found": "
|
|
33
|
+
"config.file_not_found": "檔案不存在",
|
|
34
|
+
"config.show_yaml_keys": "顯示鍵",
|
|
32
35
|
"config.edit_tooltip": "編輯 ${name}",
|
|
33
36
|
"edit.title": "編輯執行階段設定",
|
|
34
37
|
"edit.open_dashboard": "在設定儀表板中開啟 ↗",
|
|
@@ -28,14 +28,23 @@ module UltraSettings
|
|
|
28
28
|
|
|
29
29
|
# Render the HTML for the configuration settings UI.
|
|
30
30
|
#
|
|
31
|
+
# The runtime settings cache is reloaded before rendering so that values changed
|
|
32
|
+
# from the UI are displayed immediately rather than after the runtime settings
|
|
33
|
+
# engine next refreshes itself.
|
|
34
|
+
#
|
|
31
35
|
# @param select_class [String] @deprecated; no longer used.
|
|
32
36
|
# @param table_class [String] @deprecated; no longer used.
|
|
33
37
|
# @return [String] The rendered HTML.
|
|
34
38
|
def render(select_class: nil, table_class: nil)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
UltraSettings.with_runtime_settings_reloaded do
|
|
40
|
+
# Expose locale as a local for the ERB template without a direct
|
|
41
|
+
# assignment, which would emit an unused variable warning under ruby -w.
|
|
42
|
+
template_binding = binding
|
|
43
|
+
template_binding.local_variable_set(:locale, @locale)
|
|
44
|
+
html = ViewHelper.erb_template("index.html.erb").result(template_binding)
|
|
45
|
+
html = html.html_safe if html.respond_to?(:html_safe)
|
|
46
|
+
html
|
|
47
|
+
end
|
|
39
48
|
end
|
|
40
49
|
|
|
41
50
|
# Generate an HTML style tag with the CSS for the view.
|
|
@@ -91,7 +91,7 @@ module UltraSettings
|
|
|
91
91
|
def default_config_value(config, field)
|
|
92
92
|
yaml_value = config.__value_from_source__(field.name, :yaml)
|
|
93
93
|
default_value = config.__value_from_source__(field.name, :default)
|
|
94
|
-
yaml_value
|
|
94
|
+
yaml_value.nil? ? default_value : yaml_value
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
end
|
|
@@ -7,6 +7,9 @@ module UltraSettings
|
|
|
7
7
|
ALLOWED_NAME_PATTERN = /\A[a-z_][a-zA-Z0-9_]*\z/
|
|
8
8
|
ALLOWED_TYPES = [:string, :symbol, :integer, :float, :boolean, :datetime, :array].freeze
|
|
9
9
|
|
|
10
|
+
DESCENDANTS_MUTEX = Mutex.new
|
|
11
|
+
private_constant :DESCENDANTS_MUTEX
|
|
12
|
+
|
|
10
13
|
@env_var_prefix = nil
|
|
11
14
|
@runtime_setting_prefix = nil
|
|
12
15
|
@description = nil
|
|
@@ -373,10 +376,15 @@ module UltraSettings
|
|
|
373
376
|
|
|
374
377
|
# Get all descendant configuration classes (subclasses and their subclasses, recursively).
|
|
375
378
|
#
|
|
379
|
+
# Note that in Rails development mode this list can include stale classes from
|
|
380
|
+
# previous code reloads; callers that display the results should filter out
|
|
381
|
+
# classes that no longer resolve to a defined constant (see
|
|
382
|
+
# UltraSettings.__configurations__).
|
|
383
|
+
#
|
|
376
384
|
# @return [Array<Class>] All classes that inherit from this class.
|
|
377
385
|
def descendant_configurations
|
|
378
|
-
@descendants
|
|
379
|
-
|
|
386
|
+
descendants = DESCENDANTS_MUTEX.synchronize { (@descendants || []).dup }
|
|
387
|
+
descendants.flat_map { |subclass| [subclass] + subclass.descendant_configurations }
|
|
380
388
|
end
|
|
381
389
|
|
|
382
390
|
private
|
|
@@ -387,8 +395,10 @@ module UltraSettings
|
|
|
387
395
|
# @return [void]
|
|
388
396
|
def inherited(subclass)
|
|
389
397
|
super
|
|
390
|
-
|
|
391
|
-
|
|
398
|
+
DESCENDANTS_MUTEX.synchronize do
|
|
399
|
+
@descendants ||= []
|
|
400
|
+
@descendants << subclass
|
|
401
|
+
end
|
|
392
402
|
end
|
|
393
403
|
|
|
394
404
|
def defined_fields
|
|
@@ -496,7 +506,8 @@ module UltraSettings
|
|
|
496
506
|
end
|
|
497
507
|
|
|
498
508
|
def override!(values, &block)
|
|
499
|
-
|
|
509
|
+
thread_id = Thread.current.object_id
|
|
510
|
+
save_val = @ultra_settings_mutex.synchronize { @ultra_settings_override_values[thread_id] }
|
|
500
511
|
|
|
501
512
|
temp_values = (save_val || {}).dup
|
|
502
513
|
values.each do |key, value|
|
|
@@ -505,12 +516,18 @@ module UltraSettings
|
|
|
505
516
|
|
|
506
517
|
begin
|
|
507
518
|
@ultra_settings_mutex.synchronize do
|
|
508
|
-
@ultra_settings_override_values[
|
|
519
|
+
@ultra_settings_override_values[thread_id] = temp_values
|
|
509
520
|
end
|
|
510
521
|
yield
|
|
511
522
|
ensure
|
|
512
523
|
@ultra_settings_mutex.synchronize do
|
|
513
|
-
|
|
524
|
+
if save_val.nil?
|
|
525
|
+
# Remove the key entirely so the hash doesn't accumulate an entry
|
|
526
|
+
# for every thread that has ever used override!.
|
|
527
|
+
@ultra_settings_override_values.delete(thread_id)
|
|
528
|
+
else
|
|
529
|
+
@ultra_settings_override_values[thread_id] = save_val
|
|
530
|
+
end
|
|
514
531
|
end
|
|
515
532
|
end
|
|
516
533
|
end
|
|
@@ -592,11 +609,20 @@ module UltraSettings
|
|
|
592
609
|
field = self.class.send(:defined_fields)[name]
|
|
593
610
|
return nil unless field
|
|
594
611
|
|
|
595
|
-
override_values =
|
|
612
|
+
override_values = nil
|
|
613
|
+
memoized = false
|
|
614
|
+
memoized_value = nil
|
|
615
|
+
@ultra_settings_mutex.synchronize do
|
|
616
|
+
override_values = @ultra_settings_override_values[Thread.current.object_id]
|
|
617
|
+
if field.static? && @ultra_settings_memoized_values.include?(name)
|
|
618
|
+
memoized = true
|
|
619
|
+
memoized_value = @ultra_settings_memoized_values[name]
|
|
620
|
+
end
|
|
621
|
+
end
|
|
596
622
|
use_override = override_values&.include?(name)
|
|
597
623
|
|
|
598
|
-
if
|
|
599
|
-
return
|
|
624
|
+
if memoized && !use_override
|
|
625
|
+
return memoized_value
|
|
600
626
|
end
|
|
601
627
|
|
|
602
628
|
value = nil
|
|
@@ -24,13 +24,24 @@ module UltraSettings
|
|
|
24
24
|
|
|
25
25
|
# Render the HTML for the configuration view.
|
|
26
26
|
#
|
|
27
|
+
# The runtime settings cache is reloaded before rendering so that values changed
|
|
28
|
+
# from the UI are displayed immediately rather than after the runtime settings
|
|
29
|
+
# engine next refreshes itself. If a page renders more than one view, wrap them all
|
|
30
|
+
# in `UltraSettings.with_runtime_settings_reloaded` so that the settings are only
|
|
31
|
+
# reloaded once for the page instead of once per view.
|
|
32
|
+
#
|
|
27
33
|
# @param table_class [String] @deprecated CSS class for the table element (maintained for backwards compatibility).
|
|
28
34
|
# @return [String] The rendered HTML.
|
|
29
35
|
def render(table_class: "")
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
UltraSettings.with_runtime_settings_reloaded do
|
|
37
|
+
# Expose configuration as a local for the ERB template without a direct
|
|
38
|
+
# assignment, which would emit an unused variable warning under ruby -w.
|
|
39
|
+
template_binding = binding
|
|
40
|
+
template_binding.local_variable_set(:configuration, @configuration)
|
|
41
|
+
html = ViewHelper.erb_template("configuration.html.erb").result(template_binding)
|
|
42
|
+
html = html.html_safe if html.respond_to?(:html_safe)
|
|
43
|
+
html
|
|
44
|
+
end
|
|
34
45
|
end
|
|
35
46
|
|
|
36
47
|
# Convert the view to a string by rendering it.
|
|
@@ -70,6 +81,25 @@ module UltraSettings
|
|
|
70
81
|
end
|
|
71
82
|
end
|
|
72
83
|
|
|
84
|
+
# The text placed on the clipboard by the copy button. This is the raw value
|
|
85
|
+
# rather than the inspected value shown in the UI so that, for example, copying
|
|
86
|
+
# a string setting does not include the surrounding quotes.
|
|
87
|
+
#
|
|
88
|
+
# @param value [Object] The setting value.
|
|
89
|
+
# @return [String] The text to copy.
|
|
90
|
+
def copy_value(value)
|
|
91
|
+
case value
|
|
92
|
+
when nil
|
|
93
|
+
""
|
|
94
|
+
when Time
|
|
95
|
+
value.iso8601
|
|
96
|
+
when Array
|
|
97
|
+
value.join("\n")
|
|
98
|
+
else
|
|
99
|
+
value.to_s
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
73
103
|
def secret_value(value)
|
|
74
104
|
if value.nil?
|
|
75
105
|
t("field.nil")
|
|
@@ -78,13 +108,63 @@ module UltraSettings
|
|
|
78
108
|
end
|
|
79
109
|
end
|
|
80
110
|
|
|
111
|
+
# Shorten a file path for display by making it relative to the working
|
|
112
|
+
# directory or to the YAML configuration directory. The absolute path is
|
|
113
|
+
# used if the file is not inside either directory.
|
|
114
|
+
#
|
|
115
|
+
# @param path [Pathname] The absolute file path.
|
|
116
|
+
# @return [String] The path to display.
|
|
81
117
|
def relative_path(path)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
118
|
+
paths = display_paths(Pathname.new(path).expand_path)
|
|
119
|
+
display_path_roots.each do |root|
|
|
120
|
+
paths.each do |file_path|
|
|
121
|
+
relative = path_inside(file_path, root)
|
|
122
|
+
return relative if relative
|
|
123
|
+
end
|
|
86
124
|
end
|
|
87
|
-
|
|
125
|
+
paths.first.to_s
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The file path in both its literal and symlink resolved forms so that it can
|
|
129
|
+
# be matched against a directory that is specified in either form.
|
|
130
|
+
#
|
|
131
|
+
# @param path [Pathname] The absolute file path.
|
|
132
|
+
# @return [Array<Pathname>]
|
|
133
|
+
def display_paths(path)
|
|
134
|
+
[path, resolved_path(path.dirname)&.join(path.basename)].compact.uniq
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Directories that a displayed path can be made relative to, in order of
|
|
138
|
+
# preference. Directories are listed in both their literal and symlink
|
|
139
|
+
# resolved forms since a file path can be in either form.
|
|
140
|
+
#
|
|
141
|
+
# @return [Array<Pathname>]
|
|
142
|
+
def display_path_roots
|
|
143
|
+
roots = [Pathname.new(Dir.pwd)]
|
|
144
|
+
config_path = UltraSettings::Configuration.yaml_config_path
|
|
145
|
+
roots << Pathname.new(config_path) if config_path
|
|
146
|
+
roots.flat_map { |root| [root.expand_path, resolved_path(root)] }.compact.uniq
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @param path [Pathname] The absolute file path.
|
|
150
|
+
# @param root [Pathname] The absolute directory path.
|
|
151
|
+
# @return [String, nil] The path relative to the directory or nil if it is not inside it.
|
|
152
|
+
def path_inside(path, root)
|
|
153
|
+
relative = path.relative_path_from(root).to_s.delete_prefix("./")
|
|
154
|
+
return nil if relative == "." || relative.start_with?("..")
|
|
155
|
+
|
|
156
|
+
relative
|
|
157
|
+
rescue ArgumentError
|
|
158
|
+
# relative_path_from raises if the paths have no common root (e.g. different drives).
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# @param path [Pathname] The directory path.
|
|
163
|
+
# @return [Pathname, nil] The path with symlinks resolved or nil if it does not exist.
|
|
164
|
+
def resolved_path(path)
|
|
165
|
+
path.realpath
|
|
166
|
+
rescue SystemCallError
|
|
167
|
+
nil
|
|
88
168
|
end
|
|
89
169
|
|
|
90
170
|
def source_chip_label(source)
|
|
@@ -116,6 +196,34 @@ module UltraSettings
|
|
|
116
196
|
end
|
|
117
197
|
end
|
|
118
198
|
|
|
199
|
+
# True if the YAML keys for a configuration are hidden behind a toggle button.
|
|
200
|
+
# They are hidden by default when the YAML file does not exist since the keys
|
|
201
|
+
# are not used by the application.
|
|
202
|
+
#
|
|
203
|
+
# @param configuration [UltraSettings::Configuration] The configuration instance.
|
|
204
|
+
# @return [Boolean]
|
|
205
|
+
def hide_yaml_keys?(configuration)
|
|
206
|
+
config_class = configuration.class
|
|
207
|
+
file = config_class.configuration_file
|
|
208
|
+
file.is_a?(Pathname) && !file.exist? && config_class.fields.any?(&:yaml_key)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Inline script for the button that shows and hides the YAML keys. It is
|
|
212
|
+
# inlined on the element so that the button also works when the configuration
|
|
213
|
+
# view is embedded in a host application page that does not include the
|
|
214
|
+
# bundled JavaScript.
|
|
215
|
+
#
|
|
216
|
+
# @return [String] JavaScript source for an onclick attribute.
|
|
217
|
+
def toggle_yaml_keys_script
|
|
218
|
+
<<~JAVASCRIPT.gsub(/\s+/, " ").tr('"', "'")
|
|
219
|
+
var block = this.closest('.ultra-settings-block');
|
|
220
|
+
if (block) {
|
|
221
|
+
var hidden = block.classList.toggle('ultra-settings-yaml-hidden');
|
|
222
|
+
this.setAttribute('aria-checked', hidden ? 'false' : 'true');
|
|
223
|
+
}
|
|
224
|
+
JAVASCRIPT
|
|
225
|
+
end
|
|
226
|
+
|
|
119
227
|
def open_panel_script
|
|
120
228
|
<<~JAVASCRIPT.gsub(/\s+/, " ").tr('"', "'")
|
|
121
229
|
var el = this;
|
|
@@ -146,6 +254,38 @@ module UltraSettings
|
|
|
146
254
|
JAVASCRIPT
|
|
147
255
|
end
|
|
148
256
|
|
|
257
|
+
# Inline script for the copy button. It is inlined on the element so that the
|
|
258
|
+
# button also works when the configuration view is embedded in a host
|
|
259
|
+
# application page that does not include the bundled JavaScript.
|
|
260
|
+
#
|
|
261
|
+
# @return [String] JavaScript source for an onclick attribute.
|
|
262
|
+
def copy_value_script
|
|
263
|
+
<<~JAVASCRIPT.gsub(/\s+/, " ").tr('"', "'")
|
|
264
|
+
var btn = this;
|
|
265
|
+
var text = btn.dataset.copyValue || '';
|
|
266
|
+
var flash = function() {
|
|
267
|
+
btn.classList.add('copied');
|
|
268
|
+
window.setTimeout(function() { btn.classList.remove('copied'); }, 1500);
|
|
269
|
+
};
|
|
270
|
+
var fallback = function() {
|
|
271
|
+
var input = document.createElement('textarea');
|
|
272
|
+
input.value = text;
|
|
273
|
+
input.setAttribute('readonly', '');
|
|
274
|
+
input.style.position = 'fixed';
|
|
275
|
+
input.style.opacity = '0';
|
|
276
|
+
document.body.appendChild(input);
|
|
277
|
+
input.select();
|
|
278
|
+
try { if (document.execCommand('copy')) { flash(); } } catch (e) {}
|
|
279
|
+
document.body.removeChild(input);
|
|
280
|
+
};
|
|
281
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
282
|
+
navigator.clipboard.writeText(text).then(flash, fallback);
|
|
283
|
+
} else {
|
|
284
|
+
fallback();
|
|
285
|
+
}
|
|
286
|
+
JAVASCRIPT
|
|
287
|
+
end
|
|
288
|
+
|
|
149
289
|
def source_priority
|
|
150
290
|
[:env, :settings, :yaml, :default]
|
|
151
291
|
end
|
|
@@ -196,6 +336,23 @@ module UltraSettings
|
|
|
196
336
|
HTML
|
|
197
337
|
end
|
|
198
338
|
|
|
339
|
+
def copy_icon(size = 13)
|
|
340
|
+
<<~HTML
|
|
341
|
+
<svg class="ultra-settings-copy-icon" width="#{size}" height="#{size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
342
|
+
<rect x="9" y="9" width="13" height="13" rx="2"/>
|
|
343
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
344
|
+
</svg>
|
|
345
|
+
HTML
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def check_icon(size = 13)
|
|
349
|
+
<<~HTML
|
|
350
|
+
<svg class="ultra-settings-copy-check" width="#{size}" height="#{size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
351
|
+
<polyline points="20 6 9 17 4 12"/>
|
|
352
|
+
</svg>
|
|
353
|
+
HTML
|
|
354
|
+
end
|
|
355
|
+
|
|
199
356
|
def close_icon(size = 16)
|
|
200
357
|
<<~HTML
|
|
201
358
|
<svg width="#{size}" height="#{size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
@@ -76,26 +76,30 @@ module UltraSettings
|
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
# Load every JSON file from the locales directory, keyed by filename
|
|
79
|
-
# stem (e.g. "en").
|
|
79
|
+
# stem (e.g. "en"). The cache is built into a local hash and then
|
|
80
|
+
# published with a single assignment so that concurrent readers never
|
|
81
|
+
# see a partially loaded cache.
|
|
80
82
|
def load_all_locales
|
|
81
|
-
if development_mode?
|
|
82
|
-
@mutex.synchronize { @cache = {} }
|
|
83
|
-
end
|
|
83
|
+
clear_cache! if development_mode?
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
cached = @cache
|
|
86
|
+
return cached unless cached.empty?
|
|
86
87
|
|
|
87
88
|
@mutex.synchronize do
|
|
88
|
-
|
|
89
|
+
cached = @cache
|
|
90
|
+
return cached unless cached.empty?
|
|
89
91
|
|
|
92
|
+
cache = {}
|
|
90
93
|
Dir.glob(File.join(locales_dir, "*.json")).each do |path|
|
|
91
94
|
code = File.basename(path, ".json").downcase
|
|
92
|
-
|
|
95
|
+
cache[code] = JSON.parse(File.read(path, encoding: Encoding::UTF_8))
|
|
93
96
|
rescue JSON::ParserError
|
|
94
97
|
# Skip malformed locale files
|
|
95
98
|
end
|
|
96
|
-
end
|
|
97
99
|
|
|
98
|
-
|
|
100
|
+
@cache = cache
|
|
101
|
+
cache
|
|
102
|
+
end
|
|
99
103
|
end
|
|
100
104
|
|
|
101
105
|
def locales_dir
|
|
@@ -103,7 +107,7 @@ module UltraSettings
|
|
|
103
107
|
end
|
|
104
108
|
|
|
105
109
|
def development_mode?
|
|
106
|
-
|
|
110
|
+
UltraSettings.__development_mode__?
|
|
107
111
|
end
|
|
108
112
|
end
|
|
109
113
|
end
|
|
@@ -26,9 +26,9 @@ module UltraSettings
|
|
|
26
26
|
private
|
|
27
27
|
|
|
28
28
|
def webview
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
# Don't cache the view in development mode so that changes to the app files
|
|
30
|
+
# are picked up without having to restart the server.
|
|
31
|
+
@webview = nil if UltraSettings.__development_mode__?
|
|
32
32
|
@webview ||= WebView.new(color_scheme: @color_scheme)
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -62,7 +62,7 @@ module UltraSettings
|
|
|
62
62
|
parts = entry.strip.split(";")
|
|
63
63
|
tag = parts[0].to_s.strip.downcase.tr("_", "-")
|
|
64
64
|
q = 1.0
|
|
65
|
-
parts[1
|
|
65
|
+
parts[1..].each do |p|
|
|
66
66
|
if p.strip.start_with?("q=")
|
|
67
67
|
q = p.strip.sub("q=", "").to_f
|
|
68
68
|
end
|
|
@@ -4,6 +4,7 @@ module UltraSettings
|
|
|
4
4
|
# Base class for rendering views.
|
|
5
5
|
module ViewHelper
|
|
6
6
|
@cache = {}
|
|
7
|
+
@mutex = Mutex.new
|
|
7
8
|
|
|
8
9
|
class << self
|
|
9
10
|
# Get an ERB template for rendering.
|
|
@@ -11,8 +12,7 @@ module UltraSettings
|
|
|
11
12
|
# @param path [String] The path to the template file.
|
|
12
13
|
# @return [ERB] The compiled ERB template.
|
|
13
14
|
def erb_template(path)
|
|
14
|
-
|
|
15
|
-
@cache["erb:#{path}"] ||= ERB.new(read_app_file(path))
|
|
15
|
+
fetch("erb:#{path}") { ERB.new(read_app_file(path)) }
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
# Read a file from the app directory.
|
|
@@ -20,8 +20,7 @@ module UltraSettings
|
|
|
20
20
|
# @param path [String] The path to the file relative to the app directory.
|
|
21
21
|
# @return [String] The contents of the file.
|
|
22
22
|
def read_app_file(path)
|
|
23
|
-
|
|
24
|
-
@cache["file:#{path}"] ||= File.read(File.join(app_dir, path))
|
|
23
|
+
fetch("file:#{path}") { File.read(File.join(app_dir, path), encoding: Encoding::UTF_8) }
|
|
25
24
|
end
|
|
26
25
|
|
|
27
26
|
# Get the app directory path.
|
|
@@ -33,8 +32,31 @@ module UltraSettings
|
|
|
33
32
|
|
|
34
33
|
private
|
|
35
34
|
|
|
35
|
+
# Fetch a value from the cache, generating it with the block if needed.
|
|
36
|
+
# The value is generated outside of the lock since the block may itself
|
|
37
|
+
# fetch other cached values. The cache hash is never mutated in place; a
|
|
38
|
+
# copy is published with a single assignment so concurrent readers always
|
|
39
|
+
# see a consistent hash. Two threads may generate the same value
|
|
40
|
+
# concurrently; the first one to publish wins.
|
|
41
|
+
def fetch(key, &block)
|
|
42
|
+
return yield if development_mode?
|
|
43
|
+
|
|
44
|
+
cached = @cache
|
|
45
|
+
return cached[key] if cached.include?(key)
|
|
46
|
+
|
|
47
|
+
value = yield
|
|
48
|
+
@mutex.synchronize do
|
|
49
|
+
if @cache.include?(key)
|
|
50
|
+
@cache[key]
|
|
51
|
+
else
|
|
52
|
+
@cache = @cache.merge(key => value)
|
|
53
|
+
value
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
36
58
|
def development_mode?
|
|
37
|
-
|
|
59
|
+
UltraSettings.__development_mode__?
|
|
38
60
|
end
|
|
39
61
|
end
|
|
40
62
|
end
|
|
@@ -17,17 +17,30 @@ module UltraSettings
|
|
|
17
17
|
@dark_mode_selector = @color_scheme.nil? ? "[data-theme=dark]" : nil
|
|
18
18
|
@layout_template = ViewHelper.erb_template("layout.html.erb")
|
|
19
19
|
@layout_css = scheme_layout_css(@color_scheme, @dark_mode_selector)
|
|
20
|
+
@locale = nil
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
# Render the complete settings page HTML.
|
|
23
24
|
#
|
|
24
|
-
#
|
|
25
|
+
# This instance may be shared between concurrent requests, so per-request
|
|
26
|
+
# state is set on a copy of the view rather than on the shared instance.
|
|
27
|
+
#
|
|
28
|
+
# @param request [Rack::Request, nil] The current Rack request. It is accepted
|
|
29
|
+
# for backward compatibility but is not used by the view; access control and
|
|
30
|
+
# locale resolution are the caller's responsibility.
|
|
25
31
|
# @param locale [String] The locale code for translations.
|
|
26
32
|
# @return [String] The rendered HTML page.
|
|
27
33
|
def render_settings(request = nil, locale: UltraSettings::MiniI18n::DEFAULT_LOCALE)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
renderer = dup
|
|
35
|
+
renderer.instance_variable_set(:@locale, locale)
|
|
36
|
+
renderer.render_layout
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Render the layout template in the context of this instance.
|
|
40
|
+
#
|
|
41
|
+
# @return [String] The rendered HTML page.
|
|
42
|
+
# @api private
|
|
43
|
+
def render_layout
|
|
31
44
|
@layout_template.result(binding)
|
|
32
45
|
end
|
|
33
46
|
|
|
@@ -64,11 +77,5 @@ module UltraSettings
|
|
|
64
77
|
css = ViewHelper.read_app_file("layout.css")
|
|
65
78
|
"#{vars}\n#{css}"
|
|
66
79
|
end
|
|
67
|
-
|
|
68
|
-
def refresh_super_settings!
|
|
69
|
-
return unless defined?(SuperSettings) && UltraSettings.__runtime_settings__ == SuperSettings
|
|
70
|
-
|
|
71
|
-
SuperSettings.refresh_settings
|
|
72
|
-
end
|
|
73
80
|
end
|
|
74
81
|
end
|
|
@@ -52,17 +52,25 @@ module UltraSettings
|
|
|
52
52
|
private
|
|
53
53
|
|
|
54
54
|
def load_yaml(path)
|
|
55
|
-
yaml = File.read(path)
|
|
55
|
+
yaml = File.read(path, encoding: Encoding::UTF_8)
|
|
56
56
|
|
|
57
57
|
if yaml.include?("<%")
|
|
58
58
|
yaml = ERB.new(yaml).result
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
hash =
|
|
61
|
+
hash = parse_yaml(yaml)
|
|
62
62
|
hash = {} unless hash.is_a?(Hash)
|
|
63
63
|
hash
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
def parse_yaml(yaml)
|
|
67
|
+
YAML.load(yaml, aliases: true, permitted_classes: [Symbol, Date, Time]) # rubocop:disable Security/YAMLLoad
|
|
68
|
+
rescue ArgumentError
|
|
69
|
+
# Psych 3 does not support the aliases or permitted_classes options; its
|
|
70
|
+
# YAML.load already allows aliases and arbitrary classes.
|
|
71
|
+
YAML.load(yaml) # rubocop:disable Security/YAMLLoad
|
|
72
|
+
end
|
|
73
|
+
|
|
66
74
|
def environment_config(yaml, environment)
|
|
67
75
|
shared = flatten_hash(yaml.fetch("shared", {}))
|
|
68
76
|
env = flatten_hash(yaml.fetch(environment, {}))
|