janela 0.2.0 → 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 +54 -1
- data/README.md +203 -21
- data/UPGRADING.md +136 -0
- data/app/assets/javascripts/janela/chart_controller.js +16 -8
- 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 +45 -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 +107 -85
- data/app/models/janela/query.rb +149 -0
- data/app/models/janela/snapshot.rb +6 -6
- 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 -44
- 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 +26 -0
- 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/005-pane-urls-and-mount-path.md +4 -1
- data/docs/decisions/009-snapshots.md +5 -2
- data/docs/decisions/010-agent-guidance-ships-the-agent-waits.md +134 -0
- data/docs/decisions/011-panes-do-not-render-in-the-host-layout.md +81 -0
- 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 +24 -5
- data/docs/multi-tenancy.md +175 -0
- data/lib/janela/definition.rb +23 -9
- data/lib/janela/dimension.rb +5 -1
- data/lib/janela/doctor.rb +219 -0
- data/lib/janela/engine.rb +15 -2
- data/lib/janela/measure.rb +68 -4
- data/lib/janela/version.rb +1 -1
- data/lib/janela.rb +24 -1
- data/lib/tasks/janela.rake +6 -0
- metadata +57 -4
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-09-15
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Related: ADR 001, ADR 002, ADR 005, ADR 009
|
|
5
|
+
Supersedes: part of ADR 001
|
|
6
|
+
Triggers:
|
|
7
|
+
- changing what a frame or a pane stores, or how either is rendered
|
|
8
|
+
- adding an editing surface for dashboards
|
|
9
|
+
- anything that would let a persisted row widen what can be queried
|
|
10
|
+
- deciding whether a dashboard belongs in git
|
|
11
|
+
- adding layout vocabulary, or a layer between a frame and its panes
|
|
12
|
+
- naming anything "frame"
|
|
13
|
+
Topics: frames, panes, persistence, layout, css-grid, naming, authorisation, scope
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# ADR 012: Frames and Panes Are Data
|
|
17
|
+
|
|
18
|
+
## Context
|
|
19
|
+
|
|
20
|
+
ADR 001 put a drag and drop report designer out of scope and made
|
|
21
|
+
dashboards code. A dashboard was an ERB page in the host application
|
|
22
|
+
calling `janela_pane` helpers, which kept it in git, reviewable and
|
|
23
|
+
identical across environments. Everything since has been built on that
|
|
24
|
+
assumption.
|
|
25
|
+
|
|
26
|
+
Dogfooding found the assumption wrong, and the reason is ownership
|
|
27
|
+
rather than convenience. **The author of a dashboard is the analyst.**
|
|
28
|
+
In the commercial application this gem was extracted from, the people
|
|
29
|
+
who decide which panes a dashboard has, in what order and at what
|
|
30
|
+
size, are the people reading it, and they cannot deploy. A composition
|
|
31
|
+
that lives in ERB is a file held by the wrong owner. One install
|
|
32
|
+
measured a fourteen minute round trip, edit, test, lint, CI, deploy,
|
|
33
|
+
poll, to add a single pane; the cost is real, but the ownership is the
|
|
34
|
+
argument.
|
|
35
|
+
|
|
36
|
+
The gem was already half of the way there, and the asymmetry was
|
|
37
|
+
visible once pointed at. A pane is a real URL accepting `as`,
|
|
38
|
+
`granularity`, `limit` and Ransack `q[...]`, so what a pane *shows*
|
|
39
|
+
changes live for anyone with the link (ADR 005, ADR 007, ADR 008).
|
|
40
|
+
Only *composition* was frozen in code.
|
|
41
|
+
|
|
42
|
+
## Decision
|
|
43
|
+
|
|
44
|
+
**A frame is a dashboard, and it is data.** Two records, created at
|
|
45
|
+
runtime, with no requirement to be declared or seeded in code first:
|
|
46
|
+
|
|
47
|
+
- `Janela::Frame`, one dashboard. A name, a slug, and its grid: how
|
|
48
|
+
many columns and what gap.
|
|
49
|
+
- `Janela::Pane`, one visual in one frame. Its position, how many
|
|
50
|
+
columns it spans, and what it shows: model, measure, dimension,
|
|
51
|
+
renderer, granularity, limit, and an optional title of its own.
|
|
52
|
+
|
|
53
|
+
**The data definition drives the HTML.** Janela renders a frame from
|
|
54
|
+
its rows and the host composes nothing:
|
|
55
|
+
|
|
56
|
+
```erb
|
|
57
|
+
<%= janela_frame @frame %>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Grid, order, spans and each pane's Turbo Frame all derive from the
|
|
61
|
+
data. This is the point of the change: the library is data driven and
|
|
62
|
+
the markup fits the definition, rather than the definition being
|
|
63
|
+
markup.
|
|
64
|
+
|
|
65
|
+
**Layout uses CSS Grid's vocabulary, not a new one.** `columns` and
|
|
66
|
+
`gap` on the frame, `span` on a pane. Every Rails developer knows
|
|
67
|
+
these words, they map directly onto what renders with no translation
|
|
68
|
+
layer, and ADR 001 argues against inventing a glossary when a shared
|
|
69
|
+
one exists.
|
|
70
|
+
|
|
71
|
+
**The names come from window anatomy, one level each.** Janela is
|
|
72
|
+
Portuguese for window, so the gem name already carries the metaphor
|
|
73
|
+
and a class called `Window` would stutter. In a window the frame is
|
|
74
|
+
the fixed outer structure, the sash is the movable assembly inside it,
|
|
75
|
+
and the panes are the glass. The frame holds the panes, so `Frame` is
|
|
76
|
+
the container and `Pane` is the visual. `Sash`, a band of panes within
|
|
77
|
+
a frame, is deliberately **not** built: `columns` on the frame plus
|
|
78
|
+
`span` on a pane already express a row of single values, a full width
|
|
79
|
+
time series and a row of breakdowns, which is the dashboard shape this
|
|
80
|
+
project recommends. A sash would be a layer the layout does not need,
|
|
81
|
+
and it stays available if nested bands ever earn one.
|
|
82
|
+
|
|
83
|
+
**"Frame" now means two things, and the rule is written down rather
|
|
84
|
+
than discovered.** `Janela::Frame` is a dashboard. A `<turbo-frame>`
|
|
85
|
+
is the mechanism each pane is rendered inside. They sit one level
|
|
86
|
+
apart, so the frame holds frames, and the ambiguity is real: a
|
|
87
|
+
`Pane belongs_to :frame` gives `pane.frame_id` as a foreign key, which
|
|
88
|
+
collided with the existing `frame_id` meaning the DOM id of a pane's
|
|
89
|
+
turbo frame. That method is renamed `turbo_frame_id`, which is clearer
|
|
90
|
+
regardless. The rule: **frame unqualified means the dashboard; the DOM
|
|
91
|
+
element is always spelled turbo frame.**
|
|
92
|
+
|
|
93
|
+
**The code keeps the vocabulary; the data arranges it.** Measures and
|
|
94
|
+
dimensions stay declared in the `janela` block on the model, in git. A
|
|
95
|
+
`Pane` row may only name a measure and a dimension that block
|
|
96
|
+
declares, because `Janela.definition!` validates against the registry
|
|
97
|
+
and dimensions are themselves the Ransack allowlist (ADR 002, ADR
|
|
98
|
+
003). A row therefore cannot invent a query, reach a model nobody
|
|
99
|
+
exposed, or widen what is filterable. This property is worth stating
|
|
100
|
+
plainly, because it is precisely what is usually wrong with database
|
|
101
|
+
backed dashboards, and here it falls out of what is already built.
|
|
102
|
+
|
|
103
|
+
The division of labour is then: **developers define what can be asked;
|
|
104
|
+
analysts arrange what is shown.**
|
|
105
|
+
|
|
106
|
+
**The runtime object is `Janela::Query`.** Today's `Janela::Pane`
|
|
107
|
+
conflates the thing displayed with the query it runs. With `Pane`
|
|
108
|
+
taken by the persisted record, the runtime object, a definition plus a
|
|
109
|
+
measure, a dimension, filters and a renderer that resolves to a
|
|
110
|
+
result, becomes `Janela::Query`. A `Pane` row builds one to do its
|
|
111
|
+
work.
|
|
112
|
+
|
|
113
|
+
**Three editing surfaces, in this order.** All three are wanted; they
|
|
114
|
+
are not alternatives.
|
|
115
|
+
|
|
116
|
+
1. **Rows.** The models are the foundation, so a host, a script or an
|
|
117
|
+
agent composes a frame with ordinary ActiveRecord. This is the
|
|
118
|
+
whole of the first increment.
|
|
119
|
+
2. **Forms in the engine.** Plain Rails CRUD, no canvas: add a pane by
|
|
120
|
+
choosing from the declared vocabulary, reorder, set a span, set a
|
|
121
|
+
granularity. This is the analyst's minimum and the point at which
|
|
122
|
+
the change delivers what it is for.
|
|
123
|
+
3. **A visual editor.** Accepted as the direction and deferred to its
|
|
124
|
+
own ADR, because it is mostly a JavaScript design problem and
|
|
125
|
+
should not be settled in the same breath as a schema.
|
|
126
|
+
|
|
127
|
+
**Authorisation is the host's, as always.** Reading and editing go
|
|
128
|
+
through Pundit policies on `Janela::Frame` and `Janela::Pane` in the
|
|
129
|
+
host application, the same hook every other part of the gem uses.
|
|
130
|
+
Janela ships no roles.
|
|
131
|
+
|
|
132
|
+
## What this supersedes in ADR 001
|
|
133
|
+
|
|
134
|
+
ADR 001 listed a drag and drop report designer under what Janela would
|
|
135
|
+
deliberately not build, reasoning that dashboards defined in code stay
|
|
136
|
+
small, reviewable and forkable. That reasoning holds for the
|
|
137
|
+
*vocabulary*, which is why measures and dimensions stay in code. It
|
|
138
|
+
does not hold for *composition*, because it assumed the developer was
|
|
139
|
+
the author. Everything else in ADR 001 stands: ship the load bearing
|
|
140
|
+
core, prefer one obvious way, stay decoupled from any host, keep the
|
|
141
|
+
codebase small enough to fork.
|
|
142
|
+
|
|
143
|
+
## Consequences
|
|
144
|
+
|
|
145
|
+
- A dashboard is no longer in git. It cannot be code reviewed,
|
|
146
|
+
diffed, or guaranteed identical between environments, and a
|
|
147
|
+
production frame will drift from anything staging has. This is the
|
|
148
|
+
price of the decision and it is paid knowingly: the owner of a
|
|
149
|
+
dashboard is not a person who works in git.
|
|
150
|
+
- Export and import of a frame will be wanted, to move one between
|
|
151
|
+
environments and to put one under review when it matters. Not built
|
|
152
|
+
now; noted so it is not a surprise.
|
|
153
|
+
- Rendering a frame is a new surface: a helper, a grid, and a
|
|
154
|
+
stylesheet, because a grid with no CSS means nothing. The missing
|
|
155
|
+
default stylesheet (#15) and inter pane layout (#29) become part of
|
|
156
|
+
this work rather than neighbours to it.
|
|
157
|
+
- Snapshots (ADR 009) name their panes explicitly today. A frame gives
|
|
158
|
+
a snapshot an obvious subject: freeze a frame, not a list. A later
|
|
159
|
+
ADR, but the shape is now clear.
|
|
160
|
+
- Renaming the runtime object touches the helper, both controllers,
|
|
161
|
+
the snapshot model, a view and the tests. Mechanical, pre 1.0, and
|
|
162
|
+
invisible to hosts: `janela_pane` keeps its name, so neither
|
|
163
|
+
existing installation changes.
|
|
164
|
+
- Every dashboard built so far, including the demo and both hosts, is
|
|
165
|
+
ERB calling `janela_pane`. That helper keeps working. A frame is an
|
|
166
|
+
addition, not a replacement, and nothing has to move.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-09-15
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Related: ADR 005, ADR 011, ADR 012
|
|
5
|
+
Triggers:
|
|
6
|
+
- changing how a frame is addressed, or what a frame is called in a host's interface
|
|
7
|
+
- adding a slug, a friendly URL or a lookup other than the primary key
|
|
8
|
+
- adding or replacing the index of frames
|
|
9
|
+
- a frame slug or path segment colliding with the pane grammar
|
|
10
|
+
Topics: routes, urls, naming, i18n, frames, defaults
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# ADR 013: Naming and Addressing Frames
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
|
|
17
|
+
ADR 012 made a frame a database record. Three questions follow from
|
|
18
|
+
that and none were settled there: what a host calls a frame in its own
|
|
19
|
+
interface, how a frame is addressed, and how a person finds one.
|
|
20
|
+
|
|
21
|
+
The first is the same question ADR 005 answered for the mount path.
|
|
22
|
+
"Frame" is the code's word, taken from window anatomy; it is not a
|
|
23
|
+
word an analyst would use. One host calls these insights, another
|
|
24
|
+
reports, another dashboards, and the difference is business language
|
|
25
|
+
rather than preference.
|
|
26
|
+
|
|
27
|
+
The second has a trap. ADR 005 published the pane grammar as
|
|
28
|
+
`/<mount>/:model/:measure(/:dimension)`. A frame addressed directly
|
|
29
|
+
under the mount, `/insights/vamos-review`, cannot be distinguished
|
|
30
|
+
from the start of a pane URL: Rails cannot tell a frame slug from a
|
|
31
|
+
model route key, and the frame route would shadow every pane.
|
|
32
|
+
|
|
33
|
+
## Decision
|
|
34
|
+
|
|
35
|
+
**What a frame is called comes from i18n, not from configuration.**
|
|
36
|
+
The class stays `Janela::Frame`, and every label the engine renders
|
|
37
|
+
uses `Frame.model_name.human`, so a host renames it in its own locale
|
|
38
|
+
file:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
en:
|
|
42
|
+
activerecord:
|
|
43
|
+
models:
|
|
44
|
+
janela/frame:
|
|
45
|
+
one: "Dashboard"
|
|
46
|
+
other: "Dashboards"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This adds no setting, uses the mechanism Rails already has, and
|
|
50
|
+
composes with real translation rather than only relabelling. Janela
|
|
51
|
+
ships English defaults.
|
|
52
|
+
|
|
53
|
+
**Frames live under their own path segment, and that segment is the
|
|
54
|
+
host's.** A single configurable value, defaulting to `dashboards`
|
|
55
|
+
because that is the word most hosts want and no one should have to
|
|
56
|
+
read the gem to like the default:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
/insights/dashboards every frame
|
|
60
|
+
/insights/dashboards/3 one frame
|
|
61
|
+
/insights/orders/revenue/status a pane, grammar unchanged
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The segment removes the collision entirely, so the pane grammar from
|
|
65
|
+
ADR 005 needs no constraints or disambiguation and stays exactly as
|
|
66
|
+
published.
|
|
67
|
+
|
|
68
|
+
**A frame is addressed by its primary key, the Rails way.**
|
|
69
|
+
`resources :frames` inside the engine, looked up with
|
|
70
|
+
`find(params[:id])`. No slug column, therefore no uniqueness rules, no
|
|
71
|
+
reserved word list, no regeneration on rename, and no second lookup
|
|
72
|
+
path to keep working.
|
|
73
|
+
|
|
74
|
+
If readable URLs are wanted later, the Rails answer is to override
|
|
75
|
+
`to_param` to return `"3-quarterly-review"`. Rails resolves that back
|
|
76
|
+
through `to_i`, so lookups do not change and nothing has to be
|
|
77
|
+
unique. That is an option for a host or a later decision here, not a
|
|
78
|
+
schema.
|
|
79
|
+
|
|
80
|
+
**Using `resources` is also the editing surface.** ADR 012's second
|
|
81
|
+
editing surface, plain forms in the engine, is `index`, `new`, `edit`,
|
|
82
|
+
`update` and `destroy` on that resource. Choosing the conventional
|
|
83
|
+
route shape means the forms are the framework's defaults rather than
|
|
84
|
+
something invented.
|
|
85
|
+
|
|
86
|
+
**Janela ships an index, and a host may ignore it.** The mount's frame
|
|
87
|
+
segment renders a grid of cards, one per frame, with its name, how
|
|
88
|
+
many panes it has and when it changed. That is enough for a host to
|
|
89
|
+
install the gem and navigate on the same day, which is the point.
|
|
90
|
+
|
|
91
|
+
A host that wants something else writes its own page over
|
|
92
|
+
`Janela::Frame.all` and never routes to ours. This is the pattern from
|
|
93
|
+
ADR 011: ship a default that works, let the host replace it, add no
|
|
94
|
+
configuration for the choice.
|
|
95
|
+
|
|
96
|
+
**A richer browse is not this decision.** Rows grouped by category,
|
|
97
|
+
horizontal scrolling and hover previews are a different size of
|
|
98
|
+
feature, and a preview in particular is expensive because a thumbnail
|
|
99
|
+
means rendering panes. The card grid is the default; anything more
|
|
100
|
+
earns its own ADR once a host has enough frames to justify browsing
|
|
101
|
+
rather than listing.
|
|
102
|
+
|
|
103
|
+
## Consequences
|
|
104
|
+
|
|
105
|
+
- Nothing in a host's interface says "frame" unless the host wants it
|
|
106
|
+
to, and the word in the URL is the host's too. The code keeps one
|
|
107
|
+
vocabulary and the reader keeps theirs.
|
|
108
|
+
- One configuration value is added, for the path segment. It is
|
|
109
|
+
justified on the same grounds as ADR 005's mount path: it appears in
|
|
110
|
+
a URL a person reads. The noun in labels deliberately is not
|
|
111
|
+
configuration, because i18n already does it.
|
|
112
|
+
- Integer ids mean a frame URL is not self describing. `to_param` is
|
|
113
|
+
the escape hatch and costs nothing to adopt later.
|
|
114
|
+
- The index is a new view in the engine, so it is more surface to
|
|
115
|
+
style, which reinforces that the default stylesheet (#15) is part of
|
|
116
|
+
this body of work and not adjacent to it.
|
|
117
|
+
- Frames being a conventional Rails resource means the forms surface
|
|
118
|
+
arrives largely for free, which moves ADR 012's second editing stage
|
|
119
|
+
closer than it looked.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-09-15
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Related: ADR 001, ADR 002, ADR 005, ADR 008, ADR 009, ADR 010, ADR 011, ADR 012, ADR 013
|
|
5
|
+
Supersedes: part of ADR 012, part of ADR 013
|
|
6
|
+
Triggers:
|
|
7
|
+
- building frames or panes as records
|
|
8
|
+
- naming anything in the gem's public vocabulary
|
|
9
|
+
- adding a route, a layout or an index to the engine
|
|
10
|
+
- scoping or authorising a frame
|
|
11
|
+
- rendering a pane from a row rather than from URL parameters
|
|
12
|
+
Topics: frames, panes, naming, routes, layouts, authorisation, tenancy, sequencing
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# ADR 014: Corrections Before Frames Are Built
|
|
16
|
+
|
|
17
|
+
## Context
|
|
18
|
+
|
|
19
|
+
ADR 012 made frames and panes data and ADR 013 decided how a frame is
|
|
20
|
+
named and addressed. Both were accepted within an hour of each other
|
|
21
|
+
at the end of a long session. An independent review before any code
|
|
22
|
+
was written found three decisions that are wrong rather than
|
|
23
|
+
incomplete, and several claims that are overstated. This ADR corrects
|
|
24
|
+
them, records the naming rule that was implicit, and sequences the
|
|
25
|
+
build. It is the document to read before writing any of it.
|
|
26
|
+
|
|
27
|
+
## Decision
|
|
28
|
+
|
|
29
|
+
### A frame page renders its panes, not empty frames
|
|
30
|
+
|
|
31
|
+
ADR 013 has the engine serve an index and a show page for frames. ADR
|
|
32
|
+
011 has anything the engine serves render in Janela's own minimal
|
|
33
|
+
layout, which deliberately loads no assets. A frame page is a grid of
|
|
34
|
+
lazily loaded `turbo-frame` elements, so with no Turbo on the page
|
|
35
|
+
they never fetch and **the page renders blank**. The two ADRs conflict
|
|
36
|
+
and neither noticed.
|
|
37
|
+
|
|
38
|
+
A frame page therefore renders each pane's content inline, inside its
|
|
39
|
+
`turbo-frame`, on the first response. Cross-filtering still replaces
|
|
40
|
+
frames afterwards when Turbo is present, and without Turbo the page is
|
|
41
|
+
a correct static dashboard.
|
|
42
|
+
|
|
43
|
+
This is better than either ADR intended, for reasons beyond the
|
|
44
|
+
conflict: it removes a fan out of one request per pane on every load,
|
|
45
|
+
and it is what ADR 008 already required of a dashboard, that a shared
|
|
46
|
+
link be correct before any JavaScript runs. The host composed
|
|
47
|
+
dashboard should do the same, which makes `loading: :lazy` on a pane an
|
|
48
|
+
option rather than the default.
|
|
49
|
+
|
|
50
|
+
### A frame carries an opaque owner, and is always read through a scope
|
|
51
|
+
|
|
52
|
+
ADR 012 said authorisation is the host's Pundit policy on
|
|
53
|
+
`Janela::Frame`. A frame has no ownership column, so a multi tenant
|
|
54
|
+
host cannot write a `Scope` over it at all, and the shipped index
|
|
55
|
+
would list every tenant's frames with enumerable integer ids. One of
|
|
56
|
+
the two real hosts is multi tenant.
|
|
57
|
+
|
|
58
|
+
`Janela::Frame belongs_to :owner, polymorphic: true, optional: true`.
|
|
59
|
+
Janela never interprets the owner: it sets nothing, reads nothing from
|
|
60
|
+
it, and offers no roles. It exists so a host's policy `Scope` has
|
|
61
|
+
something to filter on. A single user host leaves it null.
|
|
62
|
+
|
|
63
|
+
Every controller lookup goes through the host's scope rather than
|
|
64
|
+
`find` on the class, so a frame belonging to another tenant is a 404
|
|
65
|
+
and ids stay unguessable in effect if not in form. The same correction
|
|
66
|
+
applies to the existing snapshot pane controller, which looks a
|
|
67
|
+
snapshot up unscoped today.
|
|
68
|
+
|
|
69
|
+
### A pane row is addressed as a row
|
|
70
|
+
|
|
71
|
+
ADR 012 assumed a persisted pane could be rendered through the
|
|
72
|
+
existing pane URL. It cannot, for two reasons found in the code. The
|
|
73
|
+
`turbo-frame` id is derived from the query, so two rows in one frame
|
|
74
|
+
with the same measure, dimension and renderer produce duplicate DOM
|
|
75
|
+
ids and Turbo replaces the wrong one. And a row's own title cannot
|
|
76
|
+
reach the response, because the controller builds everything from URL
|
|
77
|
+
parameters.
|
|
78
|
+
|
|
79
|
+
A pane row gets its own nested route, `frames/:frame_id/panes/:id`,
|
|
80
|
+
identified in the DOM as `janela_pane_<id>`, built from the row. The
|
|
81
|
+
ADR 005 pane grammar stays exactly as published and keeps its purpose:
|
|
82
|
+
an ad hoc pane, addressable and shareable without a row existing.
|
|
83
|
+
|
|
84
|
+
This also resolves a collision ADR 012 missed. The controller that
|
|
85
|
+
renders a query is already `PanesController`, and the forms surface
|
|
86
|
+
needs that name for CRUD. The query renderer keeps the ADR 005 URL and
|
|
87
|
+
is renamed with it.
|
|
88
|
+
|
|
89
|
+
### Frames sit at the mount root, and the path configuration is dropped
|
|
90
|
+
|
|
91
|
+
ADR 013 gave frames their own path segment, configurable, defaulting
|
|
92
|
+
to `dashboards`, and claimed this removed the collision with the pane
|
|
93
|
+
grammar entirely. That claim is false. `/dashboards/3` matches the
|
|
94
|
+
pane route as model `dashboards` and measure `3`, and worse, a host
|
|
95
|
+
model whose route key equals the chosen segment, `Report`, `Dashboard`
|
|
96
|
+
or `Insight`, has its panes shadowed by it. Those are precisely the
|
|
97
|
+
words the option exists to allow.
|
|
98
|
+
|
|
99
|
+
Frames live at the mount root with a numeric id constraint:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
/insights every frame
|
|
103
|
+
/insights/3 one frame
|
|
104
|
+
/insights/orders/revenue an ad hoc pane, grammar unchanged
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
No model route key is all digits, so nothing collides, and the mount
|
|
108
|
+
path is already the host's noun by ADR 005. The configuration value
|
|
109
|
+
ADR 013 added is deleted before it ships. ADR 001 prefers this.
|
|
110
|
+
|
|
111
|
+
### The security claim, stated accurately
|
|
112
|
+
|
|
113
|
+
ADR 012 claimed a persisted pane adds no query surface because
|
|
114
|
+
dimensions are the Ransack allowlist. The registry and the fetch or
|
|
115
|
+
raise lookups do hold, and a row genuinely cannot invent a query,
|
|
116
|
+
reach an unexposed model or widen what is filterable. Two corrections
|
|
117
|
+
to how it was stated:
|
|
118
|
+
|
|
119
|
+
- Dimensions are the allowlist **only when the model has not already
|
|
120
|
+
declared its own**. Janela skips generating the allowlist if the
|
|
121
|
+
host defined `ransackable_attributes`, so in that case the surface
|
|
122
|
+
is the host's list, not Janela's. Both real hosts define their own.
|
|
123
|
+
- Filter parameters are still permitted wholesale, so any Ransack
|
|
124
|
+
predicate on an allowed attribute is reachable. That is issue #8 and
|
|
125
|
+
is unchanged by frames.
|
|
126
|
+
|
|
127
|
+
Rows are validated against the registry on save, not only at render:
|
|
128
|
+
model, measure, dimension, renderer, granularity and limit. Otherwise
|
|
129
|
+
deleting a dimension from a `janela` block silently breaks every row
|
|
130
|
+
naming it, and the failure appears to a reader as a missing pane. A
|
|
131
|
+
pane's title is the first analyst authored text the gem renders, and
|
|
132
|
+
is escaped like any other string, never marked safe.
|
|
133
|
+
|
|
134
|
+
### A pane whose model has no policy fails as one pane
|
|
135
|
+
|
|
136
|
+
A frame spanning several models may include one the viewer's host has
|
|
137
|
+
no policy for, which raises inside a single pane request. That pane
|
|
138
|
+
renders a refusal in its own frame with a 403 rather than taking out
|
|
139
|
+
the response.
|
|
140
|
+
|
|
141
|
+
### Naming: uncommon but conceivable, so a host is not pigeonholed
|
|
142
|
+
|
|
143
|
+
The words in the gem's vocabulary, Janela, Frame, Pane, are chosen to
|
|
144
|
+
be uncommon and still conceivable. A class called `Dashboard` would
|
|
145
|
+
push every host into calling the thing a dashboard; `Frame` leaves
|
|
146
|
+
them free to say insight, report or scorecard through i18n while the
|
|
147
|
+
gem keeps one internal vocabulary. This is the same principle as the
|
|
148
|
+
mount path belonging to the host (ADR 005) and the noun coming from a
|
|
149
|
+
locale file (ADR 013), and it is a rule rather than a preference: **the
|
|
150
|
+
gem's own words should not become a host's product language.**
|
|
151
|
+
|
|
152
|
+
The review's argument for `Dashboard`, that it removes glue, is
|
|
153
|
+
therefore declined, and the glue is realigned to `Frame` instead:
|
|
154
|
+
|
|
155
|
+
- `janela_dashboard do ... end` becomes `janela_frame do ... end`, and
|
|
156
|
+
gains a record form, `janela_frame @frame`. One helper, two ways to
|
|
157
|
+
supply the panes.
|
|
158
|
+
- The Stimulus controller `janela--dashboard` becomes `janela--frame`,
|
|
159
|
+
and its file follows. Hosts register it by name, so this is a
|
|
160
|
+
breaking change for the two installations and belongs in the
|
|
161
|
+
changelog.
|
|
162
|
+
- The helper module becomes `Janela::FramesHelper`.
|
|
163
|
+
- Today's runtime `Janela::Pane` becomes `Janela::Query`, freeing
|
|
164
|
+
`Pane` for the record, as ADR 012 decided.
|
|
165
|
+
|
|
166
|
+
### Sequence: four builds, not one
|
|
167
|
+
|
|
168
|
+
1. **Rename only.** `Pane` to `Query`, `janela_dashboard` to
|
|
169
|
+
`janela_frame`, the Stimulus controller, the helper module. No new
|
|
170
|
+
behaviour, no records, both hosts updated for the controller name.
|
|
171
|
+
2. **Records and rendering.** Migrations, `Frame` and `Pane`, the
|
|
172
|
+
opaque owner, row validations, the nested pane route, inline first
|
|
173
|
+
render, the grid and the stylesheet. A host renders a frame in its
|
|
174
|
+
own page and owns the authorisation.
|
|
175
|
+
3. **The engine's own pages.** Index and show at the mount root, only
|
|
176
|
+
once 1 and 2 are settled and scoping is proven in a real host.
|
|
177
|
+
4. **Forms.** The analyst's editing surface, as conventional Rails
|
|
178
|
+
CRUD on the nested resources.
|
|
179
|
+
|
|
180
|
+
The visual editor keeps its own future ADR, and ADR 012's commitment
|
|
181
|
+
to it is softened here to undecided: the ownership argument justifies
|
|
182
|
+
records and forms, and does not by itself justify a canvas.
|
|
183
|
+
|
|
184
|
+
### Documentation that is now wrong
|
|
185
|
+
|
|
186
|
+
The README still lists a report designer as out of scope and says
|
|
187
|
+
there is no drag and drop designer. ADR 009 states there is no
|
|
188
|
+
Dashboard model. ADR 010's skill instructs agents not to build a
|
|
189
|
+
report designer, and requires the skill to describe only the README's
|
|
190
|
+
API. All three must be corrected as part of build 2, not after it.
|
|
191
|
+
|
|
192
|
+
## Consequences
|
|
193
|
+
|
|
194
|
+
- Nothing in ADR 012's core is reversed: frames and panes are still
|
|
195
|
+
data, created at runtime, and the HTML still derives from the rows.
|
|
196
|
+
What changes is how they are addressed, scoped and first rendered.
|
|
197
|
+
- One configuration value is removed before shipping and none is
|
|
198
|
+
added, so the gem still has exactly one, `parent_controller`.
|
|
199
|
+
- Inline first render means a frame page does one query per pane on
|
|
200
|
+
the server. At the scale a dashboard renders that is cheaper than
|
|
201
|
+
the request per pane it replaces, but it makes the ordering and
|
|
202
|
+
limit decision of ADR 007 load bearing for page speed rather than
|
|
203
|
+
only for legibility.
|
|
204
|
+
- The polymorphic owner is the first column Janela adds that it does
|
|
205
|
+
not itself use. It is a hook and is documented as one.
|
|
206
|
+
- Build 1 is a breaking change for two known installations with no
|
|
207
|
+
user visible benefit, which is the right moment to take it: the only
|
|
208
|
+
installations are the author's.
|
|
209
|
+
- Uncommon words are collision free by construction, which is the
|
|
210
|
+
point of the rule: no host names a model `Frame` or `Pane`, so the
|
|
211
|
+
class, the CSS hook, the DOM id and the sentence "the pane is not
|
|
212
|
+
loading" are unambiguous in any application with no coordination.
|
|
213
|
+
The application this gem was extracted from already has a
|
|
214
|
+
`Dashboard` model, a `DashboardPolicy` and a `dashboards` table, so
|
|
215
|
+
the declined name would have collided in conversation there
|
|
216
|
+
permanently. Active Storage's `Blob` and Action Text's `RichText`
|
|
217
|
+
are the same choice; Blazer's `Dashboard` is the counter example.
|
|
218
|
+
- The cost is a permanent translation tax on documentation. Every
|
|
219
|
+
README example, error message and line of ADR 010's skill has to
|
|
220
|
+
decide whether to speak the gem's word or the reader's, and a gloss
|
|
221
|
+
like "a frame is a dashboard" never stops being necessary. That is
|
|
222
|
+
accepted as much smaller than a name that collides in every host.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-09-16
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Related: ADR 001, ADR 010, ADR 014
|
|
5
|
+
Triggers:
|
|
6
|
+
- making any change a host must act on
|
|
7
|
+
- cutting a release whose version's middle number changes
|
|
8
|
+
- adding a deprecation, an alias or a compatibility shim
|
|
9
|
+
- adding or changing what janela:doctor checks
|
|
10
|
+
- writing anything an agent is expected to follow
|
|
11
|
+
Topics: releases, upgrades, documentation, ai, dx, deprecation
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# ADR 015: How Breaking Change Is Communicated
|
|
15
|
+
|
|
16
|
+
## Context
|
|
17
|
+
|
|
18
|
+
Build 1 of ADR 014 renamed a helper, a Stimulus controller, a helper
|
|
19
|
+
module, two controllers and a class. Every host has to act on it, and
|
|
20
|
+
the only record is `CHANGELOG.md`, which a reader has to know to look
|
|
21
|
+
for. The failures a host meets first are uninformative:
|
|
22
|
+
`undefined method 'janela_dashboard'` and `uninitialized constant
|
|
23
|
+
Janela::Pane` say nothing about what replaced them.
|
|
24
|
+
|
|
25
|
+
The people and agents upgrading this gem are the same ones ADR 010 was
|
|
26
|
+
written for. That ADR decided guidance ships inside the gem so nobody
|
|
27
|
+
has to rediscover what the gem already knows. An upgrade path is
|
|
28
|
+
guidance, and none has shipped. This is the first case where the
|
|
29
|
+
principle has something concrete to do.
|
|
30
|
+
|
|
31
|
+
A changelog is also the wrong document for the job. It records what
|
|
32
|
+
changed, for a reader deciding whether to upgrade. It does not tell
|
|
33
|
+
someone mid-upgrade what to do, in order, with exact before and after.
|
|
34
|
+
Those are different readers and mixing them serves neither.
|
|
35
|
+
|
|
36
|
+
## Decision
|
|
37
|
+
|
|
38
|
+
**`UPGRADING.md` ships inside the gem.** One section per version that
|
|
39
|
+
requires action, newest first, written as imperative steps with the
|
|
40
|
+
exact old and new text. Not prose: something a person or an agent can
|
|
41
|
+
work through and tick off. It is listed in the gemspec's files, so it
|
|
42
|
+
travels with an installed gem and is readable without network access.
|
|
43
|
+
|
|
44
|
+
The division of labour: `CHANGELOG.md` says what changed and why,
|
|
45
|
+
`UPGRADING.md` says what to do about it, `docs/decisions/` says why
|
|
46
|
+
the change was decided at all.
|
|
47
|
+
|
|
48
|
+
**A breaking release sets `post_install_message`.** One or two lines
|
|
49
|
+
naming the version and pointing at `UPGRADING.md`. Bundler prints it
|
|
50
|
+
when the gem is updated, which makes it the only mechanism that
|
|
51
|
+
reaches whoever, or whatever, ran the command at the moment they
|
|
52
|
+
needed it.
|
|
53
|
+
|
|
54
|
+
Used only for releases that require host action, and removed in the
|
|
55
|
+
release after. A post install message on every version is noise that
|
|
56
|
+
teaches people to skip it.
|
|
57
|
+
|
|
58
|
+
**`rails janela:doctor` reports what a host needs to fix.** One
|
|
59
|
+
command, a list of findings, exit status zero when clean. It checks
|
|
60
|
+
what experience says actually breaks a host:
|
|
61
|
+
|
|
62
|
+
- Stale identifiers from any previous version, for example
|
|
63
|
+
`janela--dashboard` or `janela_dashboard` in the host's markup and
|
|
64
|
+
JavaScript, or `Janela::Pane` in its Ruby.
|
|
65
|
+
- Whether Janela's Stimulus controllers are registered at all, which
|
|
66
|
+
is the failure that looks like nothing happening.
|
|
67
|
+
- A `through:` dimension whose associated model does not allowlist the
|
|
68
|
+
attribute, which Ransack refuses per class.
|
|
69
|
+
- Whether Janela's endpoints are reachable without authentication,
|
|
70
|
+
which depends entirely on what the host's `ApplicationController`
|
|
71
|
+
does.
|
|
72
|
+
- Whether the engine is mounted, and where.
|
|
73
|
+
|
|
74
|
+
This is the most agent shaped surface the gem can offer: one command
|
|
75
|
+
whose output is a list of actions rather than a paragraph to
|
|
76
|
+
interpret. It covers install as well as upgrade, so the traps ADR 010
|
|
77
|
+
listed become something a machine finds rather than something a person
|
|
78
|
+
remembers.
|
|
79
|
+
|
|
80
|
+
**No deprecation aliases and no compatibility shims.** A renamed
|
|
81
|
+
helper is not kept alive with a warning. The codebase's posture is
|
|
82
|
+
that a thing has one name, and a shim is a second name that lives
|
|
83
|
+
forever because nobody dares delete it. A loud failure plus a
|
|
84
|
+
findable, executable guide is better than a quiet alias, particularly
|
|
85
|
+
before 1.0 where the version number already says what to expect.
|
|
86
|
+
|
|
87
|
+
The exception, if one is ever wanted, is a constant that raises with a
|
|
88
|
+
message naming its replacement, because that is an error rather than a
|
|
89
|
+
second way to write working code. Not adopted now.
|
|
90
|
+
|
|
91
|
+
**ADR 010's skill will point at all three.** When the skill ships it
|
|
92
|
+
tells an agent to read `UPGRADING.md` before changing a version
|
|
93
|
+
constraint and to run `janela:doctor` after, which is the loop this
|
|
94
|
+
ADR exists to make possible.
|
|
95
|
+
|
|
96
|
+
## Consequences
|
|
97
|
+
|
|
98
|
+
- Three documents now have to stay in step on a breaking release:
|
|
99
|
+
changelog entry, upgrade section, and the doctor check that catches
|
|
100
|
+
the thing being renamed. The doctor is what keeps the other two
|
|
101
|
+
honest, because a check either finds the stale identifier or it does
|
|
102
|
+
not.
|
|
103
|
+
- `janela:doctor` reads the host's source, which is the first time the
|
|
104
|
+
gem inspects an application rather than serving it. It only reads,
|
|
105
|
+
reports, and changes nothing.
|
|
106
|
+
- A doctor check for authentication has to guess, since whether an
|
|
107
|
+
endpoint is public depends on the host's controller. It reports what
|
|
108
|
+
it observes and says plainly that it cannot be certain, rather than
|
|
109
|
+
asserting a system is safe.
|
|
110
|
+
- Refusing aliases means an upgrade cannot be gradual within one
|
|
111
|
+
version. That is the intended cost: the guide is short because there
|
|
112
|
+
is exactly one way the code can be.
|
|
113
|
+
- This ADR is itself the thing that makes 0.3.0 shippable to anyone
|
|
114
|
+
other than the author.
|