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.
@@ -0,0 +1,30 @@
1
+ module Janela
2
+ class Dimension
3
+ attr_reader :name, :model, :through
4
+
5
+ def initialize(name, model:, through: nil)
6
+ @name = name
7
+ @model = model
8
+ @through = through
9
+
10
+ raise Error, "#{model} has no association #{through.inspect}" if through && reflection.nil?
11
+ end
12
+
13
+ def attribute
14
+ klass.arel_table[name]
15
+ end
16
+
17
+ def ransack_name
18
+ through ? "#{through}_#{name}" : name.to_s
19
+ end
20
+
21
+ private
22
+ def klass
23
+ through ? reflection.klass : model
24
+ end
25
+
26
+ def reflection
27
+ model.reflect_on_association(through)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ require "rails/engine"
2
+
3
+ module Janela
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Janela
6
+
7
+ initializer "janela.model" do
8
+ ActiveSupport.on_load(:active_record) { extend Janela::Model }
9
+ end
10
+
11
+ # isolate_namespace keeps engine helpers out of the host, but the dashboard
12
+ # helpers are the engine's public API and belong in the host's views.
13
+ initializer "janela.helpers" do
14
+ ActiveSupport.on_load(:action_view) { include Janela::DashboardHelper }
15
+ end
16
+
17
+ initializer "janela.importmap", before: "importmap" do |app|
18
+ app.config.importmap.paths << root.join("config/importmap.rb") if app.config.respond_to?(:importmap)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ module Janela
2
+ class Measure
3
+ AGGREGATES = %i[sum count average minimum maximum].freeze
4
+
5
+ attr_reader :name, :aggregate, :column
6
+
7
+ def self.build(name, **options)
8
+ aggregate, column = options.first
9
+ unless options.size == 1 && AGGREGATES.include?(aggregate)
10
+ raise Error, "measure #{name.inspect} needs exactly one of #{AGGREGATES.join(', ')}"
11
+ end
12
+
13
+ new(name, aggregate, column == true ? nil : column)
14
+ end
15
+
16
+ def initialize(name, aggregate, column)
17
+ @name = name
18
+ @aggregate = aggregate
19
+ @column = column
20
+ end
21
+
22
+ def apply(relation)
23
+ column ? relation.public_send(aggregate, column) : relation.public_send(aggregate)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ module Janela
2
+ module Model
3
+ def janela(&block)
4
+ return @janela_definition unless block
5
+
6
+ @janela_definition = Definition.new(self)
7
+ @janela_definition.instance_eval(&block)
8
+ define_janela_ransack_allowlist
9
+ Janela.register(self)
10
+ @janela_definition
11
+ end
12
+
13
+ private
14
+ # Dimensions are the only things Janela filters on, so they are the
15
+ # Ransack allowlist. A model that already declares its own allowlist
16
+ # keeps it.
17
+ def define_janela_ransack_allowlist
18
+ return if singleton_class.method_defined?(:ransackable_attributes, false)
19
+
20
+ definition = @janela_definition
21
+ define_singleton_method(:ransackable_attributes) { |_auth_object = nil| definition.ransackable_attributes }
22
+ define_singleton_method(:ransackable_associations) { |_auth_object = nil| definition.ransackable_associations }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Janela
4
+ VERSION = "0.1.0"
5
+ end
data/lib/janela.rb ADDED
@@ -0,0 +1,39 @@
1
+ require "ransack"
2
+ require "turbo-rails"
3
+ require "stimulus-rails"
4
+
5
+ require "janela/version"
6
+ require "janela/engine"
7
+ require "janela/model"
8
+ require "janela/definition"
9
+ require "janela/measure"
10
+ require "janela/dimension"
11
+
12
+ module Janela
13
+ class Error < StandardError; end
14
+
15
+ # Janela's controllers inherit from the host's, so the host's authentication
16
+ # and authorisation apply to dashboards with no configuration.
17
+ mattr_accessor :parent_controller, default: "ApplicationController"
18
+
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.
23
+ def self.registry
24
+ @registry ||= Set.new
25
+ end
26
+
27
+ def self.register(model)
28
+ registry << model.name
29
+ end
30
+
31
+ def self.definition!(name)
32
+ # In development a model is only registered once autoloaded, so a cold
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)
36
+
37
+ name.constantize.janela
38
+ end
39
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: janela
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jay Killeen
8
+ - Vanessa Soares
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '8.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '8.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ransack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: turbo-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: stimulus-rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ description: Janela lets you define dashboards directly on your ActiveRecord models
70
+ and associations. Slicers, cross-filtering charts, and dimension drill-down as first-class
71
+ Rails citizens, not a bolted-on admin panel. Built on Ruby and Stimulus so it drops
72
+ onto any Rails application with no separate frontend build step. Dashboards can
73
+ run live and interactive for internal analysts, or be published as locked, static
74
+ views for external audiences, from the same underlying definition.
75
+ email:
76
+ - jay@retailtasker.com.au
77
+ executables: []
78
+ extensions: []
79
+ extra_rdoc_files: []
80
+ files:
81
+ - CHANGELOG.md
82
+ - LICENSE.txt
83
+ - README.md
84
+ - Rakefile
85
+ - app/assets/javascripts/janela/chart_controller.js
86
+ - app/assets/javascripts/janela/dashboard_controller.js
87
+ - app/assets/javascripts/janela/vendor/chart.js
88
+ - app/controllers/janela/application_controller.rb
89
+ - app/controllers/janela/visuals_controller.rb
90
+ - app/helpers/janela/dashboard_helper.rb
91
+ - app/models/janela/visual.rb
92
+ - app/views/janela/visuals/show.html.erb
93
+ - config/importmap.rb
94
+ - config/routes.rb
95
+ - docs/decisions/001-built-to-be-forked.md
96
+ - docs/decisions/002-measures-and-dimensions-over-ransack.md
97
+ - docs/decisions/003-cross-filtering-with-turbo-frames.md
98
+ - docs/decisions/004-charts-and-javascript-delivery.md
99
+ - docs/decisions/INDEX.md
100
+ - lib/janela.rb
101
+ - lib/janela/definition.rb
102
+ - lib/janela/dimension.rb
103
+ - lib/janela/engine.rb
104
+ - lib/janela/measure.rb
105
+ - lib/janela/model.rb
106
+ - lib/janela/version.rb
107
+ homepage: https://github.com/retail-tasker/janela
108
+ licenses:
109
+ - MIT
110
+ metadata:
111
+ allowed_push_host: https://rubygems.org
112
+ source_code_uri: https://github.com/retail-tasker/janela
113
+ changelog_uri: https://github.com/retail-tasker/janela/blob/main/CHANGELOG.md
114
+ bug_tracker_uri: https://github.com/retail-tasker/janela/issues
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: 3.3.0
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ requirements: []
129
+ rubygems_version: 3.6.9
130
+ specification_version: 4
131
+ summary: PowerBI-style dashboards and cross-filtering slicers, native to Rails and
132
+ ActiveRecord.
133
+ test_files: []