dashboards 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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +8 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +183 -0
  5. data/Rakefile +4 -0
  6. data/app/assets/config/dashboards_manifest.js +1 -0
  7. data/app/assets/javascript/dashboards/application.js +3 -0
  8. data/app/assets/stylesheets/dashboards/application.css +5 -0
  9. data/app/assets/stylesheets/dashboards/box.css +19 -0
  10. data/app/assets/stylesheets/dashboards/components.css +37 -0
  11. data/app/assets/stylesheets/dashboards/dashboard.css +41 -0
  12. data/app/assets/stylesheets/dashboards/layout.css +12 -0
  13. data/app/controllers/dashboards/base_controller.rb +5 -0
  14. data/app/controllers/dashboards/dashboards_controller.rb +21 -0
  15. data/app/views/dashboards/dashboards/no_dashboards.html.erb +26 -0
  16. data/app/views/dashboards/dashboards/show.html.erb +22 -0
  17. data/app/views/layouts/dashboards/application.html.erb +24 -0
  18. data/config/importmap.rb +2 -0
  19. data/config/routes.rb +4 -0
  20. data/lib/dashboards/configuration.rb +21 -0
  21. data/lib/dashboards/dsl/box.rb +34 -0
  22. data/lib/dashboards/dsl/change_over_period.rb +56 -0
  23. data/lib/dashboards/dsl/chart.rb +87 -0
  24. data/lib/dashboards/dsl/dashboard.rb +17 -0
  25. data/lib/dashboards/dsl/element.rb +26 -0
  26. data/lib/dashboards/dsl/metric.rb +56 -0
  27. data/lib/dashboards/dsl/summary.rb +53 -0
  28. data/lib/dashboards/dsl/table.rb +67 -0
  29. data/lib/dashboards/dsl.rb +30 -0
  30. data/lib/dashboards/engine.rb +33 -0
  31. data/lib/dashboards/loader.rb +23 -0
  32. data/lib/dashboards/version.rb +5 -0
  33. data/lib/dashboards.rb +36 -0
  34. data/sig/dashboards.rbs +4 -0
  35. metadata +178 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a8670fb127e114c56df0a9dc69c2aa2c66828d4b6f73f9b4d97b360d6381450b
