dashboards 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []