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
|
@@ -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
|
data/lib/janela/model.rb
ADDED
|
@@ -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
|
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: []
|