4
+ data.tar.gz: 604825e5112690747b03341162ea63e2e2e302bc37ce87fa2acea85b6b6c1443
5
+ SHA512:
6
+ metadata.gz: f29c9255b958f8d5a9ad30f7b1a8e2168f493eb6c231588843073d6d66f97c22680ed07a977f6989db530ad4e799804b284a24e0f449412388185ebd1e53e5be
7
+ data.tar.gz: 706a900eb7c1475d95d699ca3c9292c857265b1061d4f77a308892f5b88b939024ca3bf59212a0481000a370de660ba4c3f4b8e3046c8e98cc629523cc9e6bf8
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2024-09-28
4
+
5
+ - Initial release of the `dashboards` gem
6
+ - Basic DSL for defining dashboards, boxes, and elements
7
+ - Rails integration via mountable engine
8
+ - Chartkick integration for chart rendering
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Javi R
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,183 @@
1
+ # 🍱 `dashboards` - Ruby gem to create customizable bento-style admin dashboards in your Rails app
2
+
3
+ `dashboards` is a Ruby gem that allows you to create beautiful admin dashboards in your Rails application with a very simple and straightforward DSL.
4
+
5
+ Creating a dashboard looks something like this:
6
+ ```ruby
7
+ dashboard "Admin Dashboard" do
8
+ box "Total Users" do
9
+ metric value: -> { User.count }
10
+ end
11
+
12
+ box "Posts over time" do
13
+ chart type: :line, data: -> { Post.group_by_day(:created_at).count }
14
+ end
15
+ end
16
+ ```
17
+
18
+ Which automatically creates a beautiful bento-style dashboard like this:
19
+
20
+ ![Dashboard](https://via.placeholder.com/150)
21
+
22
+ `dashboards` has a minimal setup so you can quickly build dashboards with metrics, charts, tables, summaries, and change-over-period indicators.
23
+
24
+ It uses [Chartkick](https://github.com/ankane/chartkick) for charts, and [groupdate](https://github.com/ankane/groupdate) for time-based grouping.
25
+
26
+ ## Installation
27
+
28
+ Add this line to your application's Gemfile:
29
+ ```ruby
30
+ gem 'dashboards'
31
+ ```
32
+
33
+ And then execute:
34
+ ```bash
35
+ bundle install
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ 1. Create a file `config/dashboards.rb` in your Rails application, and define your dashboard using the Dashboards DSL:
41
+ ```ruby
42
+ dashboard "Admin Dashboard" do
43
+ box "User Statistics" do
44
+ metric value: -> { User.count }
45
+ summary data: -> { User }
46
+ chart "User Signups", type: :line, data: -> { User.group_by_day(:created_at).count }
47
+ change_over_period -> { User }
48
+ end
49
+
50
+ box "Post Statistics" do
51
+ chart "Posts by Category", type: :pie, data: -> { Post.group(:category).count }
52
+ table "Recent Posts", data: -> { Post.order(created_at: :desc).limit(5) }
53
+ end
54
+ end
55
+ ```
56
+
57
+ 2. Mount the Dashboards engine in your `config/routes.rb`:
58
+ ```ruby
59
+ mount Dashboards::Engine, at: "/admin/dashboard"
60
+ ```
61
+
62
+ It's a good idea to make sure you're adding some authentication to the `dashboards` route to avoid exposing sensitive information:
63
+ ```ruby
64
+ authenticate :user, ->(user) { user.admin? } do
65
+ mount Dashboards::Engine, at: "/admin/dashboard"
66
+ end
67
+ ```
68
+
69
+ 3. Visit `/admin/dashboard` in your browser to see your new dashboard!
70
+
71
+ ## Available Components
72
+
73
+ ### Metrics
74
+
75
+ ```ruby
76
+ metric value: -> { User.count }
77
+ ```
78
+
79
+ Metrics display a single value, a big number inside the box.
80
+
81
+ Metrics can be either a generic number or a currency:
82
+
83
+ ```ruby
84
+ metric value: -> { Order.sum(:total) }, currency: "$"
85
+ ```
86
+
87
+ You can also display a percentage:
88
+
89
+ ```ruby
90
+ # This will show, for example, a percentage of users that have posted at least one time.
91
+ metric value: -> { (Post.group(:user_id).count.uniq.count.to_f / User.count * 100).round(0) }, percentage: true
92
+ ```
93
+
94
+ You can also pretty print big numbers:
95
+
96
+ ```ruby
97
+ # This will display 1.2B, 1.2M, or 1.2K for 1.2 billion, million, or thousand.
98
+ metric value: -> { 1234567890 }, format_big_numbers: true
99
+ ```
100
+
101
+ ### Charts
102
+
103
+ ```ruby
104
+ chart type: :line, data: -> { User.group_by_day(:created_at).count }
105
+ ```
106
+
107
+ Supported chart types: `:line`, `:bar`, `:column`, `:area`, `:pie`
108
+
109
+ Charts can be customized with colors, height, and other options:
110
+
111
+ ```ruby
112
+ chart type: :line, data: -> { Order.group_by_month(:created_at).sum(:total) },
113
+ color: "#4CAF50", height: "300px"
114
+ ```
115
+
116
+ You can also add a title to the chart:
117
+
118
+ ```ruby
119
+ chart "Order Totals", type: :line, data: -> { Order.group_by_month(:created_at).sum(:total) },
120
+ color: "#4CAF50", height: "300px", title: "Monthly Order Totals"
121
+ ```
122
+
123
+ ### Tables
124
+
125
+ ```ruby
126
+ table data: -> { { "US" => 100, "UK" => 200, "CA" => 300 } }
127
+ ```
128
+
129
+ Tables display data in a tabular format.
130
+
131
+ Currently, only two-column tables are supported. The data input should be a hash, like this:
132
+
133
+ ```ruby
134
+ # This will display a table of the 5 most recent users that have confirmed their email address.
135
+
136
+ table value: -> {
137
+ User.order(created_at: :desc).limit(5).pluck(:email, :confirmed_at).map do |email, confirmed_at|
138
+ {
139
+ email: email,
140
+ confirmed_at: ActionController::Base.helpers.time_ago_in_words(confirmed_at) + " ago"
141
+ }
142
+ end
143
+ }
144
+ ```
145
+
146
+ ### Summaries
147
+
148
+ ```ruby
149
+ summary data: -> { User }
150
+ ```
151
+
152
+ Summaries show quick statistics for the last 24 hours, 7 days, and 30 days. The periods can be customized:
153
+
154
+ ```ruby
155
+ summary data: -> { User }, periods: [
156
+ { name: '1h', duration: 1.hour },
157
+ { name: '12h', duration: 12.hours },
158
+ { name: '24h', duration: 24.hours }
159
+ ]
160
+ ```
161
+
162
+ ### Change Over Period
163
+
164
+ ```ruby
165
+ change_over_period -> { User }
166
+ change_over_period -> { Post }, period: 30.days, date_column: :published_at
167
+ ```
168
+
169
+ This component shows the percentage change over a specified period (default is 7 days). You can customize the period and the date column used for comparison.
170
+
171
+ ## Development
172
+
173
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
174
+
175
+ To install this gem onto your local machine, run `bundle exec rake install`.
176
+
177
+ ## Contributing
178
+
179
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rameerez/dashboards. Our code of conduct is: just be nice and make your mom proud of what you do and post online.
180
+
181
+ ## License
182
+
183
+ 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,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
@@ -0,0 +1 @@
1
+ //= link dashboards/application.js
@@ -0,0 +1,3 @@
1
+ // This file is automatically compiled by importmap-rails
2
+ // = require chartkick
3
+ // = require Chart.bundle
@@ -0,0 +1,5 @@
1
+ /* This is the main entry point for the Dashboards styles */
2
+ @import "layout";
3
+ @import "dashboard";
4
+ @import "box";
5
+ @import "components";
@@ -0,0 +1,19 @@
1
+ .box {
2
+ display: flex;
3
+ flex-direction: column;
4
+ justify-content: center;
5
+ text-align: left;
6
+ min-height: 100px;
7
+ background-color: rgba(255, 255, 255, 0.05);
8
+ border-radius: 8px;
9
+ padding: 1em;
10
+ }
11
+
12
+ .box > * {
13
+ margin: 0.25em 0;
14
+ }
15
+
16
+ .box-title {
17
+ font-size: 0.85em;
18
+ font-weight: lighter;
19
+ }
@@ -0,0 +1,37 @@
1
+ .metric-value {
2
+ font-size: 2em;
3
+ font-weight: bold;
4
+ }
5
+
6
+ .summary {
7
+ font-size: 0.65em;
8
+ margin: 0.5em 0 1.2em;
9
+ }
10
+
11
+ .change-over-period {
12
+ font-size: 0.65em;
13
+ margin: 1.2em 0;
14
+ display: flex;
15
+ justify-content: space-between;
16
+ align-items: center;
17
+ }
18
+
19
+ .change-over-period-pill {
20
+ padding: 0.25em 0.5em;
21
+ margin-right: 0.25em;
22
+ border-radius: 8px;
23
+ }
24
+
25
+ .table table {
26
+ width: 100%;
27
+ font-size: 0.65em;
28
+ }
29
+
30
+ .table th,
31
+ .table td {
32
+ border: none;
33
+ }
34
+
35
+ .table tr:nth-child(even) {
36
+ background-color: rgba(0, 0, 0, 0.05);
37
+ }
@@ -0,0 +1,41 @@
1
+ .dashboard {
2
+ display: grid;
3
+ grid-template-columns: repeat(4, 1fr);
4
+ gap: 0.5em;
5
+ }
6
+
7
+ .dashboard-nav ul {
8
+ list-style-type: none;
9
+ padding: 0;
10
+ display: flex;
11
+ justify-content: center;
12
+ }
13
+
14
+ .dashboard-nav li {
15
+ font-size: 0.9em;
16
+ }
17
+
18
+ .dashboard-nav li:not(:last-child)::after {
19
+ content: "·";
20
+ margin: 0 0.5em;
21
+ }
22
+
23
+ .dashboard-nav a {
24
+ text-decoration: none;
25
+ }
26
+
27
+ .dashboard-nav a:hover {
28
+ text-decoration: underline;
29
+ }
30
+
31
+ @media (max-width: 768px) {
32
+ .dashboard {
33
+ grid-template-columns: repeat(2, 1fr);
34
+ }
35
+ }
36
+
37
+ @media (max-width: 480px) {
38
+ .dashboard {
39
+ grid-template-columns: repeat(1, 1fr);
40
+ }
41
+ }
@@ -0,0 +1,12 @@
1
+ body {
2
+ /* Add any global styles here */
3
+ }
4
+
5
+ h1 {
6
+ /* Styles for h1 */
7
+ }
8
+
9
+ h3 {
10
+ font-size: 1em;
11
+ font-weight: bold;
12
+ }
@@ -0,0 +1,5 @@
1
+ module Dashboards
2
+ class BaseController < ApplicationController
3
+ layout 'dashboards/application'
4
+ end
5
+ end
@@ -0,0 +1,21 @@
1
+ module Dashboards
2
+ class DashboardsController < BaseController
3
+ # caches_action :show, expires_in: -> { Dashboards.configuration.cache_duration }
4
+
5
+ def index
6
+ if Dashboards.configuration.dashboards.empty?
7
+ render :no_dashboards
8
+ else
9
+ first_dashboard = Dashboards.configuration.dashboards.first
10
+ redirect_to dashboard_path(first_dashboard.slug)
11
+ end
12
+ end
13
+
14
+ def show
15
+ @dashboard = Dashboards.configuration.find_dashboard(params[:dashboard])
16
+ if @dashboard.nil?
17
+ redirect_to root_path, alert: "Dashboard not found"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ <h1>Welcome to 🍱 <b>dashboards</b></h1>
2
+
3
+ <p>It looks like you haven't defined any dashboards yet. Let's get started!</p>
4
+
5
+ <h2>How to Create Your First Dashboard</h2>
6
+
7
+ <ol>
8
+ <li>Create a file named <code>config/dashboards.rb</code> in your Rails application.</li>
9
+ <li>Open the file and define your first dashboard using the Dashboards DSL. Follow the instructions in the <a href="https://github.com/rameerez/dashboards#readme" target="_blank">🍱 dashboards README</a>. Here's an example:</li>
10
+ </ol>
11
+
12
+ <pre><code>
13
+ dashboard "My First Dashboard" do
14
+ box "User Statistics" do
15
+ metric "Total Users", value: -> { User.count }
16
+ chart "New Users", type: :line, data: -> { User.group_by_day(:created_at).count }
17
+ end
18
+
19
+ box "Post Statistics" do
20
+ metric "Total Posts", value: -> { Post.count }
21
+ chart "Posts by Category", type: :pie, data: -> { Post.group(:category).count }
22
+ end
23
+ end
24
+ </code></pre>
25
+
26
+ <p>After adding your dashboard definition, restart your Rails server to see your new dashboard in action!</p>
@@ -0,0 +1,22 @@
1
+ <h1><%= @dashboard.name %></h1>
2
+
3
+ <nav class="dashboard-nav">
4
+ <ul>
5
+ <% Dashboards.configuration.dashboards.each_with_index do |dashboard, index| %>
6
+ <li><%= (dashboard.slug == @dashboard.slug) ? "<u>#{dashboard.name}</u>".html_safe : link_to(dashboard.name, dashboard_path(dashboard.slug)) %></li>
7
+ <% end %>
8
+ </ul>
9
+ </nav>
10
+
11
+ <div class="dashboard">
12
+
13
+ <% @dashboard.boxes.each do |box| %>
14
+ <div class="box">
15
+ <h3 class="box-title"><%= box.name %></h3>
16
+ <% box.elements.each do |element| %>
17
+ <%= element.render(self).html_safe %>
18
+ <% end %>
19
+ </div>
20
+ <% end %>
21
+
22
+ </div>
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dashboards</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
9
+ <%= stylesheet_link_tag "dashboards/application", media: "all" %>
10
+
11
+ <%= javascript_importmap_tags %>
12
+ <%= javascript_include_tag "dashboards/application", "data-turbo-track": "reload", defer: true %>
13
+
14
+ <style>
15
+ body {
16
+ grid-template-columns: 1fr min(75rem,90%) 1fr;
17
+ }
18
+ </style>
19
+
20
+ </head>
21
+ <body>
22
+ <%= yield %>
23
+ </body>
24
+ </html>
@@ -0,0 +1,2 @@
1
+ pin "chartkick", to: "chartkick.js"
2
+ pin "Chart.bundle", to: "Chart.bundle.js"
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ Dashboards::Engine.routes.draw do
2
+ root 'dashboards#index'
3
+ get '/:dashboard', to: 'dashboards#show', as: :dashboard
4
+ end
@@ -0,0 +1,21 @@
1
+ module Dashboards
2
+ class Configuration
3
+ attr_reader :dashboards
4
+ attr_accessor :chart_library, :cache_duration #, :per_page
5
+
6
+ def initialize
7
+ @dashboards = []
8
+ @chart_library = :chartkick # Default to chartkick
9
+ @cache_duration = 5.minutes
10
+ # @per_page = 20
11
+ end
12
+
13
+ def add_dashboard(dashboard)
14
+ @dashboards << dashboard
15
+ end
16
+
17
+ def find_dashboard(slug)
18
+ @dashboards.find { |d| d.slug == slug }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,34 @@
1
+ module Dashboards
2
+ class Box
3
+ attr_reader :name, :elements
4
+
5
+ def initialize(name)
6
+ @name = name
7
+ @elements = []
8
+ end
9
+
10
+ def metric(name, options = {})
11
+ @elements << Metric.new(name, options)
12
+ end
13
+
14
+ def chart(name, options = {})
15
+ @elements << Chart.new(name, options)
16
+ end
17
+
18
+ def table(name, options = {})
19
+ @elements << Table.new(name, options)
20
+ end
21
+
22
+ def summary(data_or_options, options = {})
23
+ @elements << Summary.new(data_or_options, options)
24
+ end
25
+
26
+ def custom(&block)
27
+ @elements << CustomElement.new(block)
28
+ end
29
+
30
+ def change_over_period(data_or_options, options = {})
31
+ @elements << ChangeOverPeriod.new(data_or_options, options)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dashboards
4
+ class ChangeOverPeriod
5
+ attr_reader :data, :options
6
+
7
+ def initialize(data_or_options, options = {})
8
+ if data_or_options.is_a?(Hash)
9
+ @data = data_or_options[:data]
10
+ @options = data_or_options.except(:data)
11
+ else
12
+ @data = data_or_options
13
+ @options = options
14
+ end
15
+ @period = @options[:period] || 7.days
16
+ @date_column = @options[:date_column] || :created_at
17
+ end
18
+
19
+ def render(context)
20
+ data = @data.is_a?(Proc) ? context.instance_exec(&@data) : @data
21
+ render_change(data, context)
22
+ end
23
+
24
+ private
25
+
26
+ def render_change(data, context)
27
+ now = Time.current
28
+ current_count = data.where(@date_column => @period.ago(now)..now).count
29
+ previous_count = data.where(@date_column => @period.ago(@period.ago(now))..@period.ago(now)).count
30
+
31
+ if previous_count.zero?
32
+ percentage_change = current_count.positive? ? 100 : 0
33
+ else
34
+ percentage_change = ((current_count - previous_count).to_f / previous_count * 100).round
35
+ end
36
+
37
+ sign = percentage_change.positive? ? '+' : ''
38
+ chevron = percentage_change.positive? ? '▴' : '▾'
39
+ color = percentage_change.positive? ? 'green' : 'red'
40
+ period_text = case @period
41
+ when 7.days then 'last week'
42
+ when 30.days then '30d ago'
43
+ else "#{@period.inspect} ago"
44
+ end
45
+
46
+ html = "<div class='change-over-period'>"
47
+ html += "<span>Since #{period_text}</span>"
48
+ html += "<span class='change-over-period-pill' style='color: #{color};'>"
49
+ html += "#{sign}#{percentage_change}% #{chevron}"
50
+ html += "</span>"
51
+ html += "</div>"
52
+
53
+ html
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dashboards
4
+ class Chart
5
+ attr_reader :name, :type, :data, :options
6
+
7
+ VALID_TYPES = [:line, :pie, :column, :bar, :area, :scatter]
8
+
9
+ DEFAULT_HEIGHT = '120px'
10
+ DEFAULT_COLOR = '#FFFFFF' # White
11
+
12
+ def initialize(name_or_options, options = {})
13
+ if name_or_options.is_a?(Hash)
14
+ @name = nil
15
+ options = name_or_options
16
+ else
17
+ @name = name_or_options
18
+ end
19
+ @type = options.delete(:type) || :line
20
+ @data = options.delete(:data)
21
+ @options = options
22
+ @options[:height] ||= options[:height] || DEFAULT_HEIGHT
23
+ @options[:color] ||= DEFAULT_COLOR
24
+ end
25
+
26
+ def render(context)
27
+ data = @data.is_a?(Proc) ? context.instance_exec(&@data) : @data
28
+ chartkick_method = chartkick_method(@type)
29
+
30
+ options = @options.dup
31
+ options[:title] = @name if @name
32
+
33
+ # Apply color to different chart types
34
+ case @type
35
+ when :pie, :donut
36
+ options[:colors] ||= [@options[:color]]
37
+ when :column, :bar
38
+ options[:colors] ||= [@options[:color]]
39
+ else
40
+ options[:dataset] ||= {
41
+ borderColor: @options[:color],
42
+ backgroundColor: @options[:color],
43
+ pointBackgroundColor: @options[:color],
44
+ pointBorderColor: @options[:color],
45
+ pointHoverBackgroundColor: @options[:color],
46
+ pointHoverBorderColor: @options[:color],
47
+ hoverBackgroundColor: @options[:color],
48
+ hoverBorderColor: @options[:color]
49
+ }
50
+ end
51
+
52
+ # Apply height
53
+ options[:height] = @options[:height]
54
+
55
+ raise ArgumentError, "Invalid chart type: #{@type}" unless VALID_TYPES.include?(@type)
56
+
57
+ if context.respond_to?(chartkick_method)
58
+ context.public_send(chartkick_method, data, **options)
59
+ else
60
+ render_fallback(data)
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def chartkick_method(type)
67
+ case type
68
+ when :line then :line_chart
69
+ when :pie then :pie_chart
70
+ when :column then :column_chart
71
+ when :bar then :bar_chart
72
+ when :area then :area_chart
73
+ when :scatter then :scatter_chart
74
+ else
75
+ raise ArgumentError, "Unsupported chart type: #{type}"
76
+ end
77
+ end
78
+
79
+ def render_fallback(data)
80
+ "<div class='chart' style='height: #{@options[:height]}; color: #{@options[:color]};'>
81
+ <h3>#{@name}</h3>
82
+ <p>Chart data: #{data.inspect}</p>
83
+ <p>Note: Chartkick is not available. Please include it in your application for chart rendering.</p>
84
+ </div>"
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dashboards
4
+ class Dashboard
5
+ attr_reader :name, :boxes, :slug
6
+
7
+ def initialize(name, options = {})
8
+ @name = name
9
+ @slug = options[:slug] || name.parameterize
10
+ @boxes = []
11
+ end
12
+
13
+ def box(name, &block)
14
+ @boxes << Box.new(name).tap { |b| b.instance_eval(&block) }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dashboards
4
+ class Element
5
+ attr_reader :type, :name, :options
6
+
7
+ def initialize(type, name, options)
8
+ @type = type
9
+ @name = name
10
+ @options = options
11
+ end
12
+
13
+ def render(context)
14
+ case @type
15
+ when :metric
16
+ Metric.new(@name, @options).render(context)
17
+ when :chart
18
+ Chart.new(@name, @options).render(context)
19
+ when :table
20
+ Table.new(@name, @options).render(context)
21
+ else
22
+ raise Error, "Unsupported element type: #{@type}"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dashboards
4
+ class Metric
5
+ attr_reader :name, :value, :options
6
+
7
+ def initialize(name_or_options, options = {})
8
+ if name_or_options.is_a?(Hash)
9
+ @name = nil
10
+ options = name_or_options
11
+ else
12
+ @name = name_or_options
13
+ end
14
+ @value = options[:value]
15
+ @options = options.except(:value)
16
+ @format_big_numbers = options[:format_big_numbers] || false
17
+ @percentage = options[:percentage] || false
18
+ @currency = options[:currency]
19
+ end
20
+
21
+ def render(context)
22
+ value_content = @value.is_a?(Proc) ? context.instance_exec(&@value) : @value
23
+ formatted_value = value_content.nil? ? 'N/A' : format_number(value_content)
24
+ formatted_value = "#{@currency}#{formatted_value}" if @currency && !value_content.nil?
25
+ formatted_value += '%' if @percentage && !value_content.nil?
26
+ if @name
27
+ "<div class='metric'>
28
+ <h3>#{@name}</h3>
29
+ <div class='metric-value'>#{formatted_value}</div>
30
+ </div>"
31
+ else
32
+ "<div class='metric'>
33
+ <div class='metric-value'>#{formatted_value}</div>
34
+ </div>"
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def format_number(number)
41
+ return number unless number.is_a?(Numeric)
42
+
43
+ if @format_big_numbers
44
+ if number >= 1_000_000
45
+ "#{(number / 1_000_000.0).round(1)}M"
46
+ elsif number >= 1_000
47
+ "#{(number / 1_000.0).round(1)}K"
48
+ else
49
+ number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
50
+ end
51
+ else
52
+ number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dashboards
4
+ class Summary
5
+ attr_reader :data, :options
6
+
7
+ DEFAULT_PERIODS = [
8
+ { name: '24h', duration: 24.hours },
9
+ { name: '7d', duration: 7.days },
10
+ { name: '30d', duration: 30.days }
11
+ ]
12
+
13
+ def initialize(data_or_options, options = {})
14
+ if data_or_options.is_a?(Hash)
15
+ @data = data_or_options[:data]
16
+ @options = data_or_options.except(:data)
17
+ else
18
+ @data = data_or_options
19
+ @options = options
20
+ end
21
+ @periods = @options[:periods] || DEFAULT_PERIODS
22
+ end
23
+
24
+ def render(context)
25
+ data = @data.is_a?(Proc) ? context.instance_exec(&@data) : @data
26
+ render_summary(data, context)
27
+ end
28
+
29
+ private
30
+
31
+ def render_summary(data, context)
32
+ summary_data = calculate_summary(data)
33
+
34
+ html = "<div class='summary'><span>"
35
+
36
+ html += @periods.map.with_index do |period, index|
37
+ count = context.number_with_delimiter(summary_data[index], delimiter: ',')
38
+ "<b>last #{period[:name]}:</b> #{count}"
39
+ end.join(' · ')
40
+
41
+ html += "</span></div>"
42
+
43
+ html
44
+ end
45
+
46
+ def calculate_summary(data)
47
+ now = Time.now
48
+ @periods.map do |period|
49
+ data.where(created_at: period[:duration].ago..now).count
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dashboards
4
+ class Table
5
+ attr_reader :name, :data, :options
6
+
7
+ def initialize(name_or_options, options = {})
8
+ if name_or_options.is_a?(Hash)
9
+ @name = nil
10
+ options = name_or_options
11
+ else
12
+ @name = name_or_options
13
+ end
14
+ @data = options[:data] || options[:value]
15
+ @options = options.except(:data, :value)
16
+ end
17
+
18
+ def render(context)
19
+ data = @data.is_a?(Proc) ? context.instance_exec(&@data) : @data
20
+ render_table(data)
21
+ end
22
+
23
+ private
24
+
25
+ def render_table(data)
26
+ table_html = "<div class='table'>"
27
+ table_html += "<h3>#{@name}</h3>" if @name
28
+ table_html += "<table>"
29
+
30
+ if data.is_a?(Hash)
31
+ table_html += render_hash_data(data)
32
+ elsif data.respond_to?(:each)
33
+ table_html += render_array_data(data)
34
+ else
35
+ table_html += "<tr><td>No data available</td></tr>"
36
+ end
37
+
38
+ table_html += '</table></div>'
39
+ table_html
40
+ end
41
+
42
+ def render_hash_data(data)
43
+ html = ""
44
+ # html += "<tr><th>Key</th><th>Value</th></tr>"
45
+ data.each do |key, value|
46
+ html += "<tr><td>#{key}</td><td>#{value}</td></tr>"
47
+ end
48
+ html
49
+ end
50
+
51
+ def render_array_data(data)
52
+ html = ""
53
+ if data.first.is_a?(Hash)
54
+ keys = data.first.keys
55
+ html += "<tr>#{keys.map { |key| "<th>#{key}</th>" }.join}</tr>"
56
+ data.each do |item|
57
+ html += "<tr>#{keys.map { |key| "<td>#{item[key]}</td>" }.join}</tr>"
58
+ end
59
+ else
60
+ data.each do |item|
61
+ html += "<tr><td>#{item}</td></tr>"
62
+ end
63
+ end
64
+ html
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dsl/chart"
4
+ require_relative "dsl/dashboard"
5
+ require_relative "dsl/box"
6
+ require_relative "dsl/element"
7
+ require_relative "dsl/metric"
8
+ require_relative "dsl/table"
9
+ require_relative "dsl/summary"
10
+ require_relative "dsl/change_over_period"
11
+
12
+ module Dashboards
13
+ module DSL
14
+ def dashboard(name, &block)
15
+ dashboard = Dashboard.new(name)
16
+ dashboard.instance_eval(&block)
17
+ Dashboards.configuration.add_dashboard(dashboard)
18
+ end
19
+ end
20
+
21
+ class CustomElement
22
+ def initialize(block)
23
+ @block = block
24
+ end
25
+
26
+ def render(context)
27
+ context.instance_eval(&@block)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "loader"
4
+
5
+ module Dashboards
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace Dashboards
8
+
9
+ initializer "dashboards.load_configuration" do
10
+ config.after_initialize do
11
+ Dashboards::Loader.load
12
+ end
13
+ end
14
+
15
+ initializer "dashboards.importmap", before: "importmap" do |app|
16
+ app.config.importmap.paths << root.join("config/importmap.rb")
17
+ app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
18
+ end
19
+
20
+ initializer "dashboards.assets" do |app|
21
+ app.config.assets.precompile += %w[dashboards_manifest.js dashboards/application.js dashboards/application.css]
22
+ end
23
+
24
+ initializer "dashboards.append_assets_path" do |app|
25
+ app.config.assets.paths << root.join("app/assets/javascripts")
26
+ app.config.assets.paths << Chartkick::Engine.root.join("vendor/assets/javascripts")
27
+ end
28
+
29
+ initializer "dashboards.cache" do |app|
30
+ app.config.dashboards_cache_store = :memory_store
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dashboards
4
+ class Loader
5
+ def self.load
6
+ config_file = Rails.root.join("config", "dashboards.rb")
7
+ unless File.exist?(config_file)
8
+ Rails.logger.warn "Dashboards configuration file not found at #{config_file}"
9
+ return
10
+ end
11
+
12
+ begin
13
+ Class.new do
14
+ extend Dashboards::DSL
15
+ instance_eval(File.read(config_file))
16
+ end
17
+ rescue StandardError => e
18
+ Rails.logger.error "Error loading Dashboards configuration: #{e.message}"
19
+ raise Dashboards::ConfigurationError, "Failed to load Dashboards configuration: #{e.message}"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dashboards
4
+ VERSION = "0.1.0"
5
+ end
data/lib/dashboards.rb ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dashboards/version"
4
+ require_relative "dashboards/configuration"
5
+ require_relative "dashboards/dsl"
6
+ require_relative "dashboards/engine" if defined?(Rails)
7
+
8
+ # Require all the dependencies
9
+ require "importmap-rails"
10
+ require "chartkick"
11
+ require "groupdate"
12
+
13
+ module Dashboards
14
+ class Error < StandardError; end
15
+
16
+ class << self
17
+ attr_writer :configuration
18
+
19
+ def configuration
20
+ @configuration ||= Configuration.new
21
+ end
22
+
23
+ def configure
24
+ yield(configuration)
25
+ end
26
+
27
+ def reset_configuration!
28
+ @configuration = Configuration.new
29
+ end
30
+ end
31
+ end
32
+
33
+ # Set default configuration
34
+ Dashboards.configure do |config|
35
+ config.chart_library = :chartkick
36
+ end
@@ -0,0 +1,4 @@
1
+ module Dashboards
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,178 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dashboards
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - rameerez
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-09-28 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: '7.0'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 7.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '7.0'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 7.0.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: importmap-rails
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 2.0.0
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '2.0'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 2.0.0
53
+ - !ruby/object:Gem::Dependency
54
+ name: chartkick
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '5.0'
60
+ type: :runtime
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '5.0'
67
+ - !ruby/object:Gem::Dependency
68
+ name: groupdate
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '6.1'
74
+ type: :runtime
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '6.1'
81
+ - !ruby/object:Gem::Dependency
82
+ name: rspec
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '3.12'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '3.12'
95
+ - !ruby/object:Gem::Dependency
96
+ name: rubocop
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '1.50'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '1.50'
109
+ description: Create beautiful, customizable bento-style admin dashboards in your Rails
110
+ application with a very simple DSL.
111
+ email:
112
+ - rubygems@rameerez.com
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - CHANGELOG.md
118
+ - LICENSE.txt
119
+ - README.md
120
+ - Rakefile
121
+ - app/assets/config/dashboards_manifest.js
122
+ - app/assets/javascript/dashboards/application.js
123
+ - app/assets/stylesheets/dashboards/application.css
124
+ - app/assets/stylesheets/dashboards/box.css
125
+ - app/assets/stylesheets/dashboards/components.css
126
+ - app/assets/stylesheets/dashboards/dashboard.css
127
+ - app/assets/stylesheets/dashboards/layout.css
128
+ - app/controllers/dashboards/base_controller.rb
129
+ - app/controllers/dashboards/dashboards_controller.rb
130
+ - app/views/dashboards/dashboards/no_dashboards.html.erb
131
+ - app/views/dashboards/dashboards/show.html.erb
132
+ - app/views/layouts/dashboards/application.html.erb
133
+ - config/importmap.rb
134
+ - config/routes.rb
135
+ - lib/dashboards.rb
136
+ - lib/dashboards/configuration.rb
137
+ - lib/dashboards/dsl.rb
138
+ - lib/dashboards/dsl/box.rb
139
+ - lib/dashboards/dsl/change_over_period.rb
140
+ - lib/dashboards/dsl/chart.rb
141
+ - lib/dashboards/dsl/dashboard.rb
142
+ - lib/dashboards/dsl/element.rb
143
+ - lib/dashboards/dsl/metric.rb
144
+ - lib/dashboards/dsl/summary.rb
145
+ - lib/dashboards/dsl/table.rb
146
+ - lib/dashboards/engine.rb
147
+ - lib/dashboards/loader.rb
148
+ - lib/dashboards/version.rb
149
+ - sig/dashboards.rbs
150
+ homepage: https://github.com/rameerez/dashboards
151
+ licenses:
152
+ - MIT
153
+ metadata:
154
+ allowed_push_host: https://rubygems.org
155
+ homepage_uri: https://github.com/rameerez/dashboards
156
+ source_code_uri: https://github.com/rameerez/dashboards
157
+ changelog_uri: https://github.com/rameerez/dashboards/blob/main/CHANGELOG.md
158
+ post_install_message:
159
+ rdoc_options: []
160
+ require_paths:
161
+ - lib
162
+ required_ruby_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: 3.0.0
167
+ required_rubygems_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ requirements: []
173
+ rubygems_version: 3.5.17
174
+ signing_key:
175
+ specification_version: 4
176
+ summary: A simple and powerful DSL for creating beautiful, customizable bento-style
177
+ admin dashboards in Rails applications.
178
+ test_files: []