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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Janela
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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, so a
20
- # request parameter can never name an arbitrary class. Names are stored
21
- # rather than definitions so a reloaded model does not leave a stale class
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 ||= Set.new
24
+ @registry ||= {}
25
25
  end
26
26
 
27
27
  def self.register(model)
28
- registry << model.name
28
+ registry[model.model_name.route_key] = model.name
29
29
  end
30
30
 
31
- def self.definition!(name)
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.include?(name)
35
- raise Error, "#{name.inspect} is not a janela model" unless registry.include?(name)
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
- name.constantize.janela
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.1.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/visuals_controller.rb
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/models/janela/visual.rb
92
- - app/views/janela/visuals/show.html.erb
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]
@@ -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 %>