janela 0.1.0 → 0.2.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 +25 -0
- data/README.md +62 -11
- data/app/assets/javascripts/janela/chart_controller.js +9 -3
- data/app/assets/javascripts/janela/dashboard_controller.js +27 -9
- data/app/controllers/janela/{visuals_controller.rb → panes_controller.rb} +6 -4
- data/app/controllers/janela/snapshot_panes_controller.rb +21 -0
- data/app/helpers/janela/dashboard_helper.rb +38 -5
- data/app/jobs/janela/snapshot_job.rb +18 -0
- data/app/models/janela/pane.rb +118 -0
- data/app/models/janela/snapshot.rb +51 -0
- data/app/views/janela/panes/show.html.erb +45 -0
- data/config/routes.rb +7 -1
- data/db/migrate/20260915000001_create_janela_snapshots.rb +13 -0
- data/docs/decisions/004-charts-and-javascript-delivery.md +5 -4
- data/docs/decisions/005-pane-urls-and-mount-path.md +136 -0
- data/docs/decisions/006-time-dimensions-with-groupdate.md +93 -0
- data/docs/decisions/007-ordering-and-limits.md +74 -0
- data/docs/decisions/008-dashboard-filters-in-the-page-url.md +68 -0
- data/docs/decisions/009-snapshots.md +141 -0
- data/docs/decisions/INDEX.md +14 -6
- data/lib/janela/definition.rb +26 -11
- data/lib/janela/dimension.rb +39 -4
- data/lib/janela/measure.rb +6 -0
- data/lib/janela/version.rb +1 -1
- data/lib/janela.rb +10 -10
- metadata +28 -5
- data/Rakefile +0 -23
- data/app/models/janela/visual.rb +0 -65
- data/app/views/janela/visuals/show.html.erb +0 -36
data/lib/janela/measure.rb
CHANGED
|
@@ -22,5 +22,11 @@ module Janela
|
|
|
22
22
|
def apply(relation)
|
|
23
23
|
column ? relation.public_send(aggregate, column) : relation.public_send(aggregate)
|
|
24
24
|
end
|
|
25
|
+
|
|
26
|
+
# The column alias ActiveRecord gives a grouped calculation, so a
|
|
27
|
+
# relation can be ordered by the measure before it is calculated.
|
|
28
|
+
def sql_alias
|
|
29
|
+
"#{aggregate}_#{column || 'all'}"
|
|
30
|
+
end
|
|
25
31
|
end
|
|
26
32
|
end
|
data/lib/janela/version.rb
CHANGED
data/lib/janela.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "ransack"
|
|
2
|
+
require "groupdate"
|
|
2
3
|
require "turbo-rails"
|
|
3
4
|
require "stimulus-rails"
|
|
4
5
|
|
|
@@ -16,24 +17,23 @@ module Janela
|
|
|
16
17
|
# and authorisation apply to dashboards with no configuration.
|
|
17
18
|
mattr_accessor :parent_controller, default: "ApplicationController"
|
|
18
19
|
|
|
19
|
-
# Only models that declare a janela block are addressable over HTTP,
|
|
20
|
-
#
|
|
21
|
-
# rather than
|
|
22
|
-
# behind in development.
|
|
20
|
+
# Only models that declare a janela block are addressable over HTTP, keyed by
|
|
21
|
+
# the route key that appears in pane URLs (orders, sales_orders). Names are
|
|
22
|
+
# stored rather than classes so a reloaded model leaves nothing stale behind.
|
|
23
23
|
def self.registry
|
|
24
|
-
@registry ||=
|
|
24
|
+
@registry ||= {}
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def self.register(model)
|
|
28
|
-
registry
|
|
28
|
+
registry[model.model_name.route_key] = model.name
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def self.definition!(
|
|
31
|
+
def self.definition!(route_key)
|
|
32
32
|
# In development a model is only registered once autoloaded, so a cold
|
|
33
33
|
# lookup loads the app rather than constantizing an unvetted parameter.
|
|
34
|
-
Rails.application.eager_load! unless registry.
|
|
35
|
-
raise Error, "#{
|
|
34
|
+
Rails.application.eager_load! unless registry.key?(route_key)
|
|
35
|
+
class_name = registry.fetch(route_key) { raise Error, "#{route_key.inspect} is not a janela model" }
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
class_name.constantize.janela
|
|
38
38
|
end
|
|
39
39
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: janela
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jay Killeen
|
|
@@ -38,6 +38,20 @@ dependencies:
|
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: '4.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: groupdate
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '6.0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '6.0'
|
|
41
55
|
- !ruby/object:Gem::Dependency
|
|
42
56
|
name: turbo-rails
|
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -81,21 +95,29 @@ files:
|
|
|
81
95
|
- CHANGELOG.md
|
|
82
96
|
- LICENSE.txt
|
|
83
97
|
- README.md
|
|
84
|
-
- Rakefile
|
|
85
98
|
- app/assets/javascripts/janela/chart_controller.js
|
|
86
99
|
- app/assets/javascripts/janela/dashboard_controller.js
|
|
87
100
|
- app/assets/javascripts/janela/vendor/chart.js
|
|
88
101
|
- app/controllers/janela/application_controller.rb
|
|
89
|
-
- app/controllers/janela/
|
|
102
|
+
- app/controllers/janela/panes_controller.rb
|
|
103
|
+
- app/controllers/janela/snapshot_panes_controller.rb
|
|
90
104
|
- app/helpers/janela/dashboard_helper.rb
|
|
91
|
-
- app/
|
|
92
|
-
- app/
|
|
105
|
+
- app/jobs/janela/snapshot_job.rb
|
|
106
|
+
- app/models/janela/pane.rb
|
|
107
|
+
- app/models/janela/snapshot.rb
|
|
108
|
+
- app/views/janela/panes/show.html.erb
|
|
93
109
|
- config/importmap.rb
|
|
94
110
|
- config/routes.rb
|
|
111
|
+
- db/migrate/20260915000001_create_janela_snapshots.rb
|
|
95
112
|
- docs/decisions/001-built-to-be-forked.md
|
|
96
113
|
- docs/decisions/002-measures-and-dimensions-over-ransack.md
|
|
97
114
|
- docs/decisions/003-cross-filtering-with-turbo-frames.md
|
|
98
115
|
- docs/decisions/004-charts-and-javascript-delivery.md
|
|
116
|
+
- docs/decisions/005-pane-urls-and-mount-path.md
|
|
117
|
+
- docs/decisions/006-time-dimensions-with-groupdate.md
|
|
118
|
+
- docs/decisions/007-ordering-and-limits.md
|
|
119
|
+
- docs/decisions/008-dashboard-filters-in-the-page-url.md
|
|
120
|
+
- docs/decisions/009-snapshots.md
|
|
99
121
|
- docs/decisions/INDEX.md
|
|
100
122
|
- lib/janela.rb
|
|
101
123
|
- lib/janela/definition.rb
|
|
@@ -112,6 +134,7 @@ metadata:
|
|
|
112
134
|
source_code_uri: https://github.com/retail-tasker/janela
|
|
113
135
|
changelog_uri: https://github.com/retail-tasker/janela/blob/main/CHANGELOG.md
|
|
114
136
|
bug_tracker_uri: https://github.com/retail-tasker/janela/issues
|
|
137
|
+
rubygems_mfa_required: 'true'
|
|
115
138
|
rdoc_options: []
|
|
116
139
|
require_paths:
|
|
117
140
|
- lib
|
data/Rakefile
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
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]
|
data/app/models/janela/visual.rb
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
module Janela
|
|
2
|
-
# One measure grouped by one dimension, rendered as a table or a chart. A
|
|
3
|
-
# visual ignores filters on its own dimension: clicking a value in a visual
|
|
4
|
-
# should re-scope the others, not collapse itself to the value clicked.
|
|
5
|
-
class Visual
|
|
6
|
-
RENDERERS = %w[table bar].freeze
|
|
7
|
-
|
|
8
|
-
attr_reader :definition, :measure, :dimension, :renderer, :filters
|
|
9
|
-
|
|
10
|
-
# The helper renders the frame and the controller renders its replacement,
|
|
11
|
-
# so both derive the id the same way from the same parameters.
|
|
12
|
-
def self.frame_id(model:, measure:, by:, as: :table)
|
|
13
|
-
"janela_#{model.to_s.underscore}_#{measure}_by_#{by}_#{as}"
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def initialize(definition:, measure:, dimension:, renderer: "table", filters: {})
|
|
17
|
-
@definition = definition
|
|
18
|
-
@measure = measure
|
|
19
|
-
@dimension = dimension
|
|
20
|
-
@renderer = renderer.to_s
|
|
21
|
-
@filters = filters
|
|
22
|
-
|
|
23
|
-
raise Error, "unknown visual renderer #{renderer.inspect}" unless RENDERERS.include?(@renderer)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def model
|
|
27
|
-
definition.model
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def chart?
|
|
31
|
-
renderer != "table"
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def frame_id
|
|
35
|
-
self.class.frame_id(model: model.name, measure: measure, by: dimension, as: renderer)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def title
|
|
39
|
-
"#{measure.to_s.humanize} by #{dimension.to_s.humanize}"
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def result(on: nil)
|
|
43
|
-
definition.query(measure, by: dimension, where: applicable_filters, on: on)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def filter_key
|
|
47
|
-
"#{ransack_name}_eq"
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# The filter on this visual's own dimension is not applied to its query,
|
|
51
|
-
# but it is what the user clicked here, so the view highlights it.
|
|
52
|
-
def selected_value
|
|
53
|
-
filters[filter_key] || filters[filter_key.to_sym]
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
def ransack_name
|
|
58
|
-
definition.dimension!(dimension).ransack_name
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def applicable_filters
|
|
62
|
-
filters.reject { |key, _| key.to_s.start_with?(ransack_name) }
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
<%= turbo_frame_tag @visual.frame_id do %>
|
|
2
|
-
<% if @result.empty? %>
|
|
3
|
-
<p class="janela-visual janela-empty"><%= @visual.title %>: no data</p>
|
|
4
|
-
<% elsif @visual.chart? %>
|
|
5
|
-
<canvas class="janela-visual janela-chart"
|
|
6
|
-
data-controller="janela--chart"
|
|
7
|
-
data-action="janela--chart:toggle->janela--dashboard#toggle"
|
|
8
|
-
data-janela--chart-type-value="<%= @visual.renderer %>"
|
|
9
|
-
data-janela--chart-title-value="<%= @visual.title %>"
|
|
10
|
-
data-janela--chart-key-value="<%= @visual.filter_key %>"
|
|
11
|
-
data-janela--chart-selected-value="<%= @visual.selected_value %>"
|
|
12
|
-
data-janela--chart-labels-value="<%= @result.keys.to_json %>"
|
|
13
|
-
data-janela--chart-values-value="<%= @result.values.map(&:to_f).to_json %>"
|
|
14
|
-
role="img" aria-label="<%= @visual.title %>"></canvas>
|
|
15
|
-
<% else %>
|
|
16
|
-
<table class="janela-visual">
|
|
17
|
-
<caption><%= @visual.title %></caption>
|
|
18
|
-
<tbody>
|
|
19
|
-
<% @result.each do |value, measured| %>
|
|
20
|
-
<tr>
|
|
21
|
-
<td>
|
|
22
|
-
<button type="button"
|
|
23
|
-
aria-pressed="<%= value.to_s == @visual.selected_value.to_s && @visual.selected_value.present? %>"
|
|
24
|
-
data-action="janela--dashboard#toggle"
|
|
25
|
-
data-janela--dashboard-key-param="<%= @visual.filter_key %>"
|
|
26
|
-
data-janela--dashboard-value-param="<%= value %>">
|
|
27
|
-
<%= value %>
|
|
28
|
-
</button>
|
|
29
|
-
</td>
|
|
30
|
-
<td><%= number_with_delimiter(measured) %></td>
|
|
31
|
-
</tr>
|
|
32
|
-
<% end %>
|
|
33
|
-
</tbody>
|
|
34
|
-
</table>
|
|
35
|
-
<% end %>
|
|
36
|
-
<% end %>
|