janela 0.12.0 → 0.14.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 +36 -1
- data/README.md +31 -26
- data/UPGRADING.md +60 -1
- data/app/assets/javascripts/janela/chart_controller.js +54 -3
- data/app/assets/stylesheets/vitral.css +21 -0
- data/app/controllers/janela/application_controller.rb +2 -2
- data/app/controllers/janela/panes_controller.rb +1 -1
- data/app/controllers/janela/queries_controller.rb +1 -0
- data/app/controllers/janela/snapshot_queries_controller.rb +1 -0
- data/app/helpers/janela/frames_helper.rb +23 -5
- data/app/models/janela/pane.rb +1 -1
- data/app/models/janela/query.rb +30 -2
- data/app/views/janela/frames/_pane.html.erb +3 -2
- data/app/views/janela/panes/_form.html.erb +6 -0
- data/app/views/janela/queries/_query.html.erb +17 -15
- data/config/locales/en.yml +6 -0
- data/db/migrate/20261006000001_add_value_labels_to_janela_panes.rb +7 -0
- data/docs/agents.md +97 -0
- data/docs/composing.md +14 -14
- data/docs/decisions/020-formatting-belongs-to-the-measure.md +4 -1
- data/docs/decisions/053-an-agent-reaches-janela-through-tools-the-host-scopes.md +187 -0
- data/docs/decisions/054-a-chart-reads-numbers-the-way-its-measure-formats-them.md +221 -0
- data/docs/decisions/INDEX.md +11 -9
- data/docs/multi-tenancy.md +22 -4
- data/docs/roadmap.md +37 -13
- data/docs/theming.md +6 -4
- data/lib/janela/doctor.rb +66 -8
- data/lib/janela/tools.rb +221 -0
- data/lib/janela/version.rb +1 -1
- data/lib/janela.rb +16 -0
- metadata +16 -8
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-10-06
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Related: ADR 004, ADR 020, ADR 029, ADR 037, ADR 038, ADR 046, ADR 047, ADR 050, ADR 053
|
|
5
|
+
Triggers:
|
|
6
|
+
- a chart's axis showing a number the tooltip shows differently
|
|
7
|
+
- drawing text on a chart canvas, or adding a Chart.js plugin
|
|
8
|
+
- adding a boolean to a pane, a column to janela_panes, or a parameter to the pane URL
|
|
9
|
+
- a ratio, a currency or a percentage on a chart
|
|
10
|
+
- a chart too dense to label every bar
|
|
11
|
+
Topics: charts, formatting, measures, public-api, panes, urls
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# ADR 054: A Chart Reads Numbers the Way Its Measure Formats Them
|
|
15
|
+
|
|
16
|
+
## Context
|
|
17
|
+
|
|
18
|
+
#72 and #76, raised from a real install building a client report: a share
|
|
19
|
+
of rows by category, drawn as bars, with the percentage on each bar. Two
|
|
20
|
+
things stood in the way, and both are the same gap. A chart is the one
|
|
21
|
+
place a number reaches a reader without the measure's format applied
|
|
22
|
+
(ADR 020), except the tooltip, which a reader has to hover to see.
|
|
23
|
+
|
|
24
|
+
Both were reproduced against the demo on `main` (Chart.js 4.5.1), with the
|
|
25
|
+
chart built live and the drawn scale read from it.
|
|
26
|
+
|
|
27
|
+
**The axis is raw.** A bar chart of a ratio drew ticks `0, 0.1 ... 1.0`
|
|
28
|
+
beside tooltips reading `0.0%`. Revenue drew `0, 50,000 ... 500,000` beside
|
|
29
|
+
tooltips reading `$469,097.85`. `chart_controller.js` sets
|
|
30
|
+
`scales: { y: { beginAtZero: true } }` and formats only the tooltip.
|
|
31
|
+
|
|
32
|
+
**#72 said this was never decided. It was.** ADR 020 closes its Decision
|
|
33
|
+
with "The chart still plots raw numbers, because an axis is a scale and not
|
|
34
|
+
a label." That sentence is wrong for a reader, who reads the axis as labels
|
|
35
|
+
whatever it is to Chart.js. A ratio's axis at 0.5 against a tooltip at 50.0%
|
|
36
|
+
reads as two different numbers (ADR 038 keeps the stored value a fraction
|
|
37
|
+
for the data's sake, which is not a reason for the axis to show one). The
|
|
38
|
+
rest of ADR 020 stands: the number is never rounded or transformed, and the
|
|
39
|
+
measure is the layer that owns its format.
|
|
40
|
+
|
|
41
|
+
**Tick precision does not need a rule of Janela's own.** The worry in #72
|
|
42
|
+
was that ticks fall between data points, so the precision that suits a
|
|
43
|
+
value (`25.0%`) is wrong for a tick (`25%`). Chart.js's own numeric tick
|
|
44
|
+
formatter already derives its decimals from the spacing between ticks.
|
|
45
|
+
Calling it with a ratio's ticks scaled by 100 and appending `%` gave, on the
|
|
46
|
+
live chart:
|
|
47
|
+
|
|
48
|
+
| ratio data up to | ticks |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| 0.53 | `0%, 10% ... 60%` |
|
|
51
|
+
| 0.03 | `0%, 0.5%, 1% ... 3%` |
|
|
52
|
+
| 1 | `0% ... 100%` |
|
|
53
|
+
|
|
54
|
+
Whole percentages where the spacing is whole, half percentages only where
|
|
55
|
+
the ticks really are half a percent apart. One catch: scaling by 100 leaves
|
|
56
|
+
float noise (`3.5000000000000004%`), so the scaled value has to be rounded
|
|
57
|
+
before it is formatted.
|
|
58
|
+
|
|
59
|
+
**A bar label has to fit, and nothing guarantees it does.** Measured with a
|
|
60
|
+
label drawn at each bar's end:
|
|
61
|
+
|
|
62
|
+
- The tallest bar of a default chart touches the top of the plot area, so a
|
|
63
|
+
label above it is clipped. `scales.y.grace = "10%"` left 30px of
|
|
64
|
+
headroom in a 300px chart, and Chart.js 4.5.1 supports it.
|
|
65
|
+
- A label inside the bar was never an option: the shortest bars are about
|
|
66
|
+
2px tall.
|
|
67
|
+
- With 30 bars in a 600px chart a bar was 13px wide and a label such as
|
|
68
|
+
`$88,000.00` was 60px, so neighbouring labels overlap several deep.
|
|
69
|
+
Hiding only the ones that collide would label some bars and not others
|
|
70
|
+
for a reason a reader cannot see.
|
|
71
|
+
- A negative bar ends below its base, so a label placed above the end would
|
|
72
|
+
sit on the bar.
|
|
73
|
+
- A plugin cannot be added to a chart after it is built, so the drawing has
|
|
74
|
+
to be handed over when the controller constructs the chart, as
|
|
75
|
+
`maintainAspectRatio` is for a height (ADR 047).
|
|
76
|
+
|
|
77
|
+
### What was considered
|
|
78
|
+
|
|
79
|
+
**Formatted tick strings from the server.** The server formats a value
|
|
80
|
+
(ADR 020), but the client chooses the ticks, where they fall depends on the
|
|
81
|
+
chart's size and data, and the server cannot know them. It would have to
|
|
82
|
+
format values it is never asked about.
|
|
83
|
+
|
|
84
|
+
**A `precision` of the measure's own for ticks.** A measure's precision is
|
|
85
|
+
what one value means. A scale spanning `$0` to `$500,000` in steps of
|
|
86
|
+
50,000 does not want cents, and a ratio at one decimal place would print
|
|
87
|
+
`20.0%` at every tick. It is the answer to a different question.
|
|
88
|
+
|
|
89
|
+
**A host-supplied tick callback.** A setting, to do what one formatter
|
|
90
|
+
already does for every measure. ADR 001 prefers one obvious way, and ADR 021
|
|
91
|
+
and ADR 023 only allow a setting where a judgement is made once at boot.
|
|
92
|
+
|
|
93
|
+
**`chartjs-plugin-datalabels` for the bar labels.** A dependency, and a
|
|
94
|
+
second way to format a number on a chart. ADR 004 keeps Chart.js the one
|
|
95
|
+
thing the engine ships, and an inline plugin that draws a string the server
|
|
96
|
+
already produced is about twenty lines.
|
|
97
|
+
|
|
98
|
+
**Labels on by default.** Changes every bar chart that exists, and a chart
|
|
99
|
+
of thirty bars gains nothing from it. Unset must change nothing, as it does
|
|
100
|
+
for height and prominence.
|
|
101
|
+
|
|
102
|
+
**A setting for where the label sits, or whether it fits.** Outside the end
|
|
103
|
+
is the only place that works for every bar, including the shortest and the
|
|
104
|
+
negative, and a fit that is a judgement per chart is a decision a reader of
|
|
105
|
+
the dashboard cannot see made.
|
|
106
|
+
|
|
107
|
+
**Labels on a line, or on a ring.** A line has a point per bucket, often
|
|
108
|
+
sixty or more, and a ring already prints every value beside it (ADR 046).
|
|
109
|
+
Neither gains from it.
|
|
110
|
+
|
|
111
|
+
## Decision
|
|
112
|
+
|
|
113
|
+
**A chart's axis is formatted by the measure, always, and a bar chart can
|
|
114
|
+
be asked to draw each bar's value as a label.** The first supersedes the one
|
|
115
|
+
sentence in ADR 020 above and nothing else in it. The second is opt in.
|
|
116
|
+
|
|
117
|
+
### The axis
|
|
118
|
+
|
|
119
|
+
The server sends what a tick needs as one more data attribute on the canvas:
|
|
120
|
+
the measure's `prefix`, its `suffix`, and whether it is a ratio.
|
|
121
|
+
|
|
122
|
+
```erb
|
|
123
|
+
data-janela--chart-tick-format-value='{"prefix":"$","suffix":"","ratio":false}'
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The controller formats each tick with Chart.js's own numeric formatter and
|
|
127
|
+
adds those. A ratio's ticks are scaled by 100, rounded to remove float
|
|
128
|
+
noise, and given a `%`. The measure's `precision` is not used: the tick's
|
|
129
|
+
decimals come from the spacing between ticks, which is what answers "25%,
|
|
130
|
+
not 25.0%". It applies to a bar and a line, both of which have a value axis,
|
|
131
|
+
and it is not a setting: it is what the axis always was, now correct.
|
|
132
|
+
|
|
133
|
+
A measure with no prefix, suffix or ratio draws what it draws today.
|
|
134
|
+
|
|
135
|
+
### The labels
|
|
136
|
+
|
|
137
|
+
**A pane may carry `value_labels`, a boolean. Unset is exactly what it is
|
|
138
|
+
today.** When set on a bar chart, each bar's formatted string, the same one
|
|
139
|
+
the tooltip shows, is drawn beyond the end of the bar, above for a positive
|
|
140
|
+
value and below for a negative one. The value axis is given 10% of grace so
|
|
141
|
+
the tallest bar keeps its label in the plot.
|
|
142
|
+
|
|
143
|
+
**All or none.** The labels are drawn only if every one of them fits within
|
|
144
|
+
its category's slot, the chart area's width divided by the number of bars.
|
|
145
|
+
If any does not, none is drawn and the chart is what it was without the flag.
|
|
146
|
+
A chart never labels some bars and not others, and a host that wants the
|
|
147
|
+
labels on a dense chart makes the chart wider or the pane a bar chart of
|
|
148
|
+
fewer categories (`limit:`, ADR 007).
|
|
149
|
+
|
|
150
|
+
**One vocabulary in three places,** as height and prominence do:
|
|
151
|
+
|
|
152
|
+
```erb
|
|
153
|
+
<%= janela_pane Order, :expedited_rate, by: :status, as: :bar, value_labels: true %>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- `Janela::Pane#value_labels`, a nullable boolean, offered on the pane form
|
|
157
|
+
as a checkbox.
|
|
158
|
+
- `janela_pane` and `janela_snapshot_pane` take `value_labels:`.
|
|
159
|
+
- The pane URL carries it as `?value_labels=1`, because the server draws the
|
|
160
|
+
pane again from that URL on every cross-filter.
|
|
161
|
+
|
|
162
|
+
**The name avoids `labels`.** In this codebase a chart's labels are its
|
|
163
|
+
categories: the controller's `labels` value is the x axis. `value_labels`
|
|
164
|
+
says which labels, and `values` would collide the same way with the
|
|
165
|
+
controller's `values`. It is the one name that reads the same at the helper,
|
|
166
|
+
the column, the URL and the controller (ADR 037).
|
|
167
|
+
|
|
168
|
+
**It is not part of the pane's identity.** ADR 029 identifies a frame by who
|
|
169
|
+
it is, and whether its bars carry numbers does not say which query it is. It
|
|
170
|
+
stays out of `Query.turbo_frame_id`.
|
|
171
|
+
|
|
172
|
+
**On anything that is not a bar chart it is ignored, not an error.** A
|
|
173
|
+
table, a single value, a line and a ring keep what they have, and a pane
|
|
174
|
+
switched between renderers keeps the flag, as height does on a ring
|
|
175
|
+
(ADR 047).
|
|
176
|
+
|
|
177
|
+
## Consequences
|
|
178
|
+
|
|
179
|
+
- **Every existing chart's axis can change.** A revenue chart gains a `$`
|
|
180
|
+
on its ticks and a ratio chart reads in percentages. Nothing else about it
|
|
181
|
+
moves, and a measure with no prefix, suffix or ratio is untouched. It is
|
|
182
|
+
in `CHANGELOG.md` under Changed as a visible correction, with no action for
|
|
183
|
+
a host; it is not an `UPGRADING.md` step (ADR 015), because nothing a host
|
|
184
|
+
wrote has to change. A host that overrode the tick callback on its own
|
|
185
|
+
chart is unaffected, since Janela does not set one for it.
|
|
186
|
+
- A ratio chart is readable end to end: a percentage on the axis, in the
|
|
187
|
+
tooltip, and on the bars if asked.
|
|
188
|
+
- **A migration for stored frames.** `janela_panes` gains a nullable
|
|
189
|
+
boolean `value_labels`, additive, so every existing pane is unset and
|
|
190
|
+
unchanged. A host using stored frames runs `bin/rails
|
|
191
|
+
janela:install:migrations` and `db:migrate`, as it did for `height`,
|
|
192
|
+
`prominence` and `companions`. The doctor's `unmigrated-columns` check
|
|
193
|
+
(ADR 035) is taught the new column, so a host that upgrades and skips the
|
|
194
|
+
migration is told rather than meeting an error on the pane form. A host that
|
|
195
|
+
does not use stored frames has nothing to do.
|
|
196
|
+
- **`Janela::Tools` has to learn the attribute.** Its `PANE_ATTRIBUTES` is the
|
|
197
|
+
list of a pane's own attributes an agent may set (ADR 053), and an
|
|
198
|
+
attribute not on it is refused. `value_labels` joins it in the same change as
|
|
199
|
+
the column, or an agent could arrange every other part of a pane and not
|
|
200
|
+
this one.
|
|
201
|
+
- **The pane URL grammar grows by one optional parameter.** ADR 037 counts
|
|
202
|
+
that as public surface, which is why this belongs before 1.0.
|
|
203
|
+
- **Ticks use the browser's number locale, and the tooltip uses the host's
|
|
204
|
+
delimiter** (ADR 020). A host whose `number.format.delimiter` differs from
|
|
205
|
+
its readers' browsers would see two styles on one chart. This is from how
|
|
206
|
+
Chart.js formats and is not something measured here; if it turns up in
|
|
207
|
+
practice the answer is to pass the delimiter in the same attribute, not to
|
|
208
|
+
stop using Chart.js's formatter.
|
|
209
|
+
- The all or none rule means the flag can do nothing on a dense chart, and
|
|
210
|
+
the pane form cannot say so. That is a real cost, accepted because the
|
|
211
|
+
alternative is a chart that labels bars selectively.
|
|
212
|
+
- Drawn pixels are not available to a screen reader. That is as true of the
|
|
213
|
+
bars themselves, which ADR 042 answers with the figure's caption and the
|
|
214
|
+
table beside it, and this decision does not change it.
|
|
215
|
+
- Tests read what the code produced: the drawn tick labels, and the position
|
|
216
|
+
and text of each drawn bar label, not the attribute that asked for them
|
|
217
|
+
(ADR 035).
|
|
218
|
+
- What would change this decision: a need for a label on a line or a ring,
|
|
219
|
+
which is an argument about those renderers and not about this one; or a
|
|
220
|
+
host that needs the labels to survive on a dense chart, which would be a
|
|
221
|
+
case for a step in how they are thinned and not for a placement setting.
|
data/docs/decisions/INDEX.md
CHANGED
|
@@ -23,26 +23,26 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
|
|
|
23
23
|
| **Vision, scope, forkability** | 001, 010, 012, 037, 052 |
|
|
24
24
|
| **Open-source & host-decoupling** | 001, 022, 036, 041, 052 |
|
|
25
25
|
| **DSL & query layer** | 002, 006, 007, 020, 025, 038, 044, 049, 051 |
|
|
26
|
-
| **Dependencies** | 002, 003, 004, 006, 017, 025 |
|
|
27
|
-
| **Authorisation** | 002, 003, 004, 009, 017, 019, 022, 032, 033, 034, 035, 039, 040, 048 |
|
|
26
|
+
| **Dependencies** | 002, 003, 004, 006, 017, 025, 053 |
|
|
27
|
+
| **Authorisation** | 002, 003, 004, 009, 017, 019, 022, 032, 033, 034, 035, 039, 040, 048, 053 |
|
|
28
28
|
| **Performance & storage** | 007, 017, 025, 048, 051 |
|
|
29
|
-
| **Ordering & formatting** | 007, 020, 038 |
|
|
29
|
+
| **Ordering & formatting** | 007, 020, 038, 054 |
|
|
30
30
|
| **Cross-filtering & Hotwire** | 003, 004, 005, 008, 024, 025, 040, 043, 044, 045, 048, 049 |
|
|
31
31
|
| **Layouts & views** | 011, 012, 016, 018, 020, 027, 039, 047, 050 |
|
|
32
32
|
| **CSS & styling** | 016, 018, 023, 026, 027, 036, 042, 046, 047, 050 |
|
|
33
|
-
| **Frames, panes & persistence** | 012, 013, 014, 019, 029, 030, 033, 039, 040, 041, 043, 044, 047, 048, 050, 051 |
|
|
33
|
+
| **Frames, panes & persistence** | 012, 013, 014, 019, 029, 030, 033, 039, 040, 041, 043, 044, 047, 048, 050, 051, 053, 054 |
|
|
34
34
|
| **Naming rule** | 014, 023, 036 |
|
|
35
|
-
| **JavaScript delivery & charts** | 004, 006, 026, 042, 046, 047 |
|
|
35
|
+
| **JavaScript delivery & charts** | 004, 006, 026, 042, 046, 047, 054 |
|
|
36
36
|
| **Time dimensions** | 006, 025, 045 |
|
|
37
|
-
| **Routes, URLs & naming** | 005, 007, 008, 009, 011, 013, 022, 024, 025, 040, 041, 045, 047, 050, 051 |
|
|
37
|
+
| **Routes, URLs & naming** | 005, 007, 008, 009, 011, 013, 022, 024, 025, 040, 041, 045, 047, 050, 051, 054 |
|
|
38
38
|
| **Snapshots & publishing** | 009, 020, 028, 033, 034, 051 |
|
|
39
|
-
| **AI agents & guidance** | 010, 015, 021 |
|
|
39
|
+
| **AI agents & guidance** | 010, 015, 021, 053 |
|
|
40
40
|
| **The doctor & checks** | 021, 025, 032, 033, 035 |
|
|
41
41
|
| **Releases & upgrades** | 015, 021, 032, 034, 035, 036, 037, 042, 052 |
|
|
42
42
|
| **Accessibility & keyboard** | 024, 042, 045, 046, 049 |
|
|
43
43
|
| **Security** | 003, 025, 028, 031, 032, 034, 035, 044, 052 |
|
|
44
44
|
| **Testing** | 003 |
|
|
45
|
-
| **Roadmap & planning** | 001, 037, 045, 046, 052 |
|
|
45
|
+
| **Roadmap & planning** | 001, 037, 045, 046, 052, 053 |
|
|
46
46
|
|
|
47
47
|
## Chronological
|
|
48
48
|
|
|
@@ -100,7 +100,9 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
|
|
|
100
100
|
| 050 | A Single Value's Prominence Is One of Three Steps, and Unset Changes Nothing | 2026-09-30 | Accepted |
|
|
101
101
|
| 051 | A Table Can Carry Companion Columns, and a Fact Is Shown Only When It Is Shared | 2026-09-30 | Accepted |
|
|
102
102
|
| 052 | Two People Cut Releases, Reports Go Private, and Support Is Best Effort | 2026-09-30 | Accepted |
|
|
103
|
+
| 053 | An Agent Reaches Janela Through Tools the Host Scopes, and Janela Registers None | 2026-10-02 | Accepted |
|
|
104
|
+
| 054 | A Chart Reads Numbers the Way Its Measure Formats Them | 2026-10-06 | Accepted |
|
|
103
105
|
|
|
104
106
|
## Next number
|
|
105
107
|
|
|
106
|
-
Next ADR:
|
|
108
|
+
Next ADR: 055
|
data/docs/multi-tenancy.md
CHANGED
|
@@ -36,9 +36,10 @@ without asking first:
|
|
|
36
36
|
- A snapshot, read through `policy_scope(Janela::Snapshot)`.
|
|
37
37
|
|
|
38
38
|
A pane rendered inline in your own page runs in your request, so the
|
|
39
|
-
same scope applies there as in the engine's controllers
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
same scope applies there as in the engine's controllers, so a live pane
|
|
40
|
+
is never calculated where the current tenant would have gone missing. A
|
|
41
|
+
scheduled snapshot is the one thing that runs in the background, and it
|
|
42
|
+
is told whose it is (see Snapshots below).
|
|
42
43
|
|
|
43
44
|
## With Pundit
|
|
44
45
|
|
|
@@ -69,6 +70,23 @@ class ApplicationController < ActionController::Base
|
|
|
69
70
|
end
|
|
70
71
|
```
|
|
71
72
|
|
|
73
|
+
A snapshot is read through its own scope as well. Without a policy for it
|
|
74
|
+
Pundit raises, and a hand rolled `policy_scope` that does not name it
|
|
75
|
+
would hand every tenant every snapshot, so write the second one too:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# app/policies/janela/snapshot_policy.rb
|
|
79
|
+
module Janela
|
|
80
|
+
class SnapshotPolicy < ApplicationPolicy
|
|
81
|
+
class Scope < ApplicationPolicy::Scope
|
|
82
|
+
def resolve
|
|
83
|
+
scope.where(owner: Current.account)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
72
90
|
Your own models keep the policies they already have. Janela calls
|
|
73
91
|
`policy_scope(Order)` and gets whatever `OrderPolicy::Scope` returns.
|
|
74
92
|
|
|
@@ -85,7 +103,7 @@ class ApplicationController < ActionController::Base
|
|
|
85
103
|
private
|
|
86
104
|
def policy_scope(model)
|
|
87
105
|
case model.name
|
|
88
|
-
when "Janela::Frame" then model.where(owner: ActsAsTenant.current_tenant)
|
|
106
|
+
when "Janela::Frame", "Janela::Snapshot" then model.where(owner: ActsAsTenant.current_tenant)
|
|
89
107
|
else model.all # acts_as_tenant has already scoped your own models
|
|
90
108
|
end
|
|
91
109
|
end
|
data/docs/roadmap.md
CHANGED
|
@@ -8,7 +8,7 @@ Topics: roadmap, releases, scope, planning
|
|
|
8
8
|
|
|
9
9
|
<svg viewBox="0 0 680 360" width="100%" role="img" aria-labelledby="vista-title vista-desc" class="vista-art" style="display: block; margin: 1.75rem 0; border-radius: 10px;">
|
|
10
10
|
<title id="vista-title">Vista</title>
|
|
11
|
-
<desc id="vista-desc">Sea and sky with a horizon across them. The near water is dark, because every issue in 1.0 has shipped, and
|
|
11
|
+
<desc id="vista-desc">Sea and sky with a horizon across them. The near water is dark, because every issue in 1.0 has shipped, and five lights sit far off at the horizon for the work still in sight past it. A low sun rises and sets on the horizon as the pointer moves up and down, and never climbs higher: the sky above is empty, because what is not coming is not in view.</desc>
|
|
12
12
|
|
|
13
13
|
<style>
|
|
14
14
|
.vista-art .v-far { transform: translate3d(calc(var(--vitral-shift-x, 0) * 13px), calc(var(--vitral-shift-y, 0) * 7px), 0); transition: transform .45s cubic-bezier(.2,.7,.3,1); }
|
|
@@ -89,6 +89,10 @@ Topics: roadmap, releases, scope, planning
|
|
|
89
89
|
<circle cx="322" cy="201" r="1.5" fill="#FFF3DC" opacity="0.55"/>
|
|
90
90
|
<circle cx="540" cy="202.5" r="8" fill="url(#vLamp)" opacity="0.2"/>
|
|
91
91
|
<circle cx="540" cy="202.5" r="1.6" fill="#FFF3DC" opacity="0.55"/>
|
|
92
|
+
<circle cx="112" cy="201.5" r="8" fill="url(#vLamp)" opacity="0.2"/>
|
|
93
|
+
<circle cx="112" cy="201.5" r="1.5" fill="#FFF3DC" opacity="0.55"/>
|
|
94
|
+
<circle cx="430" cy="202" r="8" fill="url(#vLamp)" opacity="0.2"/>
|
|
95
|
+
<circle cx="430" cy="202" r="1.6" fill="#FFF3DC" opacity="0.55"/>
|
|
92
96
|
</g>
|
|
93
97
|
|
|
94
98
|
<g class="v-glint" stroke="#FFE2A6" stroke-linecap="round">
|
|
@@ -109,7 +113,7 @@ Topics: roadmap, releases, scope, planning
|
|
|
109
113
|
Janela is alpha. It works, it is tested against a real Rails application
|
|
110
114
|
in a real browser, and its public surface has changed in three of the
|
|
111
115
|
last four releases. This page says what has to be true before that stops,
|
|
112
|
-
what is
|
|
116
|
+
what is left before 1.0, and what is deliberately not coming. The
|
|
113
117
|
reasoning behind it is ADR 037.
|
|
114
118
|
|
|
115
119
|
## What 1.0 means
|
|
@@ -128,11 +132,11 @@ change in any release, and every change of that kind carries an entry in
|
|
|
128
132
|
`UPGRADING.md`.
|
|
129
133
|
|
|
130
134
|
**A pane can be read.** Janela draws tables, bars, lines, doughnuts and
|
|
131
|
-
pies. A table row
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
not extra features. They
|
|
135
|
-
found by people installing the gem rather than reading it.
|
|
135
|
+
pies. A table row can carry context beside its label, a single number says
|
|
136
|
+
how prominent it is, a chart has a height that suits the page, its axis reads
|
|
137
|
+
in the measure's own format, and a bar can show its value. Those
|
|
138
|
+
were not extra features. They were the 5% not finished, and most of them
|
|
139
|
+
were found by people installing the gem rather than reading it.
|
|
136
140
|
|
|
137
141
|
**The doctor can be trusted.** `rails janela:doctor` checks an
|
|
138
142
|
installation for the mistakes that produce a dashboard showing numbers
|
|
@@ -144,17 +148,24 @@ working is a check nobody should rely on.
|
|
|
144
148
|
|
|
145
149
|
The near ground is clear. Every issue that was in 1.0 has shipped: chart
|
|
146
150
|
height, a ratio measure, single value prominence, companion columns, Sprockets
|
|
147
|
-
hosts, and how the project is run.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
hosts, and how the project is run. 0.13.0 added tools an agent can use
|
|
152
|
+
(ADR 053). 0.14.0 followed: a chart's axis reads in the measure's own format,
|
|
153
|
+
so a ratio is a percentage down the side as well as in the tooltip, and a bar
|
|
154
|
+
chart can show each bar's value (ADR 054). Both came from a real install, and
|
|
155
|
+
the second adds a pane option, so it starts its own short soak. What 1.0 waits
|
|
156
|
+
on now is a week of real installs of 0.14.0 ([#71](https://github.com/retail-tasker/janela/issues/71)),
|
|
157
|
+
since much of that surface was added in the last days and has only met this
|
|
158
|
+
repository. When nothing surprising comes back, the surface is frozen and 1.0
|
|
159
|
+
is tagged. The agent tools are the one new piece of surface: the read tools are
|
|
160
|
+
frozen at 1.0, and the write tools are frozen with them only if a real host has
|
|
161
|
+
used them by the end of that week. Otherwise they wait for 1.1.
|
|
151
162
|
|
|
152
163
|
Progress is tracked on the
|
|
153
164
|
[1.0 milestone](https://github.com/retail-tasker/janela/milestone/2).
|
|
154
165
|
|
|
155
166
|
## Horizonte
|
|
156
167
|
|
|
157
|
-
The
|
|
168
|
+
The lights far off at the horizon. Wanted, not blocking a stable
|
|
158
169
|
release, and being on this list is not a refusal.
|
|
159
170
|
|
|
160
171
|
- **Drilling down on time panes** ([#65](https://github.com/retail-tasker/janela/issues/65)).
|
|
@@ -162,6 +173,17 @@ release, and being on this list is not a refusal.
|
|
|
162
173
|
that pane itself to the weeks within it is a different gesture, with
|
|
163
174
|
its own questions about getting back out and about the URL, and it
|
|
164
175
|
needs a decision record before any code.
|
|
176
|
+
- **Telling a frame to refresh** ([#68](https://github.com/retail-tasker/janela/issues/68)).
|
|
177
|
+
One action a host's own timer or push can call, and no timer or stream of
|
|
178
|
+
Janela's own (ADR 048). Additive, so it can land after 1.0.
|
|
179
|
+
- **A frame toolbar** ([#77](https://github.com/retail-tasker/janela/issues/77)).
|
|
180
|
+
A small strip of standard controls a host does not have to build, starting
|
|
181
|
+
with full screen. Additive, so it can land after 1.0, and it needs a decision
|
|
182
|
+
record first.
|
|
183
|
+
- **A theme panel in the demo** ([#84](https://github.com/retail-tasker/janela/issues/84)).
|
|
184
|
+
Setting how every pane is presented, starting with colours, as a page of
|
|
185
|
+
controls over the properties a theme already publishes. The demo, not the
|
|
186
|
+
gem, and any property beyond colour needs a decision record first.
|
|
165
187
|
- **A command palette for the demo** ([#41](https://github.com/retail-tasker/janela/issues/41)).
|
|
166
188
|
The demo site, not the gem.
|
|
167
189
|
- **A scroll drift on the gallery page** ([#45](https://github.com/retail-tasker/janela/issues/45)).
|
|
@@ -178,7 +200,9 @@ run does it better.
|
|
|
178
200
|
Natural-language query. A separate data warehouse. A row-level-security
|
|
179
201
|
subsystem, because your application already has Pundit or CanCanCan and
|
|
180
202
|
Janela reads through it. A refresh-scheduling interface, because you
|
|
181
|
-
already have a scheduler and snapshots are an ActiveJob
|
|
203
|
+
already have a scheduler and snapshots are an ActiveJob (a way to tell a
|
|
204
|
+
frame to refresh, which your scheduler can call, is
|
|
205
|
+
[#68](https://github.com/retail-tasker/janela/issues/68)). An embedding
|
|
182
206
|
SDK. A mobile application. Print and paginated reports. A drag-and-drop
|
|
183
207
|
visual dashboard designer, though frames and panes are database records,
|
|
184
208
|
so an application can build its own editor on top of them.
|
data/docs/theming.md
CHANGED
|
@@ -82,6 +82,7 @@ the names a theme spends most of its time on.
|
|
|
82
82
|
| `janela-legend` | `<table>` | its legend, one row of swatch, button and value per slice |
|
|
83
83
|
| `janela-swatch` | `<span>` | the colour beside a legend label |
|
|
84
84
|
| `janela-empty` | `<p>` | a pane whose query returned nothing |
|
|
85
|
+
| `janela-muted` | `<p>` | the note under a ring that could not be drawn and became a table (ADR 046) |
|
|
85
86
|
| `janela-error` | `<p>` | a pane that could not be read |
|
|
86
87
|
| `janela-content` | `<div>` | a stored pane holding words or a host partial rather than a query (ADR 039) |
|
|
87
88
|
| `janela-content-heading` | `<h2>` | a text pane's heading |
|
|
@@ -121,8 +122,9 @@ here rather than being left for each host to write.
|
|
|
121
122
|
`janela.css` also styles Janela's own pages, the frame index and the
|
|
122
123
|
editing forms: `janela-page`, `janela-card`, `janela-button`,
|
|
123
124
|
`janela-form`, `janela-field`, `janela-list`, `janela-crumb`,
|
|
124
|
-
`janela-flash` and the rest. They are
|
|
125
|
-
|
|
125
|
+
`janela-flash` and the rest. They are plain class names that the engine's
|
|
126
|
+
own pages use, not scoped to `janela-page`, so they affect your pages
|
|
127
|
+
only if your own markup uses the same names. Avoid them.
|
|
126
128
|
|
|
127
129
|
**Those names may change in any release.** They are the engine's own
|
|
128
130
|
chrome rather than an interface. Restyle them if you want Janela's
|
|
@@ -140,8 +142,8 @@ Janela.theme = "midnight"
|
|
|
140
142
|
|
|
141
143
|
The name is resolved against your own asset paths, so `midnight.css` in
|
|
142
144
|
your application works exactly as the gem's own `vitral` does. A name
|
|
143
|
-
that
|
|
144
|
-
unthemed
|
|
145
|
+
that your asset pipeline cannot find raises when the page renders,
|
|
146
|
+
rather than quietly rendering an unthemed one.
|
|
145
147
|
|
|
146
148
|
That setting links the theme into **Janela's own pages**. Your pages load
|
|
147
149
|
whatever your layout says, so if you want the same look around a pane you
|
data/lib/janela/doctor.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Janela
|
|
|
9
9
|
|
|
10
10
|
# Run in this order, and each one names the finding it produces: a host
|
|
11
11
|
# silences a check by that name (ADR 021).
|
|
12
|
-
CHECKS = %i[stale_identifiers unmounted_engine unmigrated_tables unregistered_controllers
|
|
12
|
+
CHECKS = %i[stale_identifiers unmounted_engine unmigrated_tables unmigrated_columns unregistered_controllers
|
|
13
13
|
through_dimensions_without_an_allowlist unscoped_reads frames_nobody_will_own
|
|
14
14
|
snapshots_nobody_will_see unauthenticated_endpoints
|
|
15
15
|
hardcoded_disallowed_predicates].freeze
|
|
@@ -21,10 +21,22 @@ module Janela
|
|
|
21
21
|
"janela/dashboard_controller" => "janela/frame_controller",
|
|
22
22
|
"janela/dashboard_controller.js" => "janela/frame_controller.js",
|
|
23
23
|
"Janela::DashboardHelper" => "Janela::FramesHelper",
|
|
24
|
-
"Janela::PanesController" => "Janela::QueriesController",
|
|
25
24
|
"Janela::SnapshotPanesController" => "Janela::SnapshotQueriesController"
|
|
26
25
|
}.freeze
|
|
27
26
|
|
|
27
|
+
# A name that was renamed away and later given back to something else, so
|
|
28
|
+
# finding it cannot say which one the host means. An error would send a
|
|
29
|
+
# host patching the stored-pane form to a controller without pane_params,
|
|
30
|
+
# and the extra param would silently stop saving (#74). A warning says what
|
|
31
|
+
# the name means now and leaves the judgement to the host.
|
|
32
|
+
REUSED = {
|
|
33
|
+
"Janela::PanesController" =>
|
|
34
|
+
"This name was the controller that renders a pane's query until 0.7, when it became " \
|
|
35
|
+
"Janela::QueriesController. Since 0.12 it is the stored-pane form's controller again, " \
|
|
36
|
+
"and it owns pane_params. A patch to the form belongs here; a patch to how a query " \
|
|
37
|
+
"renders belongs on Janela::QueriesController."
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
28
40
|
SEARCHED = %w[app config lib].freeze
|
|
29
41
|
READABLE = %w[.rb .erb .js .erb.html .html.erb .haml .slim .yml].freeze
|
|
30
42
|
|
|
@@ -79,14 +91,26 @@ module Janela
|
|
|
79
91
|
end
|
|
80
92
|
|
|
81
93
|
def stale_identifiers
|
|
82
|
-
RENAMED.filter_map do |old, new|
|
|
83
|
-
files =
|
|
84
|
-
next if files.empty?
|
|
94
|
+
renamed = RENAMED.filter_map do |old, new|
|
|
95
|
+
next unless (files = files_naming(old)).any?
|
|
85
96
|
|
|
86
|
-
Finding.new(severity: :error,
|
|
87
|
-
summary: "#{old} is now #{new}",
|
|
88
|
-
detail: files.map { |file| " #{file.relative_path_from(@root)}" }.join("\n"))
|
|
97
|
+
Finding.new(severity: :error, summary: "#{old} is now #{new}", detail: listed(files))
|
|
89
98
|
end
|
|
99
|
+
reused = REUSED.filter_map do |name, meaning|
|
|
100
|
+
next unless (files = files_naming(name)).any?
|
|
101
|
+
|
|
102
|
+
Finding.new(severity: :warning, summary: "#{name} has meant two things",
|
|
103
|
+
detail: " #{meaning}\n#{listed(files)}")
|
|
104
|
+
end
|
|
105
|
+
renamed + reused
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def files_naming(identifier)
|
|
109
|
+
source_files.select { |file| file.read.include?(identifier) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def listed(files)
|
|
113
|
+
files.map { |file| " #{file.relative_path_from(@root)}" }.join("\n")
|
|
90
114
|
end
|
|
91
115
|
|
|
92
116
|
def unmounted_engine
|
|
@@ -116,6 +140,40 @@ module Janela
|
|
|
116
140
|
nil # no database to ask yet
|
|
117
141
|
end
|
|
118
142
|
|
|
143
|
+
# The columns a release added to a table that already existed. A host that
|
|
144
|
+
# upgraded and did not run the migration has the table, so the check above
|
|
145
|
+
# passes, and meets an error the first time a pane form or a stored pane
|
|
146
|
+
# reads the column. Only the tables that exist are asked: a missing table
|
|
147
|
+
# is that check's finding, and listing every column of it here would say
|
|
148
|
+
# the same thing twice.
|
|
149
|
+
ADDED_COLUMNS = {
|
|
150
|
+
"Janela::Frame" => %w[key default_model default_where],
|
|
151
|
+
"Janela::Pane" => %w[kind heading body link partial height prominence companions value_labels],
|
|
152
|
+
"Janela::Snapshot" => %w[owner_type owner_id]
|
|
153
|
+
}.freeze
|
|
154
|
+
|
|
155
|
+
def unmigrated_columns
|
|
156
|
+
return unless engine_mounted?
|
|
157
|
+
|
|
158
|
+
absent = ADDED_COLUMNS.filter_map do |model_name, columns|
|
|
159
|
+
model = model_name.constantize
|
|
160
|
+
next unless model.table_exists?
|
|
161
|
+
|
|
162
|
+
missing = columns - model.column_names
|
|
163
|
+
"#{model.table_name} (#{missing.join(', ')})" if missing.any?
|
|
164
|
+
end
|
|
165
|
+
return if absent.empty?
|
|
166
|
+
|
|
167
|
+
Finding.new(severity: :error,
|
|
168
|
+
summary: "#{absent.to_sentence} #{absent.one? ? 'is' : 'are'} missing columns a release added",
|
|
169
|
+
detail: " The tables exist, so the migrations from an earlier release ran, but a later one has not.\n" \
|
|
170
|
+
" Run:\n" \
|
|
171
|
+
" bin/rails janela:install:migrations\n" \
|
|
172
|
+
" bin/rails db:migrate")
|
|
173
|
+
rescue StandardError
|
|
174
|
+
nil # no database to ask yet
|
|
175
|
+
end
|
|
176
|
+
|
|
119
177
|
def engine_mounted?
|
|
120
178
|
Rails.application.routes.routes.any? { |route| route.app.respond_to?(:app) && route.app.app == Janela::Engine }
|
|
121
179
|
end
|