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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/Rakefile +4 -0
- data/app/assets/config/dashboards_manifest.js +1 -0
- data/app/assets/javascript/dashboards/application.js +3 -0
- data/app/assets/stylesheets/dashboards/application.css +5 -0
- data/app/assets/stylesheets/dashboards/box.css +19 -0
- data/app/assets/stylesheets/dashboards/components.css +37 -0
- data/app/assets/stylesheets/dashboards/dashboard.css +41 -0
- data/app/assets/stylesheets/dashboards/layout.css +12 -0
- data/app/controllers/dashboards/base_controller.rb +5 -0
- data/app/controllers/dashboards/dashboards_controller.rb +21 -0
- data/app/views/dashboards/dashboards/no_dashboards.html.erb +26 -0
- data/app/views/dashboards/dashboards/show.html.erb +22 -0
- data/app/views/layouts/dashboards/application.html.erb +24 -0
- data/config/importmap.rb +2 -0
- data/config/routes.rb +4 -0
- data/lib/dashboards/configuration.rb +21 -0
- data/lib/dashboards/dsl/box.rb +34 -0
- data/lib/dashboards/dsl/change_over_period.rb +56 -0
- data/lib/dashboards/dsl/chart.rb +87 -0
- data/lib/dashboards/dsl/dashboard.rb +17 -0
- data/lib/dashboards/dsl/element.rb +26 -0
- data/lib/dashboards/dsl/metric.rb +56 -0
- data/lib/dashboards/dsl/summary.rb +53 -0
- data/lib/dashboards/dsl/table.rb +67 -0
- data/lib/dashboards/dsl.rb +30 -0
- data/lib/dashboards/engine.rb +33 -0
- data/lib/dashboards/loader.rb +23 -0
- data/lib/dashboards/version.rb +5 -0
- data/lib/dashboards.rb +36 -0
- data/sig/dashboards.rbs +4 -0
- 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
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
|
+

|
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 @@
|
|
1
|
+
//= link dashboards/application.js
|
@@ -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,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>
|
data/config/importmap.rb
ADDED
data/config/routes.rb
ADDED
@@ -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
|
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
|
data/sig/dashboards.rbs
ADDED
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: []
|