janela 0.14.0 → 0.16.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 +33 -0
- data/README.md +26 -8
- data/UPGRADING.md +59 -0
- data/app/assets/javascripts/janela/chart_controller.js +148 -42
- data/app/assets/stylesheets/janela.css +7 -0
- data/app/controllers/janela/panes_controller.rb +2 -2
- data/app/controllers/janela/queries_controller.rb +12 -0
- data/app/controllers/janela/snapshot_queries_controller.rb +2 -0
- data/app/helpers/janela/frames_helper.rb +37 -9
- data/app/models/janela/pane.rb +33 -2
- data/app/models/janela/query.rb +252 -27
- data/app/models/janela/snapshot.rb +2 -2
- data/app/views/janela/frames/show.html.erb +2 -2
- data/app/views/janela/panes/_form.html.erb +53 -0
- data/app/views/janela/queries/_query.html.erb +18 -5
- data/config/locales/en.yml +26 -1
- data/db/migrate/20261013000001_add_sort_to_janela_panes.rb +7 -0
- data/db/migrate/20261029000001_add_series_to_janela_panes.rb +9 -0
- data/db/migrate/20261029000003_add_ignore_unknown_filters_to_janela_panes.rb +8 -0
- data/docs/agents.md +33 -2
- data/docs/decisions/018-a-table-is-the-universal-renderer.md +1 -0
- data/docs/decisions/053-an-agent-reaches-janela-through-tools-the-host-scopes.md +1 -0
- data/docs/decisions/054-a-chart-reads-numbers-the-way-its-measure-formats-them.md +1 -1
- data/docs/decisions/055-a-category-pane-can-be-sorted-by-its-measure-either-way-or-by-its-label.md +204 -0
- data/docs/decisions/056-the-demo-counts-its-own-visits-and-an-agent-arranges-the-dashboard.md +51 -0
- data/docs/decisions/057-a-host-is-told-about-the-agent-tools-and-a-generator-sets-them-up.md +197 -0
- data/docs/decisions/058-the-deployed-demo-is-reached-over-ssh-and-its-visits-frame-survives-a-reset.md +94 -0
- data/docs/decisions/059-a-host-can-tell-janelas-own-pages-that-its-layout-draws-charts.md +91 -0
- data/docs/decisions/060-the-demo-tells-search-engines-what-it-is-and-shows-its-own-adoption.md +79 -0
- data/docs/decisions/061-the-demo-speaks-brazilian-portuguese-under-a-prefix-and-the-gem-stays-english.md +118 -0
- data/docs/decisions/062-a-time-pane-can-accumulate-and-carry-more-series-on-two-axes.md +282 -0
- data/docs/decisions/063-a-pane-can-ignore-a-filter-its-model-cannot-apply.md +147 -0
- data/docs/decisions/INDEX.md +25 -16
- data/docs/roadmap.md +10 -4
- data/lib/generators/janela/mcp/mcp_generator.rb +51 -0
- data/lib/generators/janela/mcp/templates/janela-mcp.tt +50 -0
- data/lib/janela/definition.rb +73 -4
- data/lib/janela/dimension.rb +9 -1
- data/lib/janela/doctor.rb +10 -1
- data/lib/janela/measure.rb +8 -0
- data/lib/janela/tools.rb +37 -4
- data/lib/janela/version.rb +1 -1
- data/lib/janela.rb +12 -0
- metadata +22 -8
data/lib/janela/definition.rb
CHANGED
|
@@ -4,6 +4,11 @@ module Janela
|
|
|
4
4
|
# applied when a host asks for none, and as the bound on one filter's values.
|
|
5
5
|
MAXIMUM = 1000
|
|
6
6
|
|
|
7
|
+
# How a category pane is ordered (ADR 055): by its measure, largest or
|
|
8
|
+
# smallest first, or by the dimension's own value, ascending or descending.
|
|
9
|
+
# The first is what a pane did before there was a choice.
|
|
10
|
+
SORTS = %w[desc asc label label_desc].freeze
|
|
11
|
+
|
|
7
12
|
attr_reader :model, :measures, :dimensions
|
|
8
13
|
|
|
9
14
|
# The class whose janela block this is, which a subclass shares with the
|
|
@@ -44,8 +49,9 @@ module Janela
|
|
|
44
49
|
# keys: narrows a categorical grouping to the labels a companion column is
|
|
45
50
|
# being fetched for (ADR 051), so it is the primary's rows and not the
|
|
46
51
|
# companion's own top 1000.
|
|
47
|
-
def query(measure_name, by: nil, where: {}, on: nil, granularity: nil, limit: nil, keys: nil)
|
|
52
|
+
def query(measure_name, by: nil, where: {}, on: nil, granularity: nil, limit: nil, keys: nil, sort: nil)
|
|
48
53
|
measure = measure!(measure_name)
|
|
54
|
+
sort = sort!(sort)
|
|
49
55
|
relation = filter(on || model.all, where)
|
|
50
56
|
return measure.apply(relation) if by.nil?
|
|
51
57
|
|
|
@@ -57,7 +63,7 @@ module Janela
|
|
|
57
63
|
bucketed(measure, dimension, relation, granularity).transform_keys { |bucket| dimension.label(bucket, granularity) }
|
|
58
64
|
else
|
|
59
65
|
relation = within_keys(relation, dimension, keys) if keys
|
|
60
|
-
grouped = relation.group(dimension.attribute).order(Arel.sql(
|
|
66
|
+
grouped = relation.group(dimension.attribute).order(Arel.sql(ordering(measure, dimension, sort)))
|
|
61
67
|
grouped = grouped.limit(limit ? limit!(limit) : MAXIMUM)
|
|
62
68
|
measure.apply(grouped).transform_keys { |value| value.nil? ? Dimension::NONE : value }
|
|
63
69
|
end
|
|
@@ -124,6 +130,15 @@ module Janela
|
|
|
124
130
|
nil # no database to ask yet; a query will raise on its own if it cannot run
|
|
125
131
|
end
|
|
126
132
|
|
|
133
|
+
# nil is the default, so a caller that never heard of a sort is unchanged.
|
|
134
|
+
def sort!(value)
|
|
135
|
+
return "desc" if value.blank?
|
|
136
|
+
|
|
137
|
+
value = value.to_s
|
|
138
|
+
raise BadRequest, "sort must be one of #{SORTS.join(', ')}, got #{value.inspect}" unless SORTS.include?(value)
|
|
139
|
+
value
|
|
140
|
+
end
|
|
141
|
+
|
|
127
142
|
def limit!(value)
|
|
128
143
|
limit = Integer(value, exception: false)
|
|
129
144
|
raise BadRequest, "limit must be a whole number from 1 to #{MAXIMUM}, got #{value.inspect}" unless limit&.between?(1, MAXIMUM)
|
|
@@ -138,6 +153,20 @@ module Janela
|
|
|
138
153
|
dimensions.values.filter_map(&:through).map(&:to_s).uniq
|
|
139
154
|
end
|
|
140
155
|
|
|
156
|
+
# The keys of these params Ransack would apply to this model, read by the
|
|
157
|
+
# same helper reject_dropped_filters! uses so the two cannot disagree about
|
|
158
|
+
# which keys are the model's. A pane that ignores unknown filters keeps
|
|
159
|
+
# these, and also a key on a dimension the model declares with a predicate
|
|
160
|
+
# Ransack does not know (path_inn), which is a misuse of a real key and is
|
|
161
|
+
# left for the filter to refuse (ADR 025, ADR 063). Only a key whose
|
|
162
|
+
# attribute the model does not declare at all is dropped.
|
|
163
|
+
def applicable(params)
|
|
164
|
+
return params if params.empty?
|
|
165
|
+
|
|
166
|
+
dropped = unapplied_keys(model.ransack(params.to_h), params)
|
|
167
|
+
params.reject { |key, _| dropped.include?(key) && !misspelt_declared_key?(key) }
|
|
168
|
+
end
|
|
169
|
+
|
|
141
170
|
# A filter the host fixed for a render (ADR 040), applied as its own
|
|
142
171
|
# condition before the reader's, so the reader's can only narrow inside
|
|
143
172
|
# it. The same bounds as any filter: declared dimensions, ADR 025's
|
|
@@ -147,6 +176,24 @@ module Janela
|
|
|
147
176
|
end
|
|
148
177
|
|
|
149
178
|
private
|
|
179
|
+
# The label comes second in a measure order so that groups tied on the
|
|
180
|
+
# measure come out the same way every time, and a limit between them
|
|
181
|
+
# always shows the same one. IS NULL puts the (none) group last wherever
|
|
182
|
+
# the database would put it: SQLite sorts NULL first in an ascending
|
|
183
|
+
# order and PostgreSQL last, and a label order would otherwise differ by
|
|
184
|
+
# host (ADR 055). The database sorts the column as its own type, so a
|
|
185
|
+
# number is in numeric order and a date in date order.
|
|
186
|
+
def ordering(measure, dimension, sort)
|
|
187
|
+
label = dimension.quoted_column
|
|
188
|
+
by_label = "#{label} IS NULL, #{label}"
|
|
189
|
+
case sort
|
|
190
|
+
when "asc" then "#{measure.order_by} ASC, #{by_label} ASC"
|
|
191
|
+
when "label" then "#{by_label} ASC"
|
|
192
|
+
when "label_desc" then "#{by_label} DESC"
|
|
193
|
+
else "#{measure.order_by} DESC, #{by_label} ASC"
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
150
197
|
def within_keys(relation, dimension, keys)
|
|
151
198
|
labels = keys - [ Dimension::NONE ]
|
|
152
199
|
condition = dimension.attribute.in(labels)
|
|
@@ -203,14 +250,36 @@ module Janela
|
|
|
203
250
|
# Ransack silently discards conditions an allowlist does not permit,
|
|
204
251
|
# which would quietly return unfiltered numbers.
|
|
205
252
|
def reject_dropped_filters!(search, params)
|
|
206
|
-
|
|
207
|
-
dropped = params.keys.reject { |key| applied.any? { |name| key.to_s.start_with?(name) } }
|
|
253
|
+
dropped = unapplied_keys(search, params)
|
|
208
254
|
return if dropped.empty?
|
|
209
255
|
|
|
210
256
|
raise BadRequest, "#{model} does not allow filtering on #{dropped.join(', ')}. " \
|
|
211
257
|
"Declare a janela dimension, or add it to ransackable_attributes."
|
|
212
258
|
end
|
|
213
259
|
|
|
260
|
+
# The keys Ransack did not apply: those that are not exactly an attribute
|
|
261
|
+
# it applied followed by a predicate it knows. Exact, so status_code_in
|
|
262
|
+
# is not taken for status because it starts with it.
|
|
263
|
+
def unapplied_keys(search, params)
|
|
264
|
+
applied = search.conditions.flat_map { |condition| condition.attributes.map(&:name) }
|
|
265
|
+
params.keys.reject { |key| applied.any? { |name| predicate_of?(key, name) } }
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def predicate_of?(key, name)
|
|
269
|
+
prefix = "#{name}_"
|
|
270
|
+
key.to_s.start_with?(prefix) && Ransack.predicates.has_key?(key.to_s.delete_prefix(prefix))
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# A key Ransack did not apply that is a declared dimension and a suffix
|
|
274
|
+
# that is not another attribute with a known predicate on it: path_inn is
|
|
275
|
+
# path with a typo, status_code_in is a different attribute entirely.
|
|
276
|
+
def misspelt_declared_key?(key)
|
|
277
|
+
dimensions.values.reject(&:through).any? do |dimension|
|
|
278
|
+
rest = key.to_s.delete_prefix("#{dimension.ransack_name}_")
|
|
279
|
+
rest != key.to_s && Ransack.predicates.keys.none? { |predicate| rest.end_with?("_#{predicate}") }
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
214
283
|
# An allowed attribute still reaches every predicate Ransack knows,
|
|
215
284
|
# including _matches, an arbitrary LIKE pattern (ADR 025). A dashboard
|
|
216
285
|
# asks in only the predicates its kind of dimension needs.
|
data/lib/janela/dimension.rb
CHANGED
|
@@ -26,11 +26,19 @@ module Janela
|
|
|
26
26
|
"hour" => ->(t) { t.strftime("%Y-%m-%d %H:00") },
|
|
27
27
|
"day" => ->(t) { t.strftime("%Y-%m-%d") },
|
|
28
28
|
"week" => ->(t) { t.strftime("%Y-%m-%d") },
|
|
29
|
-
"month" => ->(t) { t
|
|
29
|
+
"month" => ->(t) { month_label(t) },
|
|
30
30
|
"quarter" => ->(t) { "Q#{(t.month - 1) / 3 + 1} #{t.year}" },
|
|
31
31
|
"year" => ->(t) { t.strftime("%Y") }
|
|
32
32
|
}.freeze
|
|
33
33
|
|
|
34
|
+
# The month in the reader's language when the host has that language's month
|
|
35
|
+
# names, and in English when it has not: strftime is English only, and a host
|
|
36
|
+
# that never set a locale must see what it always did (ADR 061).
|
|
37
|
+
def self.month_label(time)
|
|
38
|
+
names = I18n.t("date.abbr_month_names", default: nil)
|
|
39
|
+
names.is_a?(Array) && names[time.month] ? "#{names[time.month]} #{time.year}" : time.strftime("%b %Y")
|
|
40
|
+
end
|
|
41
|
+
|
|
34
42
|
attr_reader :name, :model, :through, :column, :granularity
|
|
35
43
|
|
|
36
44
|
# A dimension is named for what it means on the dashboard and reads a
|
data/lib/janela/doctor.rb
CHANGED
|
@@ -38,6 +38,7 @@ module Janela
|
|
|
38
38
|
}.freeze
|
|
39
39
|
|
|
40
40
|
SEARCHED = %w[app config lib].freeze
|
|
41
|
+
AGENT_SERVER = "bin/janela-mcp".freeze
|
|
41
42
|
READABLE = %w[.rb .erb .js .erb.html .html.erb .haml .slim .yml].freeze
|
|
42
43
|
|
|
43
44
|
def initialize(root)
|
|
@@ -66,6 +67,14 @@ module Janela
|
|
|
66
67
|
# Said out loud every run, because a silence nobody remembers is how a
|
|
67
68
|
# real finding goes unread.
|
|
68
69
|
out.puts "Silenced: #{silenced.map(&:code).uniq.join(', ')}." if silenced.any?
|
|
70
|
+
|
|
71
|
+
# Not a finding: it is optional, so it is never counted, silenced or
|
|
72
|
+
# allowed to change the result. It says what it saw, which is a missing
|
|
73
|
+
# file, and is printed on a clean run too (ADR 057).
|
|
74
|
+
unless @root.join(AGENT_SERVER).exist?
|
|
75
|
+
out.puts
|
|
76
|
+
out.puts "Optional: let an agent arrange dashboards. bin/rails generate janela:mcp (docs/agents.md)."
|
|
77
|
+
end
|
|
69
78
|
findings.none? { |finding| finding.severity == :error }
|
|
70
79
|
end
|
|
71
80
|
|
|
@@ -148,7 +157,7 @@ module Janela
|
|
|
148
157
|
# the same thing twice.
|
|
149
158
|
ADDED_COLUMNS = {
|
|
150
159
|
"Janela::Frame" => %w[key default_model default_where],
|
|
151
|
-
"Janela::Pane" => %w[kind heading body link partial height prominence companions value_labels],
|
|
160
|
+
"Janela::Pane" => %w[kind heading body link partial height prominence companions value_labels sort cumulative series ignore_unknown_filters],
|
|
152
161
|
"Janela::Snapshot" => %w[owner_type owner_id]
|
|
153
162
|
}.freeze
|
|
154
163
|
|
data/lib/janela/measure.rb
CHANGED
|
@@ -46,6 +46,14 @@ module Janela
|
|
|
46
46
|
aggregate == :ratio
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
# A count or a sum adds up across buckets: a running total of it means
|
|
50
|
+
# something, and a bucket with no rows is zero. An average, a minimum, a
|
|
51
|
+
# maximum or a ratio does not, and a bucket with no rows has no value
|
|
52
|
+
# (ADR 062).
|
|
53
|
+
def additive?
|
|
54
|
+
%i[count sum].include?(aggregate)
|
|
55
|
+
end
|
|
56
|
+
|
|
49
57
|
def apply(relation)
|
|
50
58
|
return relation.average(ratio_expression) if ratio?
|
|
51
59
|
|
data/lib/janela/tools.rb
CHANGED
|
@@ -17,11 +17,14 @@ module Janela
|
|
|
17
17
|
|
|
18
18
|
# The attributes of a pane a caller may set: the ones the README names. The
|
|
19
19
|
# frame, the position and the timestamps are not an agent's to write.
|
|
20
|
-
PANE_ATTRIBUTES = %w[kind model measure dimension renderer granularity limit span title height prominence
|
|
21
|
-
value_labels companions heading body link partial].freeze
|
|
20
|
+
PANE_ATTRIBUTES = %w[kind model measure dimension renderer granularity limit sort span title height prominence
|
|
21
|
+
value_labels companions cumulative series ignore_unknown_filters heading body link partial].freeze
|
|
22
|
+
# A frame's grid is the only part of a frame an agent may change (ADR 057).
|
|
23
|
+
# Its name, key and owner are the host's to say (ADR 033, ADR 041).
|
|
24
|
+
FRAME_ATTRIBUTES = %w[columns gap].freeze
|
|
22
25
|
DIRECTIONS = %w[up down].freeze
|
|
23
26
|
READS = %w[describe_vocabulary list_frames get_frame read_pane].freeze
|
|
24
|
-
WRITES = %w[add_pane update_pane remove_pane move_pane].freeze
|
|
27
|
+
WRITES = %w[add_pane update_pane remove_pane move_pane update_frame].freeze
|
|
25
28
|
|
|
26
29
|
def initialize(scope: nil, write: false)
|
|
27
30
|
unless scope.respond_to?(:call)
|
|
@@ -64,7 +67,8 @@ module Janela
|
|
|
64
67
|
{ models: Janela.definitions.map { |definition| model_hash(definition) },
|
|
65
68
|
renderers: Janela.renderers.map(&:to_s),
|
|
66
69
|
granularities: Janela.granularities.map(&:to_s),
|
|
67
|
-
limits: Pane::OFFERED_LIMITS
|
|
70
|
+
limits: Pane::OFFERED_LIMITS,
|
|
71
|
+
sorts: Janela.sorts }
|
|
68
72
|
end
|
|
69
73
|
|
|
70
74
|
def list_frames
|
|
@@ -88,6 +92,12 @@ module Janela
|
|
|
88
92
|
reading.merge(values: result.transform_values(&:to_f), formatted: result.transform_values { |value| query.format(value) })
|
|
89
93
|
end
|
|
90
94
|
|
|
95
|
+
def update_frame(frame_id:, **attributes)
|
|
96
|
+
frame = frame_in_scope(frame_id)
|
|
97
|
+
frame.update(frame_attributes(attributes)) || refuse(frame)
|
|
98
|
+
frame_hash(frame)
|
|
99
|
+
end
|
|
100
|
+
|
|
91
101
|
def add_pane(frame_id:, **attributes)
|
|
92
102
|
frame = frame_in_scope(frame_id)
|
|
93
103
|
pane = frame.panes.build(pane_attributes(attributes))
|
|
@@ -143,6 +153,13 @@ module Janela
|
|
|
143
153
|
attributes.transform_keys(&:to_s)
|
|
144
154
|
end
|
|
145
155
|
|
|
156
|
+
def frame_attributes(attributes)
|
|
157
|
+
unknown = attributes.keys.map(&:to_s) - FRAME_ATTRIBUTES
|
|
158
|
+
raise BadRequest, "not a frame attribute an agent may change: #{unknown.join(', ')}. A frame's #{FRAME_ATTRIBUTES.join(' and ')} can be changed." if unknown.any?
|
|
159
|
+
|
|
160
|
+
attributes.transform_keys(&:to_s)
|
|
161
|
+
end
|
|
162
|
+
|
|
146
163
|
def refuse(record)
|
|
147
164
|
raise BadRequest, record.errors.full_messages.to_sentence
|
|
148
165
|
end
|
|
@@ -186,6 +203,11 @@ module Janela
|
|
|
186
203
|
"remove_pane" => tool("remove_pane", required: %w[pane_id],
|
|
187
204
|
description: "Remove a pane and close the gap it leaves.",
|
|
188
205
|
properties: { pane_id: { type: "integer" } }),
|
|
206
|
+
"update_frame" => tool("update_frame", required: %w[frame_id],
|
|
207
|
+
description: "Change a frame's grid: how many columns it has and the gap between panes. Nothing else of a frame can be changed.",
|
|
208
|
+
properties: { frame_id: { type: "integer" },
|
|
209
|
+
columns: { type: "integer", minimum: Frame::COLUMNS.min, maximum: Frame::COLUMNS.max },
|
|
210
|
+
gap: { type: "integer", minimum: Frame::GAPS.min, maximum: Frame::GAPS.max } }),
|
|
189
211
|
"move_pane" => tool("move_pane", required: %w[pane_id direction],
|
|
190
212
|
description: "Move a pane one place up or down its frame.",
|
|
191
213
|
properties: { pane_id: { type: "integer" }, direction: { type: "string", enum: DIRECTIONS } })
|
|
@@ -205,12 +227,23 @@ module Janela
|
|
|
205
227
|
renderer: { type: "string", enum: Janela.renderers.map(&:to_s) },
|
|
206
228
|
granularity: { type: "string", enum: Janela.granularities.map(&:to_s) },
|
|
207
229
|
limit: { type: "integer", minimum: Pane::LIMITS.min, maximum: Pane::LIMITS.max },
|
|
230
|
+
sort: { type: "string", enum: Janela.sorts },
|
|
208
231
|
span: { type: "integer", minimum: Pane::SPANS.min, maximum: Pane::SPANS.max },
|
|
209
232
|
title: { type: "string" },
|
|
210
233
|
height: { type: "integer", enum: Pane::HEIGHTS.to_a },
|
|
211
234
|
prominence: { type: "integer", enum: Pane::PROMINENCES.to_a },
|
|
212
235
|
value_labels: { type: "boolean", description: "Draw each bar's value on a bar chart; other renderers ignore it" },
|
|
213
236
|
companions: { type: "array", items: { type: "string" } },
|
|
237
|
+
cumulative: { type: "boolean", description: "A running total of the measure across a time pane's buckets" },
|
|
238
|
+
series: { type: "array", description: "Up to #{Query::MAX_SERIES} more series on a time pane, each a measure over a time dimension of the same model",
|
|
239
|
+
items: { type: "object", required: %w[measure], additionalProperties: false,
|
|
240
|
+
properties: { measure: { type: "string" },
|
|
241
|
+
by: { type: "string", description: "A time dimension; the pane's own when left out" },
|
|
242
|
+
cumulative: { type: "boolean" },
|
|
243
|
+
as: { type: "string", enum: Query::CANVAS },
|
|
244
|
+
axis: { type: "string", enum: Query::AXES },
|
|
245
|
+
title: { type: "string" } } } },
|
|
246
|
+
ignore_unknown_filters: { type: "boolean", description: "Drop the reader's filters this pane's model cannot apply rather than refusing them, for a pane beside panes over another model" },
|
|
214
247
|
heading: { type: "string" },
|
|
215
248
|
body: { type: "string" },
|
|
216
249
|
link: { type: "string", description: "A path on this site, starting with /" },
|
data/lib/janela/version.rb
CHANGED
data/lib/janela.rb
CHANGED
|
@@ -70,6 +70,14 @@ module Janela
|
|
|
70
70
|
# whatever that host's layout says (ADR 023).
|
|
71
71
|
mattr_accessor :theme, default: nil
|
|
72
72
|
|
|
73
|
+
# Whether the engine's own frame page draws a bar or line pane as a chart.
|
|
74
|
+
# False is a table, because the engine's minimal layout loads no chart
|
|
75
|
+
# runtime and cannot know whether a host's layout does (ADR 018). A host that
|
|
76
|
+
# has given Janela's own pages a layout that registers the Stimulus
|
|
77
|
+
# controllers and loads Chart.js says so here (ADR 059). A host that sets it
|
|
78
|
+
# without the runtime gets an empty hole where the table was.
|
|
79
|
+
mattr_accessor :charts_on_own_pages, default: false
|
|
80
|
+
|
|
73
81
|
# Checks janela:doctor should not report, by the name it prints beside each
|
|
74
82
|
# finding. A check that is a false alarm for one application stays a false
|
|
75
83
|
# alarm, and that is a judgement made once at boot, which is what a setting
|
|
@@ -170,6 +178,10 @@ module Janela
|
|
|
170
178
|
# The steps a pane's height and prominence take, and how many companion
|
|
171
179
|
# columns a table may carry, for a page that offers them as controls
|
|
172
180
|
# (ADR 027, ADR 047, ADR 050, ADR 051).
|
|
181
|
+
def self.sorts
|
|
182
|
+
Definition::SORTS
|
|
183
|
+
end
|
|
184
|
+
|
|
173
185
|
def self.heights
|
|
174
186
|
Pane::HEIGHTS.to_a
|
|
175
187
|
end
|
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.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jay Killeen
|
|
@@ -147,6 +147,9 @@ files:
|
|
|
147
147
|
- db/migrate/20260930000002_add_prominence_to_janela_panes.rb
|
|
148
148
|
- db/migrate/20260930000003_add_companions_to_janela_panes.rb
|
|
149
149
|
- db/migrate/20261006000001_add_value_labels_to_janela_panes.rb
|
|
150
|
+
- db/migrate/20261013000001_add_sort_to_janela_panes.rb
|
|
151
|
+
- db/migrate/20261029000001_add_series_to_janela_panes.rb
|
|
152
|
+
- db/migrate/20261029000003_add_ignore_unknown_filters_to_janela_panes.rb
|
|
150
153
|
- docs/agents.md
|
|
151
154
|
- docs/composing.md
|
|
152
155
|
- docs/decisions/001-built-to-be-forked.md
|
|
@@ -203,11 +206,22 @@ files:
|
|
|
203
206
|
- docs/decisions/052-how-the-project-is-run.md
|
|
204
207
|
- docs/decisions/053-an-agent-reaches-janela-through-tools-the-host-scopes.md
|
|
205
208
|
- docs/decisions/054-a-chart-reads-numbers-the-way-its-measure-formats-them.md
|
|
209
|
+
- docs/decisions/055-a-category-pane-can-be-sorted-by-its-measure-either-way-or-by-its-label.md
|
|
210
|
+
- docs/decisions/056-the-demo-counts-its-own-visits-and-an-agent-arranges-the-dashboard.md
|
|
211
|
+
- docs/decisions/057-a-host-is-told-about-the-agent-tools-and-a-generator-sets-them-up.md
|
|
212
|
+
- docs/decisions/058-the-deployed-demo-is-reached-over-ssh-and-its-visits-frame-survives-a-reset.md
|
|
213
|
+
- docs/decisions/059-a-host-can-tell-janelas-own-pages-that-its-layout-draws-charts.md
|
|
214
|
+
- docs/decisions/060-the-demo-tells-search-engines-what-it-is-and-shows-its-own-adoption.md
|
|
215
|
+
- docs/decisions/061-the-demo-speaks-brazilian-portuguese-under-a-prefix-and-the-gem-stays-english.md
|
|
216
|
+
- docs/decisions/062-a-time-pane-can-accumulate-and-carry-more-series-on-two-axes.md
|
|
217
|
+
- docs/decisions/063-a-pane-can-ignore-a-filter-its-model-cannot-apply.md
|
|
206
218
|
- docs/decisions/INDEX.md
|
|
207
219
|
- docs/multi-tenancy.md
|
|
208
220
|
- docs/naming.md
|
|
209
221
|
- docs/roadmap.md
|
|
210
222
|
- docs/theming.md
|
|
223
|
+
- lib/generators/janela/mcp/mcp_generator.rb
|
|
224
|
+
- lib/generators/janela/mcp/templates/janela-mcp.tt
|
|
211
225
|
- lib/janela.rb
|
|
212
226
|
- lib/janela/definition.rb
|
|
213
227
|
- lib/janela/dimension.rb
|
|
@@ -229,17 +243,17 @@ metadata:
|
|
|
229
243
|
bug_tracker_uri: https://github.com/retail-tasker/janela/issues
|
|
230
244
|
rubygems_mfa_required: 'true'
|
|
231
245
|
post_install_message: |
|
|
232
|
-
Janela 0.
|
|
246
|
+
Janela 0.16.0: one migration, if you use stored frames.
|
|
233
247
|
|
|
234
|
-
A pane can now
|
|
235
|
-
|
|
248
|
+
A time pane can now be a running total and carry more series on two
|
|
249
|
+
axes (ADR 062), and a pane can ignore a filter its model cannot apply
|
|
250
|
+
(ADR 063). That adds nullable cumulative, series and
|
|
251
|
+
ignore_unknown_filters columns to janela_panes. Run
|
|
236
252
|
bin/rails janela:install:migrations and then bin/rails db:migrate. If
|
|
237
253
|
you never use stored frames there is nothing to do.
|
|
238
254
|
|
|
239
|
-
Also: a chart
|
|
240
|
-
|
|
241
|
-
pane Janela will not draw is now that pane's own sentence and no
|
|
242
|
-
longer an error for the whole host page.
|
|
255
|
+
Also: a line chart with value_labels now writes its latest value at
|
|
256
|
+
its end.
|
|
243
257
|
|
|
244
258
|
Steps: UPGRADING.md in this gem, or
|
|
245
259
|
https://github.com/retail-tasker/janela/blob/main/UPGRADING.md
|