janela 0.2.1 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -1
- data/README.md +200 -20
- data/UPGRADING.md +136 -0
- data/app/assets/javascripts/janela/chart_controller.js +12 -5
- data/app/assets/javascripts/janela/{dashboard_controller.js → frame_controller.js} +11 -4
- data/app/assets/stylesheets/janela.css +163 -0
- data/app/controllers/janela/application_controller.rb +18 -0
- data/app/controllers/janela/frames_controller.rb +57 -0
- data/app/controllers/janela/panes_controller.rb +66 -13
- data/app/controllers/janela/queries_controller.rb +17 -0
- data/app/controllers/janela/{snapshot_panes_controller.rb → snapshot_queries_controller.rb} +7 -5
- data/app/helpers/janela/{dashboard_helper.rb → frames_helper.rb} +25 -9
- data/app/models/janela/frame.rb +28 -0
- data/app/models/janela/pane.rb +102 -99
- data/app/models/janela/query.rb +149 -0
- data/app/models/janela/snapshot.rb +5 -5
- data/app/views/janela/frames/_card.html.erb +7 -0
- data/app/views/janela/frames/_form.html.erb +23 -0
- data/app/views/janela/frames/_frame.html.erb +4 -0
- data/app/views/janela/frames/_pane.html.erb +12 -0
- data/app/views/janela/frames/edit.html.erb +25 -0
- data/app/views/janela/frames/index.html.erb +15 -0
- data/app/views/janela/frames/new.html.erb +5 -0
- data/app/views/janela/frames/show.html.erb +9 -0
- data/app/views/janela/panes/_form.html.erb +58 -0
- data/app/views/janela/panes/_row.html.erb +12 -0
- data/app/views/janela/panes/edit.html.erb +5 -0
- data/app/views/janela/panes/new.html.erb +22 -0
- data/app/views/janela/panes/show.html.erb +3 -46
- data/app/views/janela/queries/_query.html.erb +47 -0
- data/app/views/janela/queries/show.html.erb +4 -0
- data/app/views/janela/shared/_errors.html.erb +7 -0
- data/app/views/layouts/janela/application.html.erb +13 -5
- data/config/importmap.rb +1 -1
- data/config/locales/en.yml +64 -0
- data/config/routes.rb +15 -2
- data/db/migrate/20260916000001_create_janela_frames.rb +13 -0
- data/db/migrate/20260916000002_create_janela_panes.rb +22 -0
- data/docs/decisions/001-built-to-be-forked.md +4 -0
- data/docs/decisions/009-snapshots.md +5 -2
- data/docs/decisions/010-agent-guidance-ships-the-agent-waits.md +8 -4
- data/docs/decisions/012-frames-and-panes-are-data.md +166 -0
- data/docs/decisions/013-naming-and-addressing-frames.md +119 -0
- data/docs/decisions/014-corrections-before-frames-are-built.md +222 -0
- data/docs/decisions/015-how-breaking-change-is-communicated.md +114 -0
- data/docs/decisions/016-the-styling-vocabulary.md +100 -0
- data/docs/decisions/017-janela-owns-no-data-store.md +113 -0
- data/docs/decisions/018-a-table-is-the-universal-renderer.md +75 -0
- data/docs/decisions/019-a-created-frame-asks-the-host-who-owns-it.md +79 -0
- data/docs/decisions/020-formatting-belongs-to-the-measure.md +93 -0
- data/docs/decisions/021-a-check-has-a-name-a-host-can-silence.md +84 -0
- data/docs/decisions/INDEX.md +22 -7
- data/docs/multi-tenancy.md +175 -0
- data/lib/janela/definition.rb +5 -5
- data/lib/janela/doctor.rb +219 -0
- data/lib/janela/engine.rb +15 -2
- data/lib/janela/measure.rb +65 -4
- data/lib/janela/version.rb +1 -1
- data/lib/janela.rb +17 -0
- data/lib/tasks/janela.rake +6 -0
- metadata +54 -4
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
module Janela
|
|
2
|
+
# Reads a host application and reports what it still has to do. Only ever
|
|
3
|
+
# reads and reports: the checks are the install and upgrade traps that
|
|
4
|
+
# experience says actually break a host (ADR 015).
|
|
5
|
+
class Doctor
|
|
6
|
+
# code is the check that produced the finding, set by the runner rather
|
|
7
|
+
# than by each check, so the two can never drift apart.
|
|
8
|
+
Finding = Struct.new(:severity, :summary, :detail, :code, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
# Run in this order, and each one names the finding it produces: a host
|
|
11
|
+
# silences a check by that name (ADR 021).
|
|
12
|
+
CHECKS = %i[stale_identifiers unmounted_engine unmigrated_tables unregistered_controllers
|
|
13
|
+
through_dimensions_without_an_allowlist frames_nobody_will_own
|
|
14
|
+
unauthenticated_endpoints].freeze
|
|
15
|
+
|
|
16
|
+
# Identifiers a previous version of Janela used, and what replaced them.
|
|
17
|
+
RENAMED = {
|
|
18
|
+
"janela--dashboard" => "janela--frame",
|
|
19
|
+
"janela_dashboard" => "janela_frame",
|
|
20
|
+
"janela/dashboard_controller" => "janela/frame_controller",
|
|
21
|
+
"janela/dashboard_controller.js" => "janela/frame_controller.js",
|
|
22
|
+
"Janela::DashboardHelper" => "Janela::FramesHelper",
|
|
23
|
+
"Janela::PanesController" => "Janela::QueriesController",
|
|
24
|
+
"Janela::SnapshotPanesController" => "Janela::SnapshotQueriesController"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
SEARCHED = %w[app config lib].freeze
|
|
28
|
+
READABLE = %w[.rb .erb .js .erb.html .html.erb .haml .slim .yml].freeze
|
|
29
|
+
|
|
30
|
+
def initialize(root)
|
|
31
|
+
@root = Pathname.new(root)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def report(out)
|
|
35
|
+
findings, silenced = all_findings.partition { |finding| !silenced?(finding) }
|
|
36
|
+
|
|
37
|
+
if findings.empty?
|
|
38
|
+
out.puts "Janela: nothing to fix."
|
|
39
|
+
else
|
|
40
|
+
out.puts "Janela found #{findings.size} #{'thing'.pluralize(findings.size)} to look at."
|
|
41
|
+
findings.each do |finding|
|
|
42
|
+
out.puts
|
|
43
|
+
out.puts "#{finding.severity.to_s.upcase} (#{finding.code}): #{finding.summary}"
|
|
44
|
+
out.puts finding.detail
|
|
45
|
+
end
|
|
46
|
+
out.puts
|
|
47
|
+
out.puts "Silence one you have judged a false alarm, in config/initializers/janela.rb:"
|
|
48
|
+
out.puts " Janela.silenced_checks = %w[#{findings.first.code}]"
|
|
49
|
+
out.puts
|
|
50
|
+
out.puts "Steps for a version upgrade are in UPGRADING.md."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Said out loud every run, because a silence nobody remembers is how a
|
|
54
|
+
# real finding goes unread.
|
|
55
|
+
out.puts "Silenced: #{silenced.map(&:code).uniq.join(', ')}." if silenced.any?
|
|
56
|
+
findings.none? { |finding| finding.severity == :error }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def check
|
|
60
|
+
all_findings.reject { |finding| silenced?(finding) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
def all_findings
|
|
65
|
+
@all_findings ||= CHECKS.flat_map { |name| findings_from(name) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Struct responds to to_a, so a single finding is wrapped by hand rather
|
|
69
|
+
# than with Array(), which would take it apart into its members.
|
|
70
|
+
def findings_from(name)
|
|
71
|
+
found = send(name)
|
|
72
|
+
found = [ found ] unless found.is_a?(Array)
|
|
73
|
+
found.compact.each { |finding| finding.code = name.to_s.dasherize }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def silenced?(finding)
|
|
77
|
+
Janela.silenced_checks.map(&:to_s).include?(finding.code)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def stale_identifiers
|
|
81
|
+
RENAMED.filter_map do |old, new|
|
|
82
|
+
files = source_files.select { |file| file.read.include?(old) }
|
|
83
|
+
next if files.empty?
|
|
84
|
+
|
|
85
|
+
Finding.new(severity: :error,
|
|
86
|
+
summary: "#{old} is now #{new}",
|
|
87
|
+
detail: files.map { |file| " #{file.relative_path_from(@root)}" }.join("\n"))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def unmounted_engine
|
|
92
|
+
return if engine_mounted?
|
|
93
|
+
|
|
94
|
+
Finding.new(severity: :error,
|
|
95
|
+
summary: "Janela::Engine is not mounted",
|
|
96
|
+
detail: " Add to config/routes.rb, at whatever path suits you:\n" \
|
|
97
|
+
" mount Janela::Engine => \"/insights\"")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The mount root serves an index of frames, so a host that upgrades
|
|
101
|
+
# without running the migrations finds an exception where its dashboards
|
|
102
|
+
# were. Only a host that mounts the engine needs the tables at all.
|
|
103
|
+
def unmigrated_tables
|
|
104
|
+
return unless engine_mounted?
|
|
105
|
+
|
|
106
|
+
missing = [ Janela::Frame, Janela::Pane, Janela::Snapshot ].reject(&:table_exists?).map(&:table_name)
|
|
107
|
+
return if missing.empty?
|
|
108
|
+
|
|
109
|
+
Finding.new(severity: :error,
|
|
110
|
+
summary: "#{missing.to_sentence} #{missing.one? ? 'is' : 'are'} missing",
|
|
111
|
+
detail: " Janela's own pages read these the moment the engine is mounted. Run:\n" \
|
|
112
|
+
" bin/rails janela:install:migrations\n" \
|
|
113
|
+
" bin/rails db:migrate")
|
|
114
|
+
rescue StandardError
|
|
115
|
+
nil # no database to ask yet
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def engine_mounted?
|
|
119
|
+
Rails.application.routes.routes.any? { |route| route.app.respond_to?(:app) && route.app.app == Janela::Engine }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def unregistered_controllers
|
|
123
|
+
registered = source_files.any? { |file| file.read.include?("janela--frame") }
|
|
124
|
+
return if registered
|
|
125
|
+
|
|
126
|
+
Finding.new(severity: :error,
|
|
127
|
+
summary: "Janela's Stimulus controllers are not registered anywhere",
|
|
128
|
+
detail: " Without them a dashboard renders but nothing cross-filters, which\n" \
|
|
129
|
+
" looks like nothing happening at all. Register both:\n" \
|
|
130
|
+
" application.register(\"janela--frame\", JanelaFrameController)\n" \
|
|
131
|
+
" application.register(\"janela--chart\", JanelaChartController)")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Ransack's allowlist is per class, so a dimension read through an
|
|
135
|
+
# association needs the associated model to allow the attribute too.
|
|
136
|
+
def through_dimensions_without_an_allowlist
|
|
137
|
+
janela_models.flat_map do |model|
|
|
138
|
+
model.janela.dimensions.values.select(&:through).filter_map do |dimension|
|
|
139
|
+
association = model.reflect_on_association(dimension.through)
|
|
140
|
+
next unless association
|
|
141
|
+
|
|
142
|
+
allowed = association.klass.ransackable_attributes.map(&:to_s)
|
|
143
|
+
next if allowed.include?(dimension.column.to_s)
|
|
144
|
+
|
|
145
|
+
Finding.new(severity: :error,
|
|
146
|
+
summary: "#{association.klass} does not allow filtering on #{dimension.column}",
|
|
147
|
+
detail: " #{model}'s #{dimension.name.inspect} dimension reads it through " \
|
|
148
|
+
"#{dimension.through.inspect}, and Ransack's allowlist is per class. Add to " \
|
|
149
|
+
"#{association.klass}:\n" \
|
|
150
|
+
" def self.ransackable_attributes(_auth_object = nil) = " \
|
|
151
|
+
"%w[#{(allowed + [ dimension.column.to_s ]).uniq.join(' ')}]")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# A host whose policy filters frames by owner, but which never tells
|
|
157
|
+
# Janela what owns a new one, creates frames its own scope then hides.
|
|
158
|
+
# The failure is silent, and a typo in the method name looks the same as
|
|
159
|
+
# not defining it, which is the cost of asking by duck typing (ADR 019).
|
|
160
|
+
def frames_nobody_will_own
|
|
161
|
+
parent = Janela.parent_controller.safe_constantize
|
|
162
|
+
return unless parent&.private_method_defined?(:policy_scope) || parent&.method_defined?(:policy_scope)
|
|
163
|
+
return if parent.private_method_defined?(:janela_frame_owner) || parent.method_defined?(:janela_frame_owner)
|
|
164
|
+
return unless Janela::Frame.table_exists? && scope_filters_frames_by_owner?(parent)
|
|
165
|
+
|
|
166
|
+
Finding.new(severity: :error,
|
|
167
|
+
summary: "#{parent} scopes frames by owner but defines no janela_frame_owner",
|
|
168
|
+
detail: " A frame created through Janela's own form would have no owner, and your\n" \
|
|
169
|
+
" own scope would then hide it. Define this on #{parent}:\n" \
|
|
170
|
+
" def janela_frame_owner\n" \
|
|
171
|
+
" Current.account # whatever your policy scope filters frames by\n" \
|
|
172
|
+
" end")
|
|
173
|
+
rescue StandardError
|
|
174
|
+
nil # no database or no policy to ask; nothing can be concluded
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Asking the policy rather than reading its source: a scope that narrows
|
|
178
|
+
# frames is one that will hide an unowned one.
|
|
179
|
+
def scope_filters_frames_by_owner?(parent)
|
|
180
|
+
scope = parent.allocate.send(:policy_scope, Janela::Frame)
|
|
181
|
+
scope.to_sql.include?("owner")
|
|
182
|
+
rescue StandardError
|
|
183
|
+
false
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Whether an endpoint is public depends on what the host's controller
|
|
187
|
+
# does, which cannot be determined by reading, so this observes and says
|
|
188
|
+
# so rather than declaring anything safe.
|
|
189
|
+
def unauthenticated_endpoints
|
|
190
|
+
parent = Janela.parent_controller.safe_constantize
|
|
191
|
+
return unless parent
|
|
192
|
+
|
|
193
|
+
filters = parent._process_action_callbacks.map(&:filter).map(&:to_s)
|
|
194
|
+
return if filters.any? { |filter| filter.match?(/authenticat|require_user|require_login|login_required/) }
|
|
195
|
+
|
|
196
|
+
Finding.new(severity: :warning,
|
|
197
|
+
summary: "no authentication filter found on #{parent}",
|
|
198
|
+
detail: " Janela's controllers inherit #{parent}, so they are as public as it is,\n" \
|
|
199
|
+
" and this check only reads its filters: if you authenticate another way\n" \
|
|
200
|
+
" this is a false alarm. Otherwise see Securing dashboards in the README.")
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def janela_models
|
|
204
|
+
Rails.application.eager_load!
|
|
205
|
+
ActiveRecord::Base.descendants.select { |model| model.respond_to?(:janela) && model.janela }
|
|
206
|
+
rescue StandardError
|
|
207
|
+
[]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def source_files
|
|
211
|
+
@source_files ||= SEARCHED.flat_map do |directory|
|
|
212
|
+
path = @root.join(directory)
|
|
213
|
+
next [] unless path.directory?
|
|
214
|
+
|
|
215
|
+
path.glob("**/*").select { |file| file.file? && READABLE.any? { |extension| file.to_s.end_with?(extension) } }
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
data/lib/janela/engine.rb
CHANGED
|
@@ -8,10 +8,23 @@ module Janela
|
|
|
8
8
|
ActiveSupport.on_load(:active_record) { extend Janela::Model }
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
# isolate_namespace keeps engine helpers out of the host, but the
|
|
11
|
+
# isolate_namespace keeps engine helpers out of the host, but the frame
|
|
12
12
|
# helpers are the engine's public API and belong in the host's views.
|
|
13
13
|
initializer "janela.helpers" do
|
|
14
|
-
ActiveSupport.on_load(:action_view) { include Janela::
|
|
14
|
+
ActiveSupport.on_load(:action_view) { include Janela::FramesHelper }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# janela_frame renders the engine's own partials from a host's page, and
|
|
18
|
+
# an engine's views are otherwise only on the lookup path of its own
|
|
19
|
+
# controllers.
|
|
20
|
+
initializer "janela.views" do
|
|
21
|
+
ActiveSupport.on_load(:action_controller) { append_view_path Janela::Engine.root.join("app/views") }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Janela's own layout links janela.css, and a host on Sprockets serves it
|
|
25
|
+
# in production only if something declared it.
|
|
26
|
+
initializer "janela.assets" do |app|
|
|
27
|
+
app.config.assets.precompile << "janela.css" if app.config.respond_to?(:assets) && app.config.assets.respond_to?(:precompile)
|
|
15
28
|
end
|
|
16
29
|
|
|
17
30
|
initializer "janela.importmap", before: "importmap" do |app|
|
data/lib/janela/measure.rb
CHANGED
|
@@ -4,22 +4,34 @@ module Janela
|
|
|
4
4
|
# Aggregates whose answer is a number, so a boolean column would have its
|
|
5
5
|
# result cast back to true or false by ActiveRecord.
|
|
6
6
|
NUMERIC = %i[sum average].freeze
|
|
7
|
+
# Declared beside the aggregate, so they are taken out before the one
|
|
8
|
+
# remaining option is read as the aggregate.
|
|
9
|
+
FORMATS = %i[precision prefix suffix].freeze
|
|
10
|
+
# Where neither the aggregate nor the column says how many decimal places
|
|
11
|
+
# the measure means, two: enough for a rate or a currency, and short of
|
|
12
|
+
# the noise a float carries (issue #25).
|
|
13
|
+
FALLBACK_PRECISION = 2
|
|
7
14
|
|
|
8
|
-
attr_reader :name, :aggregate, :column
|
|
15
|
+
attr_reader :name, :aggregate, :column, :prefix, :suffix
|
|
9
16
|
|
|
10
|
-
def self.build(name, **options)
|
|
17
|
+
def self.build(name, model: nil, **options)
|
|
18
|
+
format = options.extract!(*FORMATS)
|
|
11
19
|
aggregate, column = options.first
|
|
12
20
|
unless options.size == 1 && AGGREGATES.include?(aggregate)
|
|
13
21
|
raise Error, "measure #{name.inspect} needs exactly one of #{AGGREGATES.join(', ')}"
|
|
14
22
|
end
|
|
15
23
|
|
|
16
|
-
new(name, aggregate, column == true ? nil : column)
|
|
24
|
+
new(name, aggregate, column == true ? nil : column, model: model, **format)
|
|
17
25
|
end
|
|
18
26
|
|
|
19
|
-
def initialize(name, aggregate, column)
|
|
27
|
+
def initialize(name, aggregate, column, model: nil, precision: nil, prefix: nil, suffix: nil)
|
|
20
28
|
@name = name
|
|
21
29
|
@aggregate = aggregate
|
|
22
30
|
@column = column
|
|
31
|
+
@model = model
|
|
32
|
+
@precision = precision!(precision)
|
|
33
|
+
@prefix = prefix
|
|
34
|
+
@suffix = suffix
|
|
23
35
|
end
|
|
24
36
|
|
|
25
37
|
def apply(relation)
|
|
@@ -31,5 +43,54 @@ module Janela
|
|
|
31
43
|
def sql_alias
|
|
32
44
|
"#{aggregate}_#{column || 'all'}"
|
|
33
45
|
end
|
|
46
|
+
|
|
47
|
+
# How many decimal places this measure means. Counting rows has none, and
|
|
48
|
+
# a decimal column already declares its own scale, so money and counts
|
|
49
|
+
# read correctly with nothing declared at all (ADR 020).
|
|
50
|
+
def precision
|
|
51
|
+
@precision || column_scale || FALLBACK_PRECISION
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Rendering, never rounding: the number itself reaches a snapshot and a
|
|
55
|
+
# comparison at full precision, so a stored pane reads back under whatever
|
|
56
|
+
# format is declared later (ADR 009). Anything that is not a number is
|
|
57
|
+
# left alone, since minimum of a string is still that string.
|
|
58
|
+
def format(value)
|
|
59
|
+
return "" if value.nil?
|
|
60
|
+
return value.to_s unless value.is_a?(Numeric)
|
|
61
|
+
|
|
62
|
+
"#{prefix}#{rounded(value)}#{suffix}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
def rounded(value)
|
|
67
|
+
ActiveSupport::NumberHelper.number_to_rounded(value, precision: precision,
|
|
68
|
+
delimiter: I18n.t("number.format.delimiter", default: ","))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Asking the schema rather than the value: one bucket of a measure can
|
|
72
|
+
# land on a whole number without the measure being a whole number.
|
|
73
|
+
def column_scale
|
|
74
|
+
return 0 if aggregate == :count || column.nil?
|
|
75
|
+
|
|
76
|
+
type = @model&.type_for_attribute(column)
|
|
77
|
+
return if type.nil?
|
|
78
|
+
return 0 if type.type == :integer && aggregate != :average
|
|
79
|
+
|
|
80
|
+
type.scale if type.respond_to?(:scale)
|
|
81
|
+
rescue ActiveRecord::ActiveRecordError
|
|
82
|
+
nil # no database to ask yet
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def precision!(value)
|
|
86
|
+
return if value.nil?
|
|
87
|
+
|
|
88
|
+
places = Integer(value, exception: false)
|
|
89
|
+
unless places&.between?(0, 10)
|
|
90
|
+
raise Error, "measure #{name.inspect} takes a precision of 0 to 10 decimal places, not #{value.inspect}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
places
|
|
94
|
+
end
|
|
34
95
|
end
|
|
35
96
|
end
|
data/lib/janela/version.rb
CHANGED
data/lib/janela.rb
CHANGED
|
@@ -9,6 +9,7 @@ require "janela/model"
|
|
|
9
9
|
require "janela/definition"
|
|
10
10
|
require "janela/measure"
|
|
11
11
|
require "janela/dimension"
|
|
12
|
+
require "janela/doctor"
|
|
12
13
|
|
|
13
14
|
module Janela
|
|
14
15
|
class Error < StandardError; end
|
|
@@ -23,6 +24,12 @@ module Janela
|
|
|
23
24
|
# and authorisation apply to dashboards with no configuration.
|
|
24
25
|
mattr_accessor :parent_controller, default: "ApplicationController"
|
|
25
26
|
|
|
27
|
+
# Checks janela:doctor should not report, by the name it prints beside each
|
|
28
|
+
# finding. A check that is a false alarm for one application stays a false
|
|
29
|
+
# alarm, and that is a judgement made once at boot, which is what a setting
|
|
30
|
+
# is for (ADR 021).
|
|
31
|
+
mattr_accessor :silenced_checks, default: []
|
|
32
|
+
|
|
26
33
|
# Only models that declare a janela block are addressable over HTTP, keyed by
|
|
27
34
|
# the route key that appears in pane URLs (orders, sales_orders). Names are
|
|
28
35
|
# stored rather than classes so a reloaded model leaves nothing stale behind.
|
|
@@ -34,6 +41,16 @@ module Janela
|
|
|
34
41
|
registry[model.model_name.route_key] = model.name
|
|
35
42
|
end
|
|
36
43
|
|
|
44
|
+
# Every model that declares a janela block, for a form that offers a choice
|
|
45
|
+
# of them. Eager loading first, because a model nobody has referenced yet has
|
|
46
|
+
# not registered. A name that no longer resolves is left out rather than
|
|
47
|
+
# raised on: a model renamed or deleted in development leaves its old key
|
|
48
|
+
# here until a restart, and a form offering it would fail to draw at all.
|
|
49
|
+
def self.definitions
|
|
50
|
+
Rails.application.eager_load!
|
|
51
|
+
registry.sort.filter_map { |_route_key, class_name| class_name.safe_constantize&.janela }
|
|
52
|
+
end
|
|
53
|
+
|
|
37
54
|
def self.definition!(route_key)
|
|
38
55
|
# In development a model is only registered once autoloaded, so a cold
|
|
39
56
|
# lookup loads the app rather than constantizing an unvetted parameter.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: janela
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jay Killeen
|
|
@@ -95,21 +95,45 @@ files:
|
|
|
95
95
|
- CHANGELOG.md
|
|
96
96
|
- LICENSE.txt
|
|
97
97
|
- README.md
|
|
98
|
+
- UPGRADING.md
|
|
98
99
|
- app/assets/javascripts/janela/chart_controller.js
|
|
99
|
-
- app/assets/javascripts/janela/
|
|
100
|
+
- app/assets/javascripts/janela/frame_controller.js
|
|
100
101
|
- app/assets/javascripts/janela/vendor/chart.js
|
|
102
|
+
- app/assets/stylesheets/janela.css
|
|
101
103
|
- app/controllers/janela/application_controller.rb
|
|
104
|
+
- app/controllers/janela/frames_controller.rb
|
|
102
105
|
- app/controllers/janela/panes_controller.rb
|
|
103
|
-
- app/controllers/janela/
|
|
104
|
-
- app/
|
|
106
|
+
- app/controllers/janela/queries_controller.rb
|
|
107
|
+
- app/controllers/janela/snapshot_queries_controller.rb
|
|
108
|
+
- app/helpers/janela/frames_helper.rb
|
|
105
109
|
- app/jobs/janela/snapshot_job.rb
|
|
110
|
+
- app/models/janela/frame.rb
|
|
106
111
|
- app/models/janela/pane.rb
|
|
112
|
+
- app/models/janela/query.rb
|
|
107
113
|
- app/models/janela/snapshot.rb
|
|
114
|
+
- app/views/janela/frames/_card.html.erb
|
|
115
|
+
- app/views/janela/frames/_form.html.erb
|
|
116
|
+
- app/views/janela/frames/_frame.html.erb
|
|
117
|
+
- app/views/janela/frames/_pane.html.erb
|
|
118
|
+
- app/views/janela/frames/edit.html.erb
|
|
119
|
+
- app/views/janela/frames/index.html.erb
|
|
120
|
+
- app/views/janela/frames/new.html.erb
|
|
121
|
+
- app/views/janela/frames/show.html.erb
|
|
122
|
+
- app/views/janela/panes/_form.html.erb
|
|
123
|
+
- app/views/janela/panes/_row.html.erb
|
|
124
|
+
- app/views/janela/panes/edit.html.erb
|
|
125
|
+
- app/views/janela/panes/new.html.erb
|
|
108
126
|
- app/views/janela/panes/show.html.erb
|
|
127
|
+
- app/views/janela/queries/_query.html.erb
|
|
128
|
+
- app/views/janela/queries/show.html.erb
|
|
129
|
+
- app/views/janela/shared/_errors.html.erb
|
|
109
130
|
- app/views/layouts/janela/application.html.erb
|
|
110
131
|
- config/importmap.rb
|
|
132
|
+
- config/locales/en.yml
|
|
111
133
|
- config/routes.rb
|
|
112
134
|
- db/migrate/20260915000001_create_janela_snapshots.rb
|
|
135
|
+
- db/migrate/20260916000001_create_janela_frames.rb
|
|
136
|
+
- db/migrate/20260916000002_create_janela_panes.rb
|
|
113
137
|
- docs/decisions/001-built-to-be-forked.md
|
|
114
138
|
- docs/decisions/002-measures-and-dimensions-over-ransack.md
|
|
115
139
|
- docs/decisions/003-cross-filtering-with-turbo-frames.md
|
|
@@ -121,14 +145,27 @@ files:
|
|
|
121
145
|
- docs/decisions/009-snapshots.md
|
|
122
146
|
- docs/decisions/010-agent-guidance-ships-the-agent-waits.md
|
|
123
147
|
- docs/decisions/011-panes-do-not-render-in-the-host-layout.md
|
|
148
|
+
- docs/decisions/012-frames-and-panes-are-data.md
|
|
149
|
+
- docs/decisions/013-naming-and-addressing-frames.md
|
|
150
|
+
- docs/decisions/014-corrections-before-frames-are-built.md
|
|
151
|
+
- docs/decisions/015-how-breaking-change-is-communicated.md
|
|
152
|
+
- docs/decisions/016-the-styling-vocabulary.md
|
|
153
|
+
- docs/decisions/017-janela-owns-no-data-store.md
|
|
154
|
+
- docs/decisions/018-a-table-is-the-universal-renderer.md
|
|
155
|
+
- docs/decisions/019-a-created-frame-asks-the-host-who-owns-it.md
|
|
156
|
+
- docs/decisions/020-formatting-belongs-to-the-measure.md
|
|
157
|
+
- docs/decisions/021-a-check-has-a-name-a-host-can-silence.md
|
|
124
158
|
- docs/decisions/INDEX.md
|
|
159
|
+
- docs/multi-tenancy.md
|
|
125
160
|
- lib/janela.rb
|
|
126
161
|
- lib/janela/definition.rb
|
|
127
162
|
- lib/janela/dimension.rb
|
|
163
|
+
- lib/janela/doctor.rb
|
|
128
164
|
- lib/janela/engine.rb
|
|
129
165
|
- lib/janela/measure.rb
|
|
130
166
|
- lib/janela/model.rb
|
|
131
167
|
- lib/janela/version.rb
|
|
168
|
+
- lib/tasks/janela.rake
|
|
132
169
|
homepage: https://github.com/retail-tasker/janela
|
|
133
170
|
licenses:
|
|
134
171
|
- MIT
|
|
@@ -138,6 +175,19 @@ metadata:
|
|
|
138
175
|
changelog_uri: https://github.com/retail-tasker/janela/blob/main/CHANGELOG.md
|
|
139
176
|
bug_tracker_uri: https://github.com/retail-tasker/janela/issues
|
|
140
177
|
rubygems_mfa_required: 'true'
|
|
178
|
+
post_install_message: |
|
|
179
|
+
Janela 0.3.0 needs two things from you. Run the migrations, because the
|
|
180
|
+
mount root now serves an index of frames:
|
|
181
|
+
|
|
182
|
+
bin/rails janela:install:migrations && bin/rails db:migrate
|
|
183
|
+
|
|
184
|
+
Then rename the Stimulus controller you register, the helper that wraps
|
|
185
|
+
your panes, and a few constants.
|
|
186
|
+
|
|
187
|
+
Steps: UPGRADING.md in this gem, or
|
|
188
|
+
https://github.com/retail-tasker/janela/blob/main/UPGRADING.md
|
|
189
|
+
|
|
190
|
+
Then run: bin/rails janela:doctor
|
|
141
191
|
rdoc_options: []
|
|
142
192
|
require_paths:
|
|
143
193
|
- lib
|