janela 0.1.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 +7 -0
- data/CHANGELOG.md +26 -0
- data/LICENSE.txt +21 -0
- data/README.md +185 -0
- data/Rakefile +23 -0
- data/app/assets/javascripts/janela/chart_controller.js +44 -0
- data/app/assets/javascripts/janela/dashboard_controller.js +40 -0
- data/app/assets/javascripts/janela/vendor/chart.js +27 -0
- data/app/controllers/janela/application_controller.rb +11 -0
- data/app/controllers/janela/visuals_controller.rb +21 -0
- data/app/helpers/janela/dashboard_helper.rb +16 -0
- data/app/models/janela/visual.rb +65 -0
- data/app/views/janela/visuals/show.html.erb +36 -0
- data/config/importmap.rb +5 -0
- data/config/routes.rb +3 -0
- data/docs/decisions/001-built-to-be-forked.md +73 -0
- data/docs/decisions/002-measures-and-dimensions-over-ransack.md +125 -0
- data/docs/decisions/003-cross-filtering-with-turbo-frames.md +104 -0
- data/docs/decisions/004-charts-and-javascript-delivery.md +135 -0
- data/docs/decisions/INDEX.md +44 -0
- data/lib/janela/definition.rb +70 -0
- data/lib/janela/dimension.rb +30 -0
- data/lib/janela/engine.rb +21 -0
- data/lib/janela/measure.rb +26 -0
- data/lib/janela/model.rb +25 -0
- data/lib/janela/version.rb +5 -0
- data/lib/janela.rb +39 -0
- metadata +133 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b37ede25ec758841e03748b59d6ebce1044ffeacc50fa38eea78a95a57f731be
|
|
4
|
+
data.tar.gz: beae0fa4879019dfd161a5196bff0420b2053bccd6d7243a272bd3b173b77ce7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fb70200d571c6ba005f868336d05e4fbe40d7acc720ecd73895c328b92429b2606fd806b9e8a42fe68ca8fb6f47423f57cb1af9fba3424d5c2d1561a299a56d6
|
|
7
|
+
data.tar.gz: 0d78429d0dfdde06f0b77bdec85704d1f7d1a13e29046926937d9fb99346097d86f4420c74cea5035b48e22950813b11a53d8b4e5064e122eb7b128788803844
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-09-15
|
|
11
|
+
|
|
12
|
+
First alpha, installed from GitHub for testing in a single host application.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `janela do ... end` on an ActiveRecord model declares measures (`sum`, `count`, `average`, `minimum`, `maximum`) and dimensions, including dimensions reached through an association.
|
|
17
|
+
- `Model.janela.query(measure, by:, where:, on:)`. Filters are Ransack params; `on:` accepts any relation such as `policy_scope(Model)`.
|
|
18
|
+
- Declaring a dimension defines the model's Ransack allowlist. A filter Ransack would silently drop raises instead.
|
|
19
|
+
- Rails engine with `janela_dashboard` and `janela_visual` helpers. Each visual is a Turbo Frame; a Stimulus controller cross-filters them by rewriting frame `src`.
|
|
20
|
+
- Bar charts via Chart.js (`as: :bar`). A click on a bar is the same filter toggle as a click on a table value.
|
|
21
|
+
- The selected value on a visual's own dimension is marked `aria-pressed` on tables and highlighted on charts.
|
|
22
|
+
- npm package `@retail-tasker/janela` exposing the two Stimulus controllers for jsbundling hosts; importmap pins for importmap hosts, with a vendored Chart.js.
|
|
23
|
+
- Only models that declare a `janela` block are addressable over HTTP.
|
|
24
|
+
- ADRs 001 to 004 in `docs/decisions/`, shipped inside the gem.
|
|
25
|
+
|
|
26
|
+
[0.1.0]: https://github.com/retail-tasker/janela/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jay Killeen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Janela
|
|
2
|
+
|
|
3
|
+
PowerBI-style dashboards and cross-filtering slicers, native to Rails and ActiveRecord. Define a dashboard on your models and associations, get a live, sliceable view for internal use, or publish the same definition as a locked, static view for an external audience.
|
|
4
|
+
|
|
5
|
+
## First principle
|
|
6
|
+
|
|
7
|
+
Janela is a PowerBI-style library built on Ruby and Stimulus, meant to drop onto any Ruby on Rails application. No JS framework, no build step of its own, no separate frontend app. Just a gem you add to an existing Rails app's Gemfile and two Stimulus controllers that ship with it.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
Every Rails BI option today is one of:
|
|
12
|
+
|
|
13
|
+
- **SQL-first** (Blazer). Powerful, but the query is a black box to your models and associations.
|
|
14
|
+
- **Admin-panel-first** (RailsAdmin, ActiveAdmin, Motor Admin, Avo). Association-aware filtering, but built for CRUD, not for composing multiple charts that filter each other.
|
|
15
|
+
- **A dead end for cross-filtering**. None of the above let clicking one chart re-scope every other chart on the page. That's the actual PowerBI/Tableau slicer experience, and nothing in the Rails ecosystem does it as a first-class citizen.
|
|
16
|
+
|
|
17
|
+
Janela's bet: the same dashboard definition should serve two audiences without being two systems.
|
|
18
|
+
|
|
19
|
+
- **Dynamic mode**. Internal analysts get live slicers, cross-filtering, free exploration.
|
|
20
|
+
- **Static mode**. The same dashboard, published, is frozen and locked for client-facing consumption. No slicers, no surprises, opinionated.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Janela is pre-release and installed from GitHub. It has two halves, a gem and an npm package, installed the same way:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
# Gemfile
|
|
28
|
+
gem "janela", github: "retail-tasker/janela"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
# config/routes.rb
|
|
33
|
+
mount Janela::Engine => "/janela"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then register the two Stimulus controllers. How depends on how your app ships JavaScript.
|
|
37
|
+
|
|
38
|
+
**With jsbundling (esbuild, bun, webpack):**
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
yarn add github:retail-tasker/janela # or: npm install github:retail-tasker/janela
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
// app/javascript/controllers/index.js
|
|
46
|
+
import JanelaDashboardController from "@retail-tasker/janela/dashboard_controller"
|
|
47
|
+
import JanelaChartController from "@retail-tasker/janela/chart_controller"
|
|
48
|
+
application.register("janela--dashboard", JanelaDashboardController)
|
|
49
|
+
application.register("janela--chart", JanelaChartController)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The chart controller imports `chart.js`, which is a peer dependency: add `chart.js` to your own `package.json` if it is not there already.
|
|
53
|
+
|
|
54
|
+
**With importmap-rails:**
|
|
55
|
+
|
|
56
|
+
Nothing to install. The engine pins `janela/dashboard_controller`, `janela/chart_controller` and a vendored `chart.js` for you (your own `chart.js` pin wins if you have one). Register the controllers:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// app/javascript/application.js
|
|
60
|
+
import JanelaDashboardController from "janela/dashboard_controller"
|
|
61
|
+
import JanelaChartController from "janela/chart_controller"
|
|
62
|
+
application.register("janela--dashboard", JanelaDashboardController)
|
|
63
|
+
application.register("janela--chart", JanelaChartController)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Requires Rails 8.0+ and Ruby 3.3+. If your app is on Rails 8.1.x with the `json` gem at 3.x, encrypted cookie reads raise inside ActiveSupport and every Turbo Frame request will 500 in the browser; pin `gem "json", "< 3"` until Rails ships the fix.
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
Declare measures and dimensions on the model:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
class Order < ApplicationRecord
|
|
74
|
+
belongs_to :customer
|
|
75
|
+
|
|
76
|
+
janela do
|
|
77
|
+
measure :revenue, sum: :amount
|
|
78
|
+
measure :orders, count: true
|
|
79
|
+
|
|
80
|
+
dimension :status
|
|
81
|
+
dimension :region, through: :customer
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then query them:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
Order.janela.query(:revenue) # => 375
|
|
90
|
+
Order.janela.query(:revenue, by: :status) # => { "paid" => 300, "refunded" => 50, "pending" => 25 }
|
|
91
|
+
Order.janela.query(:revenue, by: :region) # => { "APAC" => 150, "EU" => 225 }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Filters are [Ransack](https://github.com/activerecord-hackery/ransack) params, so a slicer built with `search_form_for` can pass `params[:q]` straight through:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
Order.janela.query(:revenue, by: :region, where: { status_in: %w[paid pending] })
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Scope a query to whatever the current user is allowed to see with `on:`:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
Order.janela.query(:revenue, by: :status, on: policy_scope(Order))
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Declaring a dimension makes that attribute filterable, so Janela defines the model's Ransack allowlist for you. A model that already defines its own keeps it. A `through:` dimension also needs the **associated** model to allow the attribute, because Ransack's allowlist is per-class:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
class Customer < ApplicationRecord
|
|
110
|
+
def self.ransackable_attributes(_auth_object = nil) = %w[region]
|
|
111
|
+
def self.ransackable_associations(_auth_object = nil) = []
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Dashboards
|
|
116
|
+
|
|
117
|
+
Compose visuals on any page. Each visual is a Turbo Frame; clicking a value in one re-scopes the others:
|
|
118
|
+
|
|
119
|
+
```erb
|
|
120
|
+
<%= janela_dashboard do %>
|
|
121
|
+
<button type="button" data-action="janela--dashboard#clear">Clear filters</button>
|
|
122
|
+
|
|
123
|
+
<%= janela_visual Order, :revenue, by: :status, as: :bar %>
|
|
124
|
+
<%= janela_visual Order, :revenue, by: :region %>
|
|
125
|
+
<%= janela_visual Order, :orders, by: :region %>
|
|
126
|
+
<% end %>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`as:` is `:table` by default or `:bar` for a Chart.js bar chart. A chart fills its container's width at Chart.js's default aspect ratio, so wrap it in an element with the width you want. Clicking a bar does exactly what clicking a table value does.
|
|
130
|
+
|
|
131
|
+
A visual ignores filters on its own dimension, so clicking a value re-scopes the rest of the dashboard rather than collapsing the visual you clicked. The selected value is marked `aria-pressed="true"` on tables and drawn solid against faded siblings on charts, so it can be styled and read. A visual with no matching rows renders a `.janela-empty` paragraph. Only models that declare a `janela` block can be requested over HTTP.
|
|
132
|
+
|
|
133
|
+
### Securing dashboards
|
|
134
|
+
|
|
135
|
+
Janela's controllers inherit from your `ApplicationController`, so they are exactly as public as it is. If your app authenticates per controller rather than globally, add this once:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
# config/initializers/janela.rb
|
|
139
|
+
Rails.application.config.to_prepare do
|
|
140
|
+
Janela::ApplicationController.prepend_before_action do
|
|
141
|
+
redirect_to main_app.new_session_path unless user_signed_in?
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Two details matter. It is *prepended* so it runs before any filter on your `ApplicationController` that assumes a signed-in user (tenant lookups, audit logging). It redirects through `main_app` because Janela is an isolated engine, so a bare `new_session_path` inside it resolves against Janela's own routes and fails.
|
|
147
|
+
|
|
148
|
+
Scoping is automatic when you use Pundit: `Janela::ApplicationController` calls `policy_scope(model)` if your `ApplicationController` defines it, and falls back to `model.all` otherwise. Every model you put on a dashboard needs a policy with a `Scope`.
|
|
149
|
+
|
|
150
|
+
## Design
|
|
151
|
+
|
|
152
|
+
Janela ships the load-bearing core of a BI tool and nothing else. The reasoning is recorded in [`docs/decisions/`](docs/decisions/INDEX.md), starting with ADR 001.
|
|
153
|
+
|
|
154
|
+
- **Measures and dimensions are a Ruby DSL on the model**, config-as-code like `routes.rb`. No drag-and-drop designer.
|
|
155
|
+
- **Querying rides on [Ransack](https://github.com/activerecord-hackery/ransack)'s association-path traversal.** Janela does not invent a query language.
|
|
156
|
+
- **Cross-filtering is a Stimulus controller plus Turbo Frames.** Click a value in one visual, shared filter state updates, every other frame on the page re-renders.
|
|
157
|
+
- **Charts are [Chart.js](https://www.chartjs.org)**, driven by one small Stimulus controller from the same values the tables show. Not a charting engine.
|
|
158
|
+
- **Publishing creates a Snapshot.** An ActiveJob freezes the result set into a new record; the live dashboard stays editable and the published view is a point-in-time fork, not a toggle on the same record. Not built yet.
|
|
159
|
+
|
|
160
|
+
Deliberately out of scope: report designer UI, natural-language query, a separate data warehouse, a row-level-security subsystem (use your app's Pundit/CanCanCan), refresh-scheduling UI (schedule the Snapshot job with whatever you already use), embedding SDK, mobile app, print/paginated reports. If you need one of those, the codebase is meant to be small enough to fork and add your own.
|
|
161
|
+
|
|
162
|
+
## Status
|
|
163
|
+
|
|
164
|
+
**v0.1.0 alpha.** The measures/dimensions DSL, cross-filtering, and bar charts work and are covered by unit and real-browser tests. Not yet built: time-granularity dimensions, other chart types, filter state in the page URL, published snapshots. Open work is in [GitHub Issues](https://github.com/retail-tasker/janela/issues).
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
Janela is a Rails engine. It ships with a minimal host application in `test/dummy` that mounts the engine at `/janela`, so the gem is always developed and tested against a real Rails app with a real (SQLite) database.
|
|
169
|
+
|
|
170
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
bin/rails test # unit tests
|
|
174
|
+
bundle exec rake # unit tests + RuboCop, what CI runs
|
|
175
|
+
bundle exec rake system # cross-filtering and charts in headless Chrome
|
|
176
|
+
bin/rails console # console inside the dummy app, engine loaded
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Contributing
|
|
180
|
+
|
|
181
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/retail-tasker/janela. Pull requests are reviewed on the merits of the diff, whether a person or an agent wrote them. Contributors are expected to adhere to the [code of conduct](https://github.com/retail-tasker/janela/blob/main/CODE_OF_CONDUCT.md).
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require "bundler/setup"
|
|
2
|
+
|
|
3
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
4
|
+
load "rails/tasks/engine.rake"
|
|
5
|
+
|
|
6
|
+
require "bundler/gem_tasks"
|
|
7
|
+
|
|
8
|
+
require "minitest/test_task"
|
|
9
|
+
|
|
10
|
+
Minitest::TestTask.create do |t|
|
|
11
|
+
t.test_globs = Dir["test/**/*_test.rb"] - Dir["test/system/**/*_test.rb"]
|
|
12
|
+
t.warning = false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Minitest::TestTask.create(:system) do |t|
|
|
16
|
+
t.test_globs = [ "test/system/**/*_test.rb" ]
|
|
17
|
+
t.warning = false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
require "rubocop/rake_task"
|
|
21
|
+
RuboCop::RakeTask.new
|
|
22
|
+
|
|
23
|
+
task default: %i[test rubocop]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { Chart, registerables } from "chart.js"
|
|
3
|
+
|
|
4
|
+
Chart.register(...registerables)
|
|
5
|
+
|
|
6
|
+
// Renders one visual as a chart and turns a click on a bar into the same
|
|
7
|
+
// toggle event a table button emits, so the dashboard controller cannot tell
|
|
8
|
+
// the difference. Turbo replaces the frame on every cross-filter, so the chart
|
|
9
|
+
// is destroyed on disconnect and rebuilt on connect.
|
|
10
|
+
export default class extends Controller {
|
|
11
|
+
static values = { type: String, labels: Array, values: Array, key: String, title: String, selected: String }
|
|
12
|
+
|
|
13
|
+
connect() {
|
|
14
|
+
this.chart = new Chart(this.element, {
|
|
15
|
+
type: this.typeValue,
|
|
16
|
+
data: {
|
|
17
|
+
labels: this.labelsValue,
|
|
18
|
+
datasets: [{ label: this.titleValue, data: this.valuesValue, backgroundColor: this.colours() }]
|
|
19
|
+
},
|
|
20
|
+
options: {
|
|
21
|
+
animation: false,
|
|
22
|
+
plugins: { legend: { display: false } },
|
|
23
|
+
onClick: (_event, elements) => {
|
|
24
|
+
if (elements.length === 0) return
|
|
25
|
+
const value = this.labelsValue[elements[0].index]
|
|
26
|
+
this.dispatch("toggle", { detail: { key: this.keyValue, value } })
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// With nothing selected every bar is solid; with a selection only that bar is.
|
|
33
|
+
colours() {
|
|
34
|
+
const selected = this.selectedValue
|
|
35
|
+
return this.labelsValue.map((label) =>
|
|
36
|
+
!selected || label === selected ? "rgba(54, 162, 235, 0.9)" : "rgba(54, 162, 235, 0.25)"
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
disconnect() {
|
|
41
|
+
this.chart?.destroy()
|
|
42
|
+
this.chart = null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Shared filter state for every visual on the page. Clicking a value rewrites
|
|
4
|
+
// each frame's src, and Turbo reloads a frame whenever its src changes, so
|
|
5
|
+
// cross-filtering needs no streams, no sockets and no state library.
|
|
6
|
+
export default class extends Controller {
|
|
7
|
+
static targets = ["visual"]
|
|
8
|
+
static values = { filters: Object }
|
|
9
|
+
|
|
10
|
+
// Table buttons send key/value as Stimulus action params; charts dispatch a
|
|
11
|
+
// custom event carrying them in detail. Either way it is one filter toggle.
|
|
12
|
+
toggle(event) {
|
|
13
|
+
const { key, value } = { ...event.detail, ...event.params }
|
|
14
|
+
const filters = { ...this.filtersValue }
|
|
15
|
+
|
|
16
|
+
if (filters[key] === String(value)) {
|
|
17
|
+
delete filters[key]
|
|
18
|
+
} else {
|
|
19
|
+
filters[key] = String(value)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.filtersValue = filters
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
clear() {
|
|
26
|
+
this.filtersValue = {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
filtersValueChanged() {
|
|
30
|
+
this.visualTargets.forEach((visual) => {
|
|
31
|
+
const url = new URL(visual.dataset.janelaSrc, window.location.origin)
|
|
32
|
+
|
|
33
|
+
for (const [key, value] of Object.entries(this.filtersValue)) {
|
|
34
|
+
url.searchParams.set(`q[${key}]`, value)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (visual.src !== url.href) visual.src = url.href
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
}
|