caffeinate_webui 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +53 -0
- data/Rakefile +20 -0
- data/app/controllers/caffeinate/webui/application_controller.rb +20 -0
- data/app/controllers/caffeinate/webui/campaigns_controller.rb +16 -0
- data/app/controllers/caffeinate/webui/dashboard_controller.rb +24 -0
- data/app/controllers/caffeinate/webui/mailings_controller.rb +38 -0
- data/app/controllers/caffeinate/webui/subscriptions/unsubscribes_controller.rb +15 -0
- data/app/controllers/caffeinate/webui/subscriptions_controller.rb +22 -0
- data/app/helpers/caffeinate/webui/application_helper.rb +16 -0
- data/app/views/caffeinate/webui/campaigns/index.html.erb +19 -0
- data/app/views/caffeinate/webui/campaigns/show.html.erb +56 -0
- data/app/views/caffeinate/webui/dashboard/show.html.erb +64 -0
- data/app/views/caffeinate/webui/layouts/application.html.erb +170 -0
- data/app/views/caffeinate/webui/mailings/index.html.erb +58 -0
- data/app/views/caffeinate/webui/mailings/show.html.erb +1 -0
- data/app/views/caffeinate/webui/subscriptions/index.html.erb +34 -0
- data/app/views/caffeinate/webui/subscriptions/show.html.erb +50 -0
- data/config/routes.rb +11 -0
- data/lib/caffeinate/webui/engine.rb +13 -0
- data/lib/caffeinate/webui/name.rb +21 -0
- data/lib/caffeinate/webui/version.rb +5 -0
- data/lib/caffeinate/webui.rb +15 -0
- data/lib/caffeinate_webui.rb +1 -0
- metadata +222 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8b252d1fca4c8a0cdc033379fe1ce1a40825be92ad378c102e13a225b977b69f
|
4
|
+
data.tar.gz: 347c84a885db57a12d2aa3d95e259342c4c67ced5f47a71d9062dcc7ff78e27b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 978ce8a1f526486b13e894428c9afb5489b912b733fcf232474a399fdeca0377e22cd3f2c3096d2ae5dab44f15fa641a016f45b959d2956451d05ab60b7d3e03
|
7
|
+
data.tar.gz: c7d521079ffb5dd87ee87e26315ecdb566bb939a0dc11848aa034d5cafd12901f2726eb78e9550b8c50a1fb44fc0a992e8bef47059167182486438fd965bc7c0
|
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# Caffeinate WebUI
|
2
|
+
|
3
|
+
Provides a simple UI to view and manage some aspects of Caffeinate.
|
4
|
+
|
5
|
+
<div align="center">
|
6
|
+
<img width="450" src="https://github.com/joshmn/caffeinate/raw/master/logo.png" alt="Caffeinate logo" />
|
7
|
+
</div>
|
8
|
+
|
9
|
+
<div align="center">
|
10
|
+
<img width="100%" src="https://github.com/joshmn/caffeinate-webui/raw/master/dashboard.png" alt="Caffeinate WebUI Example" />
|
11
|
+
</div>
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'caffeinate_webui'
|
19
|
+
```
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
$ bundle install
|
24
|
+
|
25
|
+
Drop it into your routes:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
mount Caffeinate::Webui => '/admin/caffeinate'
|
29
|
+
```
|
30
|
+
|
31
|
+
## Protect it
|
32
|
+
|
33
|
+
If you're using Devise, you can simply:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
authenticate :user, ->(user) { user.admin? } do
|
37
|
+
mount Caffeinate::Webui => '/admin/caffeinate'
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
Otherwise, protect it with your preferred rack-based strategy.
|
42
|
+
|
43
|
+
## Features
|
44
|
+
|
45
|
+
* Some lightweight dashboard stuff
|
46
|
+
* View campaigns and their steps
|
47
|
+
* View subscriptions
|
48
|
+
* Unsubscribe a subscription
|
49
|
+
* View mailings
|
50
|
+
|
51
|
+
## Dependencies
|
52
|
+
|
53
|
+
Doesn't need Sprockets, so I guess that's nice.
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Caffeinate WebUI'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
load 'rails/tasks/statistics.rake'
|
20
|
+
require 'bundler/gem_tasks'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Caffeinate
|
2
|
+
module Webui
|
3
|
+
class ApplicationController < ActionController::Base
|
4
|
+
layout 'caffeinate/webui/layouts/application'
|
5
|
+
|
6
|
+
helper_method :page_title
|
7
|
+
def page_title
|
8
|
+
if @page_title
|
9
|
+
"#{@page_title} - Caffeinate"
|
10
|
+
else
|
11
|
+
"Caffeinate"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_page_title(val)
|
16
|
+
@page_title = val
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Caffeinate
|
2
|
+
module Webui
|
3
|
+
class CampaignsController < ApplicationController
|
4
|
+
def index
|
5
|
+
@campaigns = Caffeinate::Campaign.all
|
6
|
+
set_page_title "Campaigns"
|
7
|
+
end
|
8
|
+
|
9
|
+
def show
|
10
|
+
@campaign = Caffeinate::Campaign.find_by(id: params[:id])
|
11
|
+
@subscriptions = @campaign.caffeinate_campaign_subscriptions.preload(:subscriber).paginate(per_page: 30, page: params[:page])
|
12
|
+
set_page_title "Viewing #{@campaign.name}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Caffeinate
|
2
|
+
module Webui
|
3
|
+
class DashboardController < ApplicationController
|
4
|
+
def show
|
5
|
+
@stats ||= OpenStruct.new(
|
6
|
+
subscribers: ::Caffeinate::CampaignSubscription.count,
|
7
|
+
delivered: ::Caffeinate::Mailing.sent.count,
|
8
|
+
skipped: ::Caffeinate::Mailing.skipped.count,
|
9
|
+
active: ::Caffeinate::Campaign.active.count
|
10
|
+
)
|
11
|
+
|
12
|
+
@all_stats = Caffeinate::Mailing.all.sent.group_by_day(:send_at).count
|
13
|
+
@upcoming_mailings ||= ::Caffeinate::Mailing.unsent
|
14
|
+
.joins(:caffeinate_campaign_subscription)
|
15
|
+
.preload(:caffeinate_campaign, caffeinate_campaign_subscription: [:subscriber])
|
16
|
+
.merge(::Caffeinate::CampaignSubscription.active)
|
17
|
+
.order(send_at: :asc)
|
18
|
+
.paginate(per_page: 30, page: params[:page])
|
19
|
+
|
20
|
+
set_page_title "Dashboard"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Caffeinate
|
2
|
+
module Webui
|
3
|
+
class MailingsController < ApplicationController
|
4
|
+
before_action :set_status, only: [:index]
|
5
|
+
|
6
|
+
def index
|
7
|
+
@campaigns = ::Caffeinate::Campaign.all
|
8
|
+
@mailings = ::Caffeinate::Mailing.preload(:caffeinate_campaign, caffeinate_campaign_subscription: [:subscriber])
|
9
|
+
if params[:campaign_id]
|
10
|
+
@campaign = ::Caffeinate::Campaign.find_by(id: params[:campaign_id])
|
11
|
+
end
|
12
|
+
if @campaign
|
13
|
+
@mailings = @mailings.joins(:caffeinate_campaign).where(caffeinate_campaign: { id: @campaign.id })
|
14
|
+
end
|
15
|
+
if @status
|
16
|
+
@mailings = @mailings.public_send(@status)
|
17
|
+
end
|
18
|
+
@mailings = @mailings.paginate(per_page: 30, page: params[:page])
|
19
|
+
set_page_title "Mailings"
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
def show
|
24
|
+
@mailing = ::Caffeinate::Mailing.find_by(id: params[:id])
|
25
|
+
set_page_title "Mailing Details"
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def set_status
|
32
|
+
if ['sent', 'unsent', 'skipped'].include?(params[:status])
|
33
|
+
@status = params[:status]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Caffeinate::Webui::Subscriptions::UnsubscribesController < ApplicationController
|
2
|
+
def create
|
3
|
+
@subscription = ::Caffeinate::CampaignSubscription.find_by(id: params[:subscription_id])
|
4
|
+
if @subscription
|
5
|
+
begin
|
6
|
+
@subscription.unsubscribe!
|
7
|
+
flash[:notice] = "Unsubscribed."
|
8
|
+
rescue Caffeinate::InvalidState => e
|
9
|
+
flash[:notice] = e.message
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
redirect_to subscription_path(@subscription)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Caffeinate
|
2
|
+
module Webui
|
3
|
+
class SubscriptionsController < ApplicationController
|
4
|
+
def index
|
5
|
+
@subscriptions = Caffeinate::CampaignSubscription.preload(:caffeinate_campaign, :subscriber)
|
6
|
+
if params[:campaign_id]
|
7
|
+
@campaign = ::Caffeinate::Campaign.find_by(id: params[:campaign_id])
|
8
|
+
end
|
9
|
+
if @campaign
|
10
|
+
@subscriptions = @subscriptions.where(caffeinate_campaign: { id: @campaign.id })
|
11
|
+
end
|
12
|
+
@subscriptions = @subscriptions.order(created_at: :desc).paginate(page: params[:page], per_page: 30)
|
13
|
+
set_page_title "Subscriptions"
|
14
|
+
end
|
15
|
+
|
16
|
+
def show
|
17
|
+
@subscription = Caffeinate::CampaignSubscription.find(params[:id])
|
18
|
+
set_page_title "Viewing Subscription"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Caffeinate::Webui::ApplicationHelper
|
2
|
+
def active_link_to(label, href, active: false)
|
3
|
+
link_to(label, href, class: "nav-link #{'active' if active}")
|
4
|
+
end
|
5
|
+
|
6
|
+
def time(datetime)
|
7
|
+
label = [time_ago_in_words(datetime)]
|
8
|
+
if datetime.past?
|
9
|
+
label << "ago"
|
10
|
+
else
|
11
|
+
label << "from now"
|
12
|
+
end
|
13
|
+
|
14
|
+
content_tag(:abbr, label.join(" "), title: datetime)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<h2>Campaigns</h2>
|
2
|
+
<ul>
|
3
|
+
|
4
|
+
</ul>
|
5
|
+
|
6
|
+
<div class="my-3 p-3 bg-body rounded shadow-sm">
|
7
|
+
<h6 class="border-bottom pb-2 mb-0">Campaigns</h6>
|
8
|
+
<% @campaigns.each do |campaign| %>
|
9
|
+
<div class="d-flex w-100 text-muted pt-3 flex-grow-1 justify-content-between border-bottom">
|
10
|
+
<div class="pb-3 mb-0 ">
|
11
|
+
<strong class="d-block text-gray-dark"><%= link_to campaign.name, campaign_path(campaign) %></strong>
|
12
|
+
</div>
|
13
|
+
<div>
|
14
|
+
<%= pluralize(campaign.subscriptions.active.count, "subscription") %>
|
15
|
+
</div>
|
16
|
+
</div>
|
17
|
+
<% end %>
|
18
|
+
|
19
|
+
</div>
|
@@ -0,0 +1,56 @@
|
|
1
|
+
<div class="my-3 p-3 bg-body rounded shadow-sm">
|
2
|
+
<h6 class="border-bottom pb-2 mb-0"><%= @campaign.name %></h6>
|
3
|
+
<table class="table">
|
4
|
+
<thead>
|
5
|
+
<tr>
|
6
|
+
<td>Mail</td>
|
7
|
+
<td>Delay</td>
|
8
|
+
</tr>
|
9
|
+
</thead>
|
10
|
+
<tbody>
|
11
|
+
<% @campaign.to_dripper.drips.each do |step| %>
|
12
|
+
<tr>
|
13
|
+
<td>
|
14
|
+
<%= step.options[:mailer_class] %>#<%= step.action %>
|
15
|
+
</td>
|
16
|
+
<td>
|
17
|
+
<%= step.options[:delay].inspect %>
|
18
|
+
</td>
|
19
|
+
</tr>
|
20
|
+
<% end %>
|
21
|
+
</tbody>
|
22
|
+
</table>
|
23
|
+
</div>
|
24
|
+
|
25
|
+
<div class="my-3 p-3 bg-body rounded shadow-sm">
|
26
|
+
<h6 class="border-bottom pb-2 mb-0">Subscriptions</h6>
|
27
|
+
<table class="table">
|
28
|
+
<thead>
|
29
|
+
<tr>
|
30
|
+
<td>Who</td>
|
31
|
+
<td>Created</td>
|
32
|
+
<td>Status</td>
|
33
|
+
</tr>
|
34
|
+
</thead>
|
35
|
+
<tbody>
|
36
|
+
<% @subscriptions.each do |subscriber| %>
|
37
|
+
<tr>
|
38
|
+
<td>
|
39
|
+
<%= link_to ::Caffeinate::Webui::Name.for(subscriber.subscriber), subscription_path(subscriber) %>
|
40
|
+
</td>
|
41
|
+
<td>
|
42
|
+
<%= time_ago_in_words(subscriber.created_at) %> ago
|
43
|
+
</td>
|
44
|
+
<td>
|
45
|
+
<%= "Ended" if subscriber.ended? %>
|
46
|
+
<%= "Active" if subscriber.subscribed? %>
|
47
|
+
<%= "Unsubscribed" if subscriber.unsubscribed? %>
|
48
|
+
</td>
|
49
|
+
</tr>
|
50
|
+
<% end %>
|
51
|
+
</tbody>
|
52
|
+
</table>
|
53
|
+
<div class="d-flex justify-content-end">
|
54
|
+
<%= will_paginate @subscriptions %>
|
55
|
+
</div>
|
56
|
+
</div>
|
@@ -0,0 +1,64 @@
|
|
1
|
+
<div class="row align-items-center text-center">
|
2
|
+
<div class="col">
|
3
|
+
<h5>Subscribers</h5>
|
4
|
+
<span><%= @stats.subscribers %></span>
|
5
|
+
</div>
|
6
|
+
<div class="col">
|
7
|
+
<h5>Delivered</h5>
|
8
|
+
<span><%= @stats.delivered %></span>
|
9
|
+
</div>
|
10
|
+
<div class="col">
|
11
|
+
<h5>Skipped</h5>
|
12
|
+
<span><%= @stats.skipped %></span>
|
13
|
+
</div>
|
14
|
+
<div class="col">
|
15
|
+
<h5>Active</h5>
|
16
|
+
<span><%= @stats.active %></span>
|
17
|
+
</div>
|
18
|
+
</div>
|
19
|
+
|
20
|
+
<div class="my-3 p-3 bg-body rounded shadow-sm">
|
21
|
+
<div class="d-flex justify-content-between border-bottom pb-2 mb-0">
|
22
|
+
<h6 class="">History</h6>
|
23
|
+
<div class="d-flex gap-3">
|
24
|
+
<%= link_to "One week", root_path(duration: :week) %>
|
25
|
+
<%= link_to "One month", root_path(duration: :month) %>
|
26
|
+
<%= link_to "One year", root_path(duration: :year) %>
|
27
|
+
<%= link_to "All-time", root_path %>
|
28
|
+
</div>
|
29
|
+
</div>
|
30
|
+
|
31
|
+
<div class="d-flex text-muted pt-3">
|
32
|
+
<%= line_chart(@all_stats) %>
|
33
|
+
</div>
|
34
|
+
</div>
|
35
|
+
|
36
|
+
<div class="my-3 p-3 bg-body rounded shadow-sm">
|
37
|
+
<h6 class="border-bottom pb-2 mb-0">Upcoming</h6>
|
38
|
+
<div class="d-flex text-muted pt-3">
|
39
|
+
|
40
|
+
<table class="table">
|
41
|
+
<thead>
|
42
|
+
<tr>
|
43
|
+
<td>Who</td>
|
44
|
+
<td>Campaign</td>
|
45
|
+
<td>Send at</td>
|
46
|
+
</tr>
|
47
|
+
</thead>
|
48
|
+
<tbody>
|
49
|
+
<% @upcoming_mailings.each do |mailing| %>
|
50
|
+
<tr>
|
51
|
+
<td><%= link_to Caffeinate::Webui::Name.for(mailing.subscriber), subscription_path(mailing.caffeinate_campaign_subscription) %></td>
|
52
|
+
<td><%= link_to mailing.campaign.name, mailing.campaign %></td>
|
53
|
+
<td><%= time(mailing.send_at) %></td>
|
54
|
+
</tr>
|
55
|
+
<% end %>
|
56
|
+
</tbody>
|
57
|
+
</table>
|
58
|
+
</div>
|
59
|
+
|
60
|
+
<div class="d-flex justify-content-end">
|
61
|
+
<%= will_paginate @upcoming_mailings %>
|
62
|
+
</div>
|
63
|
+
</div>
|
64
|
+
|
@@ -0,0 +1,170 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
6
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
|
7
|
+
<script src="https://code.highcharts.com/highcharts.js"></script>
|
8
|
+
<script src="https://unpkg.com/chartjs-adapter-date-fns@2.0.0/dist/chartjs-adapter-date-fns.bundle.js"></script>
|
9
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
|
11
|
+
<title><%= page_title %></title>
|
12
|
+
<script type="text/javascript">
|
13
|
+
/*!
|
14
|
+
* Chartkick.js
|
15
|
+
* Create beautiful charts with one line of JavaScript
|
16
|
+
* https://github.com/ankane/chartkick.js
|
17
|
+
* v4.2.0
|
18
|
+
* MIT License
|
19
|
+
*/ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chartkick=e()}(this,function(){"use strict";function t(t){return"[object Array]"===Object.prototype.toString.call(t)}function e(t){return t instanceof Function}function r(t){return"[object Object]"===Object.prototype.toString.call(t)&&!e(t)&&t instanceof Object}function o(e,n){var a;for(a in n)"__proto__"!==a&&(r(n[a])||t(n[a])?(r(n[a])&&!r(e[a])&&(e[a]={}),t(n[a])&&!t(e[a])&&(e[a]=[]),o(e[a],n[a])):void 0!==n[a]&&(e[a]=n[a]))}function n(t,e){var r={};return o(r,t),o(r,e),r}var a=/^(\d\d\d\d)(-)?(\d\d)(-)?(\d\d)$/i;function i(t){return""+t}function s(t){return parseFloat(t)}function l(t){var e,r,o,n;if("object"!=typeof t){if("number"==typeof t)t=new Date(1e3*t);else{if(e=(t=""+(i=t)).match(a))return r=parseInt(e[1],10),o=parseInt(e[3],10)-1,n=parseInt(e[5],10),new Date(r,o,n);var i,s=t.replace(/ /,"T").replace(" ","").replace("UTC","Z");t=new Date(Date.parse(s)||t)}}return t}function c(e){if(!t(e)){var r,o=[];for(r in e)e.hasOwnProperty(r)&&o.push([r,e[r]]);e=o}return e}function p(t,e,r,o,a,i,s,l){return function(c,p,u){var d=c.data,h=n({},t);return h=n(h,u||{}),(c.singleSeriesFormat||"legend"in p)&&e(h,p.legend,c.singleSeriesFormat),p.title&&r(h,p.title),"min"in p?o(h,p.min):!function t(e){var r,o,n;for(r=0;r<e.length;r++)for(o=0,n=e[r].data;o<n.length;o++)if(n[o][1]<0)return!0;return!1}(d)&&o(h,0),p.max&&a(h,p.max),"stacked"in p&&i(h,p.stacked),p.colors&&(h.colors=p.colors),p.xtitle&&s(h,p.xtitle),p.ytitle&&l(h,p.ytitle),h=n(h,p.library||{})}}function u(t,e){return t[0].getTime()-e[0].getTime()}function d(t,e){return t[0]-e[0]}function h(t,e){return t-e}function f(t){return 0===t.getMilliseconds()&&0===t.getSeconds()}function y(t){return f(t)&&0===t.getMinutes()}function m(t){return y(t)&&0===t.getHours()}function $(t,e){return m(t)&&t.getDay()===e}function g(t){return m(t)&&1===t.getDate()}function v(t){return g(t)&&0===t.getMonth()}function z(t){var e;return!isNaN(l(t))&&(""+(e=t)).length>=6}function b(t){return"number"==typeof t}var M=["bytes","KB","MB","GB","TB","PB","EB"];function x(t,e,r,o){t=t||"",r.prefix&&(e<0&&(e*=-1,t+="-"),t+=r.prefix);var n=r.suffix||"",a=r.precision,i=r.round;if(r.byteScale){var s,l=o?r.byteScale:e;l>=0x1000000000000000?(e/=0x1000000000000000,s=6):l>=0x4000000000000?(e/=0x4000000000000,s=5):l>=1099511627776?(e/=1099511627776,s=4):l>=1073741824?(e/=1073741824,s=3):l>=1048576?(e/=1048576,s=2):l>=1024?(e/=1024,s=1):s=0,void 0===a&&void 0===i&&(e>=1023.5&&s<M.length-1&&(e=1,s+=1),a=e>=1e3?4:3),n=" "+M[s]}if(void 0!==a&&void 0!==i)throw Error("Use either round or precision, not both");if(!o&&(void 0===a||(e=e.toPrecision(a),r.zeros||(e=parseFloat(e))),void 0!==i)){if(i<0){var c=Math.pow(10,-1*i);e=parseInt((1*e/c).toFixed(0))*c}else e=e.toFixed(i),r.zeros||(e=parseFloat(e))}if(r.thousands||r.decimal){var p,u=(e=""+(p=e)).split(".");e=u[0],r.thousands&&(e=e.replace(/\B(?=(\d{3})+(?!\d))/g,r.thousands)),u.length>1&&(e+=(r.decimal||".")+u[1])}return t+e+n}function _(t,e,r){return r in e?e[r]:r in t.options?t.options[r]:null}var C={maintainAspectRatio:!1,animation:!1,plugins:{legend:{},tooltip:{displayColors:!1,callbacks:{}},title:{font:{size:20},color:"#333"}},interaction:{}},w={scales:{y:{ticks:{maxTicksLimit:4},title:{font:{size:16},color:"#333"},grid:{}},x:{grid:{drawOnChartArea:!1},title:{font:{size:16},color:"#333"},time:{},ticks:{}}}},A=["#3366CC","#DC3912","#FF9900","#109618","#990099","#3B3EAC","#0099C6","#DD4477","#66AA00","#B82E2E","#316395","#994499","#22AA99","#AAAA11","#6633CC","#E67300","#8B0707","#329262","#5574A6","#651067"],k=function(t,e,r){void 0!==e?(t.plugins.legend.display=!!e,e&&!0!==e&&(t.plugins.legend.position=e)):r&&(t.plugins.legend.display=!1)},S=function(t,e){t.plugins.title.display=!0,t.plugins.title.text=e},T=function(t,e){null!==e&&(t.scales.y.min=s(e))},D=function(t,e){t.scales.y.max=s(e)},E=function(t,e){null!==e&&(t.scales.x.min=s(e))},L=function(t,e){t.scales.x.max=s(e)},B=function(t,e){t.scales.x.stacked=!!e,t.scales.y.stacked=!!e},O=function(t,e){t.scales.x.title.display=!0,t.scales.x.title.text=e},F=function(t,e){t.scales.y.title.display=!0,t.scales.y.title.text=e},H=function(t,e){var r=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(t);return r?"rgba("+parseInt(r[1],16)+", "+parseInt(r[2],16)+", "+parseInt(r[3],16)+", "+e+")":t},R=function(t){return null!=t},N=function(t,e,r){var o=Math.ceil(t.element.offsetWidth/4/e.labels.length);o>25?o=25:o<10&&(o=10),r.scales.x.ticks.callback||(r.scales.x.ticks.callback=function(t){var e;return(t=""+(e=this.getLabelForValue(t))).length>o?t.substring(0,o-2)+"...":t})},j=function(e,r,o){var n={prefix:e.options.prefix,suffix:e.options.suffix,thousands:e.options.thousands,decimal:e.options.decimal,precision:e.options.precision,round:e.options.round,zeros:e.options.zeros};if(e.options.bytes){var a=e.data;"pie"===o&&(a=[{data:a}]);for(var i=0,s=0;s<a.length;s++)for(var l=a[s],c=0;c<l.data.length;c++)l.data[c][1]>i&&(i=l.data[c][1]);for(var p=1;i>=1024;)p*=1024,i/=1024;n.byteScale=p}if("pie"!==o){var u=r.scales.y;"bar"===o&&(u=r.scales.x),n.byteScale&&(u.ticks.stepSize||(u.ticks.stepSize=n.byteScale/2),u.ticks.maxTicksLimit||(u.ticks.maxTicksLimit=4)),u.ticks.callback||(u.ticks.callback=function(t){return x("",t,n,!0)})}if(!r.plugins.tooltip.callbacks.label){if("scatter"===o)r.plugins.tooltip.callbacks.label=function(t){var e=t.dataset.label||"";return e&&(e+=": "),e+"("+t.label+", "+t.formattedValue+")"};else if("bubble"===o)r.plugins.tooltip.callbacks.label=function(t){var e=t.dataset.label||"";e&&(e+=": ");var r=t.raw;return e+"("+r.x+", "+r.y+", "+r.v+")"};else if("pie"===o)r.plugins.tooltip.callbacks.label=function(e){var r=e.label;return t(r)?(r=r.slice(),r[0]+=": "):r+=": ",x(r,e.parsed,n)};else{var d="bar"===o?"x":"y";r.plugins.tooltip.callbacks.label=function(t){if(null!==t.parsed[d]){var e=t.dataset.label||"";return e&&(e+=": "),x(e,t.parsed[d],n)}}}}},P=p(n(C,w),k,S,T,D,B,O,F),U=function(e,r,o){var a=[],i=[],c=e.options.colors||A,p=!0,u=!0,d=!0,z=!0,b=!0,M=!0,x=e.data,C=0;if("bubble"===o)for(var w=0;w<x.length;w++)for(var k=x[w],S=0;S<k.data.length;S++)k.data[S][2]>C&&(C=k.data[S][2]);var T,D,E,L,B,O,F=[],N=[];if("bar"===o||"column"===o||"number"!==e.xtype&&"bubble"!==e.xtype){var j,P,U=[];for(D=0;D<x.length;D++)for(E=0,L=x[D];E<L.data.length;E++)B=L.data[E],F[O="datetime"==e.xtype?B[0].getTime():B[0]]||(F[O]=Array(x.length)),F[O][D]=s(B[1]),-1===U.indexOf(O)&&U.push(O);for(("datetime"===e.xtype||"number"===e.xtype)&&U.sort(h),E=0;E<x.length;E++)N.push([]);for(P=0;P<U.length;P++)for(D=U[P],"datetime"===e.xtype?(j=new Date(s(D)),p=p&&m(j),T||(T=j.getDay()),u=u&&$(j,T),d=d&&g(j),z=z&&v(j),b=b&&y(j),M=M&&f(j)):j=D,i.push(j),E=0;E<x.length;E++)N[E].push(void 0===F[D][E]?null:F[D][E])}else for(var W=0;W<x.length;W++){for(var V=x[W],Q=[],I=0;I<V.data.length;I++){var G={x:s(V.data[I][0]),y:s(V.data[I][1])};"bubble"===o&&(G.r=20*s(V.data[I][2])/C,G.v=V.data[I][2]),Q.push(G)}N.push(Q)}for(D=0;D<x.length;D++){if(L=x[D],e.options.colors&&e.singleSeriesFormat&&("bar"===o||"column"===o)&&!L.color&&t(e.options.colors)&&!t(e.options.colors[0])){q=c,tt=[];for(var J=0;J<c.length;J++)tt[J]=H(q[J],.5)}else q=L.color||c[D],tt="line"!==o?H(q,.5):q;var K={label:L.name||"",data:N[D],fill:"area"===o,borderColor:q,backgroundColor:tt,borderWidth:2},Y="line"===o||"area"===o||"scatter"===o||"bubble"===o;Y&&(K.pointBackgroundColor=q,K.pointHoverBackgroundColor=q,K.pointHitRadius=50),"bubble"===o&&(K.pointBackgroundColor=tt,K.pointHoverBackgroundColor=tt,K.pointHoverBorderWidth=2),L.stack&&(K.stack=L.stack),!1===_(e,L,"curve")?K.tension=0:Y&&(K.tension=.4),!1===_(e,L,"points")&&(K.pointRadius=0,K.pointHoverRadius=0),K=n(K,e.options.dataset||{}),K=n(K,L.library||{}),K=n(K,L.dataset||{}),a.push(K)}var X=e.options.xmin,Z=e.options.xmax;if("datetime"===e.xtype?(R(X)&&(r.scales.x.min=l(X).getTime()),R(Z)&&(r.scales.x.max=l(Z).getTime())):"number"===e.xtype&&(R(X)&&(r.scales.x.min=X),R(Z)&&(r.scales.x.max=Z)),"datetime"===e.xtype&&0===i.length&&(R(X)&&i.push(l(X)),R(Z)&&i.push(l(Z)),p=!1,u=!1,d=!1,z=!1,b=!1,M=!1),"datetime"===e.xtype&&i.length>0){var q,tt,te,tr=(R(X)?l(X):i[0]).getTime(),to=(R(Z)?l(Z):i[0]).getTime();for(D=1;D<i.length;D++){var tn=i[D].getTime();tn<tr&&(tr=tn),tn>to&&(to=tn)}var ta=(to-tr)/864e5;if(!r.scales.x.time.unit&&(z||ta>3650?(r.scales.x.time.unit="year",te=365):d||ta>300?(r.scales.x.time.unit="month",te=30):p||ta>10?(r.scales.x.time.unit="day",te=1):b||ta>.5?(r.scales.x.time.displayFormats={hour:"MMM d, h a"},r.scales.x.time.unit="hour",te=1/24):M&&(r.scales.x.time.displayFormats={minute:"h:mm a"},r.scales.x.time.unit="minute",te=1/24/60),te&&ta>0)){var ti=e.element.offsetWidth;if(ti>0){var ts=Math.ceil(ta/te/(ti/100));u&&1===te&&(ts=7*Math.ceil(ts/7)),r.scales.x.time.stepSize=ts}}!r.scales.x.time.tooltipFormat&&(p?r.scales.x.time.tooltipFormat="PP":b?r.scales.x.time.tooltipFormat="MMM d, h a":M&&(r.scales.x.time.tooltipFormat="h:mm a"))}return{labels:i,datasets:a}},W=function t(e){this.name="chartjs",this.library=e};W.prototype.renderLineChart=function t(e,r){var o={};!e.options.max&&function t(e){var r,o,n;for(r=0;r<e.length;r++)for(o=0,n=e[r].data;o<n.length;o++)if(0!=n[o][1])return!1;return!0}(e.data)&&(o.max=1);var a=P(e,n(o,e.options));j(e,a,r);var i=U(e,a,r||"line");"number"===e.xtype?(a.scales.x.type=a.scales.x.type||"linear",a.scales.x.position=a.scales.x.position||"bottom"):a.scales.x.type="string"===e.xtype?"category":"time",this.drawChart(e,"line",i,a)},W.prototype.renderPieChart=function t(e){var r=n({},C);e.options.donut&&(r.cutout="50%"),"legend"in e.options&&k(r,e.options.legend),e.options.title&&S(r,e.options.title),r=n(r,e.options.library||{}),j(e,r,"pie");for(var o=[],a=[],i=0;i<e.data.length;i++){var s=e.data[i];o.push(s[0]),a.push(s[1])}var l={data:a,backgroundColor:e.options.colors||A};l=n(l,e.options.dataset||{});var c={labels:o,datasets:[l]};this.drawChart(e,"pie",c,r)},W.prototype.renderColumnChart=function t(e,r){if("bar"===r){var o,a=n(C,w);a.indexAxis="y",a.scales.x.grid.drawOnChartArea=!0,a.scales.y.grid.drawOnChartArea=!1,delete a.scales.y.ticks.maxTicksLimit,o=p(a,k,S,E,L,B,O,F)(e,e.options)}else o=P(e,e.options);j(e,o,r);var i=U(e,o,"column");"bar"!==r&&N(e,i,o),this.drawChart(e,"bar",i,o)},W.prototype.renderAreaChart=function t(e){this.renderLineChart(e,"area")},W.prototype.renderBarChart=function t(e){this.renderColumnChart(e,"bar")},W.prototype.renderScatterChart=function t(e,r){r=r||"scatter";var o=P(e,e.options);j(e,o,r),"showLine"in o||(o.showLine=!1);var n=U(e,o,r);o.scales.x.type=o.scales.x.type||"linear",o.scales.x.position=o.scales.x.position||"bottom","mode"in o.interaction||(o.interaction.mode="nearest"),this.drawChart(e,r,n,o)},W.prototype.renderBubbleChart=function t(e){this.renderScatterChart(e,"bubble")},W.prototype.destroy=function t(e){e.chart&&e.chart.destroy()},W.prototype.drawChart=function t(e,r,o,n){if(this.destroy(e),!e.destroyed){var a={type:r,data:o,options:n};e.options.code&&window.console.log("new Chart(ctx, "+JSON.stringify(a)+");"),e.element.innerHTML="<canvas></canvas>";var i=e.element.getElementsByTagName("CANVAS")[0];e.chart=new this.library(i,a)}};var V={chart:{},xAxis:{title:{text:null},labels:{style:{fontSize:"12px"}}},yAxis:{title:{text:null},labels:{style:{fontSize:"12px"}}},title:{text:null},credits:{enabled:!1},legend:{borderWidth:0},tooltip:{style:{fontSize:"12px"}},plotOptions:{areaspline:{},area:{},series:{marker:{}}},time:{useUTC:!1}},Q=function(t,e,r){void 0!==e?(t.legend.enabled=!!e,e&&!0!==e&&("top"===e||"bottom"===e?t.legend.verticalAlign=e:(t.legend.layout="vertical",t.legend.verticalAlign="middle",t.legend.align=e))):r&&(t.legend.enabled=!1)},I=function(t,e){t.title.text=e},G=p(V,Q,I,function(t,e){t.yAxis.min=e},function(t,e){t.yAxis.max=e},function(t,e){var r=e?!0===e?"normal":e:null;t.plotOptions.series.stacking=r,t.plotOptions.area.stacking=r,t.plotOptions.areaspline.stacking=r},function(t,e){t.xAxis.title.text=e},function(t,e){t.yAxis.title.text=e}),J=function(e,r,o){var n={prefix:e.options.prefix,suffix:e.options.suffix,thousands:e.options.thousands,decimal:e.options.decimal,precision:e.options.precision,round:e.options.round,zeros:e.options.zeros};"pie"===o||t(r.yAxis)||r.yAxis.labels.formatter||(r.yAxis.labels.formatter=function(){return x("",this.value,n)}),r.tooltip.pointFormatter||r.tooltip.pointFormat||(r.tooltip.pointFormatter=function(){return'<span style="color:'+this.color+'">●</span> '+x(this.series.name+": <b>",this.y,n)+"</b><br/>"})},K=function t(e){this.name="highcharts",this.library=e};K.prototype.renderLineChart=function t(e,r){var o={};"areaspline"===(r=r||"spline")&&(o={plotOptions:{areaspline:{stacking:"normal"},area:{stacking:"normal"},series:{marker:{enabled:!1}}}}),!1===e.options.curve&&("areaspline"===r?r="area":"spline"===r&&(r="line"));var n,a,i,s=G(e,e.options,o);"number"===e.xtype?s.xAxis.type=s.xAxis.type||"linear":s.xAxis.type="string"===e.xtype?"category":"datetime",s.chart.type||(s.chart.type=r),J(e,s,r);var l=e.data;for(a=0;a<l.length;a++){if(l[a].name=l[a].name||"Value",n=l[a].data,"datetime"===e.xtype)for(i=0;i<n.length;i++)n[i][0]=n[i][0].getTime();l[a].marker={symbol:"circle"},!1===e.options.points&&(l[a].marker.enabled=!1)}this.drawChart(e,l,s)},K.prototype.renderScatterChart=function t(e){var r=G(e,e.options,{});r.chart.type="scatter",this.drawChart(e,e.data,r)},K.prototype.renderPieChart=function t(e){var r=n(V,{});e.options.colors&&(r.colors=e.options.colors),e.options.donut&&(r.plotOptions={pie:{innerSize:"50%"}}),"legend"in e.options&&Q(r,e.options.legend),e.options.title&&I(r,e.options.title);var o=n(r,e.options.library||{});J(e,o,"pie");var a=[{type:"pie",name:e.options.label||"Value",data:e.data}];this.drawChart(e,a,o)},K.prototype.renderColumnChart=function t(e,r){r=r||"column";var o,n,a,i,s=e.data,l=G(e,e.options),c=[],p=[];for(l.chart.type=r,J(e,l,r),o=0;o<s.length;o++)for(n=0,a=s[o];n<a.data.length;n++)c[(i=a.data[n])[0]]||(c[i[0]]=Array(s.length),p.push(i[0])),c[i[0]][o]=i[1];"number"===e.xtype&&p.sort(h),l.xAxis.categories=p;var u,d=[];for(o=0;o<s.length;o++){for(n=0,i=[];n<p.length;n++)i.push(c[p[n]][o]||0);u={name:s[o].name||"Value",data:i},s[o].stack&&(u.stack=s[o].stack),d.push(u)}this.drawChart(e,d,l)},K.prototype.renderBarChart=function t(e){this.renderColumnChart(e,"bar")},K.prototype.renderAreaChart=function t(e){this.renderLineChart(e,"areaspline")},K.prototype.destroy=function t(e){e.chart&&e.chart.destroy()},K.prototype.drawChart=function t(e,r,o){this.destroy(e),!e.destroyed&&(o.chart.renderTo=e.element.id,o.series=r,e.options.code&&window.console.log("new Highcharts.Chart("+JSON.stringify(o)+");"),e.chart=new this.library.Chart(o))};var Y={},X=[],Z={chartArea:{},fontName:"'Lucida Grande', 'Lucida Sans Unicode', Verdana, Arial, Helvetica, sans-serif",pointSize:6,legend:{textStyle:{fontSize:12,color:"#444"},alignment:"center",position:"right"},curveType:"function",hAxis:{textStyle:{color:"#666",fontSize:12},titleTextStyle:{},gridlines:{color:"transparent"},baselineColor:"#ccc",viewWindow:{}},vAxis:{textStyle:{color:"#666",fontSize:12},titleTextStyle:{},baselineColor:"#ccc",viewWindow:{}},tooltip:{textStyle:{color:"#666",fontSize:12}}},q=function(t,e,r){if(void 0!==e){var o;o=e?!0===e?"right":e:"none",t.legend.position=o}else r&&(t.legend.position="none")},tt=function(t,e){t.title=e,t.titleTextStyle={color:"#333",fontSize:"20px"}},te=function(t,e){t.hAxis.viewWindow.min=e},tr=function(t,e){t.hAxis.viewWindow.max=e},to=function(t,e){t.isStacked=!!e&&e},tn=function(t,e){t.hAxis.title=e,t.hAxis.titleTextStyle.italic=!1},ta=function(t,e){t.vAxis.title=e,t.vAxis.titleTextStyle.italic=!1},ti=p(Z,q,tt,function(t,e){t.vAxis.viewWindow.min=e},function(t,e){t.vAxis.viewWindow.max=e},to,tn,ta),ts=function(t){window.attachEvent?window.attachEvent("onresize",t):window.addEventListener&&window.addEventListener("resize",t,!0),t()},tl=function t(e){this.name="google",this.library=e};function tc(t,e){var r,o,n=[];if(o="number"===e?s:"datetime"===e?l:i,"bubble"===e)for(r=0;r<t.length;r++)n.push([s(t[r][0]),s(t[r][1]),s(t[r][2])]);else for(r=0;r<t.length;r++)n.push([o(t[r][0]),s(t[r][1])]);return"datetime"===e?n.sort(u):"number"===e&&n.sort(d),n}function tp(t,e){var r,o,n;for(r=0;r<t.length;r++)for(o=0,n=c(t[r].data);o<n.length;o++)if(!e(n[o][0]))return!1;return!0}function tu(e,r,o){var n,a,i,s,l=e.options,p=e.rawData;for(e.singleSeriesFormat=!t(p)||"object"!=typeof p[0]||t(p[0]),e.singleSeriesFormat&&(p=[{name:l.label,data:p}]),p=function t(e){var r,o,n=[];for(r=0;r<e.length;r++){var a={};for(o in e[r])e[r].hasOwnProperty(o)&&(a[o]=e[r][o]);n.push(a)}return n}(p),s=0;s<p.length;s++)p[s].data=c(p[s].data);for(s=0,e.xtype=r||(l.discrete?"string":(n=p,a=o,i=l,th(n)?(i.xmin||i.xmax)&&(!i.xmin||z(i.xmin))&&(!i.xmax||z(i.xmax))?"datetime":"number":tp(n,b)?"number":!a&&tp(n,z)?"datetime":"string"));s<p.length;s++)p[s].data=tc(p[s].data,e.xtype);return p}function td(t){var e,r,o=c(t.rawData);for(r=0;r<o.length;r++)o[r]=[""+(e=o[r][0]),s(o[r][1])];return o}function th(t,e){if("PieChart"===e||"GeoChart"===e||"Timeline"===e)return 0===t.length;for(var r=0;r<t.length;r++)if(t[r].data.length>0)return!1;return!0}function tf(t,e,r){if(t.addEventListener)return t.addEventListener(e,r,!1),r;var o=function(){return r.call(t,window.event)};return t.attachEvent("on"+e,o),o}function ty(t,e,r){t.removeEventListener?t.removeEventListener(e,r,!1):t.detachEvent("on"+e,r)}function tm(t,e){if(t===e)return!1;for(;e&&e!==t;)e=e.parentNode;return e===t}tl.prototype.renderLineChart=function t(e){var r=this;this.waitForLoaded(e,function(){var t={};!1===e.options.curve&&(t.curveType="none"),!1===e.options.points&&(t.pointSize=0);var o=ti(e,e.options,t),n=r.createDataTable(e.data,e.xtype);r.drawChart(e,"LineChart",n,o)})},tl.prototype.renderPieChart=function t(e){var r=this;this.waitForLoaded(e,function(){var t={chartArea:{top:"10%",height:"80%"},legend:{}};e.options.colors&&(t.colors=e.options.colors),e.options.donut&&(t.pieHole=.5),"legend"in e.options&&q(t,e.options.legend),e.options.title&&tt(t,e.options.title);var o=n(n(Z,t),e.options.library||{}),a=new r.library.visualization.DataTable;a.addColumn("string",""),a.addColumn("number","Value"),a.addRows(e.data),r.drawChart(e,"PieChart",a,o)})},tl.prototype.renderColumnChart=function t(e){var r=this;this.waitForLoaded(e,function(){var t=ti(e,e.options),o=r.createDataTable(e.data,e.xtype);r.drawChart(e,"ColumnChart",o,t)})},tl.prototype.renderBarChart=function t(e){var r=this;this.waitForLoaded(e,function(){var t=p(Z,q,tt,te,tr,to,tn,ta)(e,e.options,{hAxis:{gridlines:{color:"#ccc"}}}),o=r.createDataTable(e.data,e.xtype);r.drawChart(e,"BarChart",o,t)})},tl.prototype.renderAreaChart=function t(e){var r=this;this.waitForLoaded(e,function(){var t=ti(e,e.options,{isStacked:!0,pointSize:0,areaOpacity:.5}),o=r.createDataTable(e.data,e.xtype);r.drawChart(e,"AreaChart",o,t)})},tl.prototype.renderGeoChart=function t(e){var r=this;this.waitForLoaded(e,"geochart",function(){var t=n(n(Z,{legend:"none",colorAxis:{colors:e.options.colors||["#f6c7b6","#ce502d"]}}),e.options.library||{}),o=new r.library.visualization.DataTable;o.addColumn("string",""),o.addColumn("number",e.options.label||"Value"),o.addRows(e.data),r.drawChart(e,"GeoChart",o,t)})},tl.prototype.renderScatterChart=function t(e){var r=this;this.waitForLoaded(e,function(){var t,o,n,a,i=ti(e,e.options,{}),s=e.data,l=[];for(t=0;t<s.length;t++)for(o=0,s[t].name=s[t].name||"Value",a=s[t].data;o<a.length;o++){var c=Array(s.length+1);c[0]=a[o][0],c[t+1]=a[o][1],l.push(c)}for((n=new r.library.visualization.DataTable).addColumn("number",""),t=0;t<s.length;t++)n.addColumn("number",s[t].name);n.addRows(l),r.drawChart(e,"ScatterChart",n,i)})},tl.prototype.renderTimeline=function t(e){var r=this;this.waitForLoaded(e,"timeline",function(){var t={legend:"none"};e.options.colors&&(t.colors=e.options.colors);var o=n(n(Z,t),e.options.library||{}),a=new r.library.visualization.DataTable;a.addColumn({type:"string",id:"Name"}),a.addColumn({type:"date",id:"Start"}),a.addColumn({type:"date",id:"End"}),a.addRows(e.data),e.element.style.lineHeight="normal",r.drawChart(e,"Timeline",a,o)})},tl.prototype.destroy=function t(e){e.chart&&e.chart.clearChart()},tl.prototype.drawChart=function t(e,r,o,n){this.destroy(e),!e.destroyed&&(e.options.code&&window.console.log("var data = new google.visualization.DataTable("+o.toJSON()+");\nvar chart = new google.visualization."+r+"(element);\nchart.draw(data, "+JSON.stringify(n)+");"),e.chart=new this.library.visualization[r](e.element),ts(function(){e.chart.draw(o,n)}))},tl.prototype.waitForLoaded=function t(e,r,o){var n=this;if(o||(o=r,r="corechart"),X.push({pack:r,callback:o}),Y[r])this.runCallbacks();else{Y[r]=!0;var a={packages:[r],callback:function(){n.runCallbacks()}},i=e.__config();i.language&&(a.language=i.language),"geochart"===r&&i.mapsApiKey&&(a.mapsApiKey=i.mapsApiKey),this.library.charts.load("current",a)}},tl.prototype.runCallbacks=function t(){for(var e,r,o=0;o<X.length;o++)e=X[o],(r=this.library.visualization&&("corechart"===e.pack&&this.library.visualization.LineChart||"timeline"===e.pack&&this.library.visualization.Timeline||"geochart"===e.pack&&this.library.visualization.GeoChart))&&(e.callback(),X.splice(o,1),o--)},tl.prototype.createDataTable=function t(e,r){var o,n,a,i,l,c,p,h=[],f=[];for(a=0;a<e.length;a++)for(i=0,l=e[a],e[a].name=e[a].name||"Value";i<l.data.length;i++)c=l.data[i],h[p="datetime"===r?c[0].getTime():c[0]]||(h[p]=Array(e.length),f.push(p)),h[p][a]=s(c[1]);var y=[],$=!0;for(i=0;i<f.length;i++)a=f[i],"datetime"===r?(n=new Date(s(a)),$=$&&m(n)):n="number"===r?s(a):a,y.push([n].concat(h[a]));if("datetime"===r)y.sort(u);else if("number"===r){for(y.sort(d),a=0;a<y.length;a++)y[a][0]=""+(o=y[a][0]);r="string"}var g=new this.library.visualization.DataTable;for(r="datetime"===r&&$?"date":r,g.addColumn(r,""),a=0;a<e.length;a++)g.addColumn("number",e[a].name);return g.addRows(y),g};var t$=[],tg=0;function tv(){if(tg<4){var t,e,r,o=t$.shift();o&&(tg++,t=o[0],e=o[1],r=o[2],function t(e,r,o){var n=window.jQuery||window.Zepto||window.$;if(n&&n.ajax)n.ajax({dataType:"json",url:e,success:r,error:o,complete:tz});else{var a=new XMLHttpRequest;a.open("GET",e,!0),a.setRequestHeader("Content-Type","application/json"),a.onload=function(){tz(),200===a.status?r(JSON.parse(a.responseText),a.statusText,a):o(a,"error",a.statusText)},a.send()}}(t,e,function(t,e,o){r("string"==typeof o?o:o.message)}),tv())}}function tz(){tg--,tv()}var tb={},tM=[];function tx(t,e){document.body.innerText?t.innerText=e:t.textContent=e}function t_(t,e,r){r||(e="Error Loading Chart: "+e),tx(t,e),t.style.color="#ff0000"}function tC(t){try{t.__render()}catch(e){throw t_(t.element,e.message),e}}function t8(t,e,r){if(r&&t.options.loading&&("string"==typeof e||"function"==typeof e)&&tx(t.element,t.options.loading),"string"==typeof e){var o,n,a;o=e,n=function(e){t.rawData=e,tC(t)},a=function(e){t_(t.element,e)},t$.push([o,n,a]),tv()}else if("function"==typeof e)try{e(function(e){t.rawData=e,tC(t)},function(e){t_(t.element,e,!0)})}catch(i){t_(t.element,i,!0)}else t.rawData=e,tC(t)}function tw(t){var r=new(function t(r){if(r){if("Highcharts"===r.product)return K;if(r.charts)return tl;if(e(r))return W}throw Error("Unknown adapter")}(t))(t);-1===tM.indexOf(r)&&tM.push(r)}var tA=function t(e,r,o){var a;if("string"==typeof e&&(a=e,!(e=document.getElementById(e))))throw Error("No element with id "+a);this.element=e,this.options=n(tB.options,o||{}),this.dataSource=r,tB.charts[e.id]=this,t8(this,r,!0),this.options.refresh&&this.startRefresh()};tA.prototype.getElement=function t(){return this.element},tA.prototype.getDataSource=function t(){return this.dataSource},tA.prototype.getData=function t(){return this.data},tA.prototype.getOptions=function t(){return this.options},tA.prototype.getChartObject=function t(){return this.chart},tA.prototype.getAdapter=function t(){return this.adapter},tA.prototype.updateData=function t(e,r){this.dataSource=e,r&&this.__updateOptions(r),t8(this,e,!0)},tA.prototype.setOptions=function t(e){this.__updateOptions(e),this.redraw()},tA.prototype.redraw=function t(){t8(this,this.rawData)},tA.prototype.refreshData=function t(){if("string"==typeof this.dataSource){var e=-1===this.dataSource.indexOf("?")?"?":"&",r=this.dataSource+e+"_="+new Date().getTime();t8(this,r)}else"function"==typeof this.dataSource&&t8(this,this.dataSource)},tA.prototype.startRefresh=function t(){var e=this,r=this.options.refresh;if(r&&"string"!=typeof this.dataSource&&"function"!=typeof this.dataSource)throw Error("Data source must be a URL or callback for refresh");if(!this.intervalId){if(r)this.intervalId=setInterval(function(){e.refreshData()},1e3*r);else throw Error("No refresh interval")}},tA.prototype.stopRefresh=function t(){this.intervalId&&(clearInterval(this.intervalId),this.intervalId=null)},tA.prototype.toImage=function t(e){if("chartjs"===this.adapter){if(!e||!e.background||"transparent"===e.background)return this.chart.toBase64Image();var r=this.chart.canvas,o=this.chart.ctx,n=document.createElement("canvas"),a=n.getContext("2d");return n.width=o.canvas.width,n.height=o.canvas.height,a.fillStyle=e.background,a.fillRect(0,0,n.width,n.height),a.drawImage(r,0,0),n.toDataURL("image/png")}throw Error("Feature only available for Chart.js")},tA.prototype.destroy=function t(){this.destroyed=!0,this.stopRefresh(),this.__adapterObject&&this.__adapterObject.destroy(this),this.__enterEvent&&ty(this.element,"mouseover",this.__enterEvent),this.__leaveEvent&&ty(this.element,"mouseout",this.__leaveEvent)},tA.prototype.__updateOptions=function t(e){var r=e.refresh&&e.refresh!==this.options.refresh;this.options=n(tB.options,e),r&&(this.stopRefresh(),this.startRefresh())},tA.prototype.__render=function t(){this.data=this.__processData(),function t(r,o){if(th(o.data,r)){var n,a,i,s,l,c=o.options.empty||o.options.messages&&o.options.messages.empty||"No data";tx(o.element,c)}else(function t(r,o){var n,a,i,s;for(i="render"+r,s=o.options.adapter,("Chart"in window)&&tw(window.Chart),("Highcharts"in window)&&tw(window.Highcharts),window.google&&window.google.charts&&tw(window.google),n=0;n<tM.length;n++)if(a=tM[n],(!s||s===a.name)&&e(a[i]))return o.adapter=a.name,o.__adapterObject=a,a[i](o);if(tM.length>0)throw Error("No charting library found for "+r);throw Error("No charting libraries found - be sure to include one before your charts")})(r,o),o.options.download&&!o.__downloadAttached&&"chartjs"===o.adapter&&(a=(n=o).element,i=document.createElement("a"),!0===(s=n.options.download)?s={}:"string"==typeof s&&(s={filename:s}),i.download=s.filename||"chart.png",i.style.position="absolute",i.style.top="20px",i.style.right="20px",i.style.zIndex=1e3,i.style.lineHeight="20px",i.target="_blank",(l=document.createElement("img")).alt="Download",l.style.border="none",l.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAMAAAC6V+0/AAABCFBMVEUAAADMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMywEsqxAAAAV3RSTlMAAQIDBggJCgsMDQ4PERQaHB0eISIjJCouLzE0OTo/QUJHSUpLTU5PUllhYmltcHh5foWLjI+SlaCio6atr7S1t7m6vsHHyM7R2tze5Obo7fHz9ff5+/1hlxK2AAAA30lEQVQYGUXBhVYCQQBA0TdYWAt2d3d3YWAHyur7/z9xgD16Lw0DW+XKx+1GgX+FRzM3HWQWrHl5N/oapW5RPe0PkBu+UYeICvozTWZVK23Ao04B79oJrOsJDOoxkZoQPWgX29pHpCZEk7rEvQYiNSFq1UMqvlCjJkRBS1R8hb00Vb/TajtBL7nTHE1X1vyMQF732dQhyF2o6SAwrzP06iUQzvwsArlnzcOdrgBhJyHa1QOgO9U1GsKuvjUTjavliZYQ8nNPapG6sap/3nrIdJ6bOWzmX/fy0XVpfzZP3S8OJT3g9EEiJwAAAABJRU5ErkJggg==",i.appendChild(l),a.style.position="relative",n.__downloadAttached=!0,n.__enterEvent=tf(a,"mouseover",function(t){var e=t.relatedTarget;e&&(e===this||tm(this,e))||!n.options.download||(i.href=n.toImage(s),a.appendChild(i))}),n.__leaveEvent=tf(a,"mouseout",function(t){var e=t.relatedTarget;e&&(e===this||tm(this,e))||!i.parentNode||i.parentNode.removeChild(i)}))}(this.__chartName(),this)},tA.prototype.__config=function t(){return tb};var tk=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){return tu(this)},e.prototype.__chartName=function t(){return"LineChart"},e}(tA),t0=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){return td(this)},e.prototype.__chartName=function t(){return"PieChart"},e}(tA),tS=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){return tu(this,null,!0)},e.prototype.__chartName=function t(){return"ColumnChart"},e}(tA),tT=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){return tu(this,null,!0)},e.prototype.__chartName=function t(){return"BarChart"},e}(tA),t4=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){return tu(this)},e.prototype.__chartName=function t(){return"AreaChart"},e}(tA),tD=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){return td(this)},e.prototype.__chartName=function t(){return"GeoChart"},e}(tA),tE=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){return tu(this,"number")},e.prototype.__chartName=function t(){return"ScatterChart"},e}(tA),t1=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){return tu(this,"bubble")},e.prototype.__chartName=function t(){return"BubbleChart"},e}(tA),tL=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){var e,r=this.rawData;for(e=0;e<r.length;e++)r[e][1]=l(r[e][1]),r[e][2]=l(r[e][2]);return r},e.prototype.__chartName=function t(){return"Timeline"},e}(tA),tB={LineChart:tk,PieChart:t0,ColumnChart:tS,BarChart:tT,AreaChart:t4,GeoChart:tD,ScatterChart:tE,BubbleChart:t1,Timeline:tL,charts:{},configure:function(t){for(var e in t)t.hasOwnProperty(e)&&(tb[e]=t[e])},setDefaultOptions:function(t){tB.options=t},eachChart:function(t){for(var e in tB.charts)tB.charts.hasOwnProperty(e)&&t(tB.charts[e])},destroyAll:function(){for(var t in tB.charts)tB.charts.hasOwnProperty(t)&&(tB.charts[t].destroy(),delete tB.charts[t])},config:tb,options:{},adapters:tM,addAdapter:tw,use:function(t){return tw(t),tB}};return"undefined"==typeof window||window.Chartkick||(window.Chartkick=tB,document.addEventListener("turbolinks:before-render",function(){!1!==tb.autoDestroy&&tB.destroyAll()}),document.addEventListener("turbo:before-render",function(){!1!==tb.autoDestroy&&tB.destroyAll()}),setTimeout(function(){window.dispatchEvent(new Event("chartkick:load"))},0)),tB.default=tB,tB});
|
20
|
+
</script>
|
21
|
+
|
22
|
+
<script type="text/javascript">
|
23
|
+
window.onload = () => {
|
24
|
+
'use strict'
|
25
|
+
document.querySelector('#navbarSideCollapse').addEventListener('click', () => {
|
26
|
+
document.querySelector('.offcanvas-collapse').classList.toggle('open')
|
27
|
+
})
|
28
|
+
}
|
29
|
+
</script>
|
30
|
+
<style type="text/css">
|
31
|
+
.bd-placeholder-img {
|
32
|
+
font-size: 1.125rem;
|
33
|
+
text-anchor: middle;
|
34
|
+
-webkit-user-select: none;
|
35
|
+
-moz-user-select: none;
|
36
|
+
user-select: none;
|
37
|
+
}
|
38
|
+
|
39
|
+
@media (min-width: 768px) {
|
40
|
+
.bd-placeholder-img-lg {
|
41
|
+
font-size: 3.5rem;
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
.b-example-divider {
|
46
|
+
height: 3rem;
|
47
|
+
background-color: rgba(0, 0, 0, .1);
|
48
|
+
border: solid rgba(0, 0, 0, .15);
|
49
|
+
border-width: 1px 0;
|
50
|
+
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
|
51
|
+
}
|
52
|
+
|
53
|
+
.b-example-vr {
|
54
|
+
flex-shrink: 0;
|
55
|
+
width: 1.5rem;
|
56
|
+
height: 100vh;
|
57
|
+
}
|
58
|
+
|
59
|
+
.bi {
|
60
|
+
vertical-align: -.125em;
|
61
|
+
fill: currentColor;
|
62
|
+
}
|
63
|
+
|
64
|
+
.nav-scroller {
|
65
|
+
position: relative;
|
66
|
+
z-index: 2;
|
67
|
+
height: 2.75rem;
|
68
|
+
overflow-y: hidden;
|
69
|
+
}
|
70
|
+
|
71
|
+
.nav-scroller .nav {
|
72
|
+
display: flex;
|
73
|
+
flex-wrap: nowrap;
|
74
|
+
padding-bottom: 1rem;
|
75
|
+
margin-top: -1px;
|
76
|
+
overflow-x: auto;
|
77
|
+
text-align: center;
|
78
|
+
white-space: nowrap;
|
79
|
+
-webkit-overflow-scrolling: touch;
|
80
|
+
}
|
81
|
+
html,
|
82
|
+
body {
|
83
|
+
overflow-x: hidden; /* Prevent scroll on narrow devices */
|
84
|
+
}
|
85
|
+
|
86
|
+
body {
|
87
|
+
padding-top: 56px;
|
88
|
+
}
|
89
|
+
|
90
|
+
@media (max-width: 991.98px) {
|
91
|
+
.offcanvas-collapse {
|
92
|
+
position: fixed;
|
93
|
+
top: 56px; /* Height of navbar */
|
94
|
+
bottom: 0;
|
95
|
+
left: 100%;
|
96
|
+
width: 100%;
|
97
|
+
padding-right: 1rem;
|
98
|
+
padding-left: 1rem;
|
99
|
+
overflow-y: auto;
|
100
|
+
visibility: hidden;
|
101
|
+
background-color: #343a40;
|
102
|
+
transition: transform .3s ease-in-out, visibility .3s ease-in-out;
|
103
|
+
}
|
104
|
+
.offcanvas-collapse.open {
|
105
|
+
visibility: visible;
|
106
|
+
transform: translateX(-100%);
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
.nav-scroller .nav {
|
111
|
+
color: rgba(255, 255, 255, .75);
|
112
|
+
}
|
113
|
+
|
114
|
+
.nav-scroller .nav-link {
|
115
|
+
padding-top: .75rem;
|
116
|
+
padding-bottom: .75rem;
|
117
|
+
font-size: .875rem;
|
118
|
+
color: #6c757d;
|
119
|
+
}
|
120
|
+
|
121
|
+
.nav-scroller .nav-link:hover {
|
122
|
+
color: #007bff;
|
123
|
+
}
|
124
|
+
|
125
|
+
.nav-scroller .active {
|
126
|
+
font-weight: 500;
|
127
|
+
color: #343a40;
|
128
|
+
}
|
129
|
+
|
130
|
+
.bg-purple {
|
131
|
+
background-color: #6f42c1;
|
132
|
+
}
|
133
|
+
</style>
|
134
|
+
</head>
|
135
|
+
<body class="bg-light">
|
136
|
+
|
137
|
+
<nav class="navbar navbar-expand-lg fixed-top navbar-dark bg-dark" aria-label="Main navigation">
|
138
|
+
<div class="container-fluid">
|
139
|
+
<a class="navbar-brand" href="#">Caffeinate</a>
|
140
|
+
<button class="navbar-toggler p-0 border-0" type="button" id="navbarSideCollapse" aria-label="Toggle navigation">
|
141
|
+
<span class="navbar-toggler-icon"></span>
|
142
|
+
</button>
|
143
|
+
|
144
|
+
<div class="navbar-collapse offcanvas-collapse" id="navbarsExampleDefault">
|
145
|
+
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
146
|
+
<li class="nav-item">
|
147
|
+
<%= active_link_to "Dashboard", root_path, active: controller_name == 'dashboard' %>
|
148
|
+
</li>
|
149
|
+
<li class="nav-item">
|
150
|
+
<%= active_link_to "Campaigns", campaigns_path, active: controller_name == 'campaigns' %>
|
151
|
+
</li>
|
152
|
+
<li class="nav-item">
|
153
|
+
<%= active_link_to "Subscriptions", subscriptions_path, active: controller_name == 'subscriptions' %>
|
154
|
+
</li>
|
155
|
+
<li class="nav-item">
|
156
|
+
<%= active_link_to "Mailings", mailings_path, active: controller_name == 'mailings' %>
|
157
|
+
</li>
|
158
|
+
</ul>
|
159
|
+
</div>
|
160
|
+
</div>
|
161
|
+
</nav>
|
162
|
+
|
163
|
+
<main class="container my-5">
|
164
|
+
<%= yield %>
|
165
|
+
</main>
|
166
|
+
<footer class="text-center small mb-3">
|
167
|
+
Running <a href="https://github.com/joshmn/caffeinate">Caffeinate <%= ::Caffeinate::VERSION %></a> and <a href="https://github.com/joshmn/caffeinate-webui">Caffeinate WebUI <%= ::Caffeinate::Webui::VERSION %></a>
|
168
|
+
</footer>
|
169
|
+
</body>
|
170
|
+
</html>
|
@@ -0,0 +1,58 @@
|
|
1
|
+
<div class="my-3 p-3 bg-body rounded shadow-sm">
|
2
|
+
<div class="d-flex justify-content-between border-bottom pb-2 mb-0">
|
3
|
+
<h6 class="">Mailings</h6>
|
4
|
+
<div class="d-flex gap-4">
|
5
|
+
<div class="dropdown">
|
6
|
+
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
7
|
+
<%= @campaign ? @campaign.name : "All campaigns" %>
|
8
|
+
</a>
|
9
|
+
<ul class="dropdown-menu">
|
10
|
+
<% @campaigns.each do |campaign| %>
|
11
|
+
<li><a class="dropdown-item <%= 'active' if params[:campaign_id] == campaign.id.to_s %>" href="<%= mailings_path(campaign_id: campaign.id, status: @status) %>"><%= campaign.name %></a></li>
|
12
|
+
<% end %>
|
13
|
+
<% if @campaign %>
|
14
|
+
<li><a class="dropdown-item" href="<%= mailings_path(status: @status) %>">All campaigns</a></li>
|
15
|
+
<% end %>
|
16
|
+
</ul>
|
17
|
+
</div>
|
18
|
+
<div class="dropdown">
|
19
|
+
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
20
|
+
<%= @status.try(:titleize) || "All" %>
|
21
|
+
</a>
|
22
|
+
<ul class="dropdown-menu">
|
23
|
+
<li><a class="dropdown-item <%= 'active' if params[:status] == 'unsent' %>" href="<%= mailings_path(campaign_id: @campaign.try(:id), status: :unsent) %>">Unsent</a></li>
|
24
|
+
<li><a class="dropdown-item <%= 'active' if params[:status] == 'sent' %>" href="<%= mailings_path(campaign_id: @campaign.try(:id), status: :sent) %>">Sent</a></li>
|
25
|
+
<li><a class="dropdown-item <%= 'active' if params[:status] == 'skipped' %>" href="<%= mailings_path(campaign_id: @campaign.try(:id), status: :skipped) %>">Skipped</a></li>
|
26
|
+
<% if @status %>
|
27
|
+
<li><a class="dropdown-item" href="<%= mailings_path(campaign_id: @campaign.try(:id)) %>">All states</a></li>
|
28
|
+
<% end %>
|
29
|
+
</ul>
|
30
|
+
</div>
|
31
|
+
</div>
|
32
|
+
</div>
|
33
|
+
|
34
|
+
<div class="d-flex text-muted pt-3">
|
35
|
+
<table class="table">
|
36
|
+
<thead>
|
37
|
+
<tr>
|
38
|
+
<td>Who</td>
|
39
|
+
<td>Campaign</td>
|
40
|
+
<td>Send date</td>
|
41
|
+
</tr>
|
42
|
+
</thead>
|
43
|
+
<tbody>
|
44
|
+
<% @mailings.each do |mailing| %>
|
45
|
+
<tr>
|
46
|
+
<td><%= link_to Caffeinate::Webui::Name.for(mailing.subscriber), subscription_path(mailing.subscriber) %></td>
|
47
|
+
<td><%= link_to mailing.campaign.name, mailing.campaign %></td>
|
48
|
+
<td><%= time(mailing.send_at) %></td>
|
49
|
+
</tr>
|
50
|
+
<% end %>
|
51
|
+
</tbody>
|
52
|
+
</table>
|
53
|
+
</div>
|
54
|
+
<div class="d-flex justify-content-end">
|
55
|
+
<%= will_paginate @mailings %>
|
56
|
+
</div>
|
57
|
+
</div>
|
58
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
<h2><%= @mailing.mailer_class %>#<%= @mailing.mailer_action %> for <%= ::Caffeinate::Webui::Name.for(@mailing.subscriber) %></h2>
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<div class="my-3 p-3 bg-body rounded shadow-sm">
|
2
|
+
<h6 class="border-bottom pb-2 mb-0">Subscriptions</h6>
|
3
|
+
<table class="table">
|
4
|
+
<thead>
|
5
|
+
<tr>
|
6
|
+
<td>Campaign</td>
|
7
|
+
<td>Who</td>
|
8
|
+
<td>Created</td>
|
9
|
+
<td>Status</td>
|
10
|
+
</tr>
|
11
|
+
</thead>
|
12
|
+
<tbody>
|
13
|
+
<% @subscriptions.each do |subscriber| %>
|
14
|
+
<tr>
|
15
|
+
<td><%= link_to subscriber.campaign.name, campaign_path(subscriber.campaign) %></td>
|
16
|
+
<td>
|
17
|
+
<%= link_to ::Caffeinate::Webui::Name.for(subscriber.subscriber), subscription_path(subscriber) %>
|
18
|
+
</td>
|
19
|
+
<td>
|
20
|
+
<%= time(subscriber.created_at) %>
|
21
|
+
</td>
|
22
|
+
<td>
|
23
|
+
<%= "Ended" if subscriber.ended? %>
|
24
|
+
<%= "Active" if subscriber.subscribed? %>
|
25
|
+
<%= "Unsubscribed" if subscriber.unsubscribed? %>
|
26
|
+
</td>
|
27
|
+
</tr>
|
28
|
+
<% end %>
|
29
|
+
</tbody>
|
30
|
+
</table>
|
31
|
+
<div class="d-flex justify-content-end">
|
32
|
+
<%= will_paginate @subscriptions %>
|
33
|
+
</div>
|
34
|
+
</div>
|
@@ -0,0 +1,50 @@
|
|
1
|
+
<div class="d-flex justify-content-between">
|
2
|
+
<div>
|
3
|
+
<h3><%= ::Caffeinate::Webui::Name.for(@subscription.subscriber) %></h3>
|
4
|
+
<p>
|
5
|
+
Subscribed: <%= time_ago_in_words @subscription.created_at %> ago
|
6
|
+
</p>
|
7
|
+
</div>
|
8
|
+
<div class="dropdown">
|
9
|
+
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
10
|
+
<%= "Active" if @subscription.subscribed? %><%= "Completed" if @subscription.ended? %><%= "Unsubscribed" if @subscription.unsubscribed? %>
|
11
|
+
</button>
|
12
|
+
<ul class="dropdown-menu">
|
13
|
+
<% if @subscription.subscribed? %>
|
14
|
+
<li><%= button_to "Unsubscribe", subscription_unsubscribe_path(@subscription), method: :post, class: "dropdown-item" if @subscription.subscribed? %></li>
|
15
|
+
<% else %>
|
16
|
+
<li>No actions</li>
|
17
|
+
<% end %>
|
18
|
+
</ul>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
<div class="my-3 p-3 bg-body rounded shadow-sm">
|
22
|
+
<h6 class="border-bottom pb-2 mb-0">Mailings</h6>
|
23
|
+
<table class="table">
|
24
|
+
<thead>
|
25
|
+
<tr>
|
26
|
+
<td>Mail</td>
|
27
|
+
<td>Status</td>
|
28
|
+
<td></td>
|
29
|
+
</tr>
|
30
|
+
</thead>
|
31
|
+
<tbody>
|
32
|
+
<% @subscription.mailings.each do |mailing| %>
|
33
|
+
<tr>
|
34
|
+
<td>
|
35
|
+
<%= mailing.mailer_class %>#<%= mailing.mailer_action %>
|
36
|
+
</td>
|
37
|
+
<td>
|
38
|
+
<%= "Skipped" if mailing.skipped? %>
|
39
|
+
<% if @subscription.subscribed? %>
|
40
|
+
Sends in <%= distance_of_time_in_words_to_now(mailing.send_at) if mailing.unsent? %> from now
|
41
|
+
<%= time_ago_in_words(mailing.sent_at) if mailing.sent? %>
|
42
|
+
<% else %>
|
43
|
+
Unsubscribed
|
44
|
+
<% end %>
|
45
|
+
</td>
|
46
|
+
</tr>
|
47
|
+
<% end %>
|
48
|
+
</tbody>
|
49
|
+
</table>
|
50
|
+
</div>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
Caffeinate::Webui::Engine.routes.draw do
|
2
|
+
root to: 'dashboard#show'
|
3
|
+
|
4
|
+
resources :campaigns, only: [:index, :show]
|
5
|
+
resources :subscriptions, only: [:index, :show, :destroy] do
|
6
|
+
scope module: :subscriptions do
|
7
|
+
resource :unsubscribe, only: [:create]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
resources :mailings, only: [:index, :show, :destroy]
|
11
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Caffeinate
|
2
|
+
module Webui
|
3
|
+
class Engine < ::Rails::Engine
|
4
|
+
isolate_namespace ::Caffeinate::Webui
|
5
|
+
|
6
|
+
config.generators do |g|
|
7
|
+
g.test_framework :rspec, fixture: false
|
8
|
+
end
|
9
|
+
|
10
|
+
config.autoload_paths += Dir["#{config.root}/lib/**/"]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Caffeinate
|
2
|
+
module Webui
|
3
|
+
class Name
|
4
|
+
def self.for(object)
|
5
|
+
if object.respond_to?(:name)
|
6
|
+
object.name
|
7
|
+
elsif object.respond_to?(:full_name)
|
8
|
+
object.full_name
|
9
|
+
elsif object.respond_to?(:display_name)
|
10
|
+
object.display_name
|
11
|
+
elsif object.respond_to?(:email)
|
12
|
+
object.email
|
13
|
+
elsif object.respond_to?(:to_label)
|
14
|
+
object.to_label
|
15
|
+
else
|
16
|
+
"#{object.class.name}##{object.to_param}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
|
2
|
+
require 'active_support'
|
3
|
+
require 'groupdate'
|
4
|
+
require 'chartkick'
|
5
|
+
require 'will_paginate'
|
6
|
+
require 'will_paginate-bootstrap-style'
|
7
|
+
require 'caffeinate/version'
|
8
|
+
require 'caffeinate/webui/version'
|
9
|
+
require 'caffeinate/webui/engine'
|
10
|
+
require 'caffeinate/webui/name'
|
11
|
+
|
12
|
+
module Caffeinate
|
13
|
+
module Webui
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'caffeinate/webui'
|
metadata
ADDED
@@ -0,0 +1,222 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: caffeinate_webui
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Josh Brody
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-10-31 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: 5.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: caffeinate
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sprockets-rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: groupdate
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: chartkick
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '4'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '4'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: will_paginate-bootstrap-style
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.2.4
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.2.4
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: will_paginate
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '3'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '3'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: pry
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry-rails
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: sqlite3
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: codecov
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
description: Create, manage, and send scheduled email sequences and drip campaigns
|
168
|
+
from your Rails app.
|
169
|
+
email:
|
170
|
+
- josh@josh.mn
|
171
|
+
executables: []
|
172
|
+
extensions: []
|
173
|
+
extra_rdoc_files: []
|
174
|
+
files:
|
175
|
+
- README.md
|
176
|
+
- Rakefile
|
177
|
+
- app/controllers/caffeinate/webui/application_controller.rb
|
178
|
+
- app/controllers/caffeinate/webui/campaigns_controller.rb
|
179
|
+
- app/controllers/caffeinate/webui/dashboard_controller.rb
|
180
|
+
- app/controllers/caffeinate/webui/mailings_controller.rb
|
181
|
+
- app/controllers/caffeinate/webui/subscriptions/unsubscribes_controller.rb
|
182
|
+
- app/controllers/caffeinate/webui/subscriptions_controller.rb
|
183
|
+
- app/helpers/caffeinate/webui/application_helper.rb
|
184
|
+
- app/views/caffeinate/webui/campaigns/index.html.erb
|
185
|
+
- app/views/caffeinate/webui/campaigns/show.html.erb
|
186
|
+
- app/views/caffeinate/webui/dashboard/show.html.erb
|
187
|
+
- app/views/caffeinate/webui/layouts/application.html.erb
|
188
|
+
- app/views/caffeinate/webui/mailings/index.html.erb
|
189
|
+
- app/views/caffeinate/webui/mailings/show.html.erb
|
190
|
+
- app/views/caffeinate/webui/subscriptions/index.html.erb
|
191
|
+
- app/views/caffeinate/webui/subscriptions/show.html.erb
|
192
|
+
- config/routes.rb
|
193
|
+
- lib/caffeinate/webui.rb
|
194
|
+
- lib/caffeinate/webui/engine.rb
|
195
|
+
- lib/caffeinate/webui/name.rb
|
196
|
+
- lib/caffeinate/webui/version.rb
|
197
|
+
- lib/caffeinate_webui.rb
|
198
|
+
homepage: https://github.com/joshmn/caffeinate_webui
|
199
|
+
licenses:
|
200
|
+
- MIT
|
201
|
+
metadata: {}
|
202
|
+
post_install_message:
|
203
|
+
rdoc_options: []
|
204
|
+
require_paths:
|
205
|
+
- lib
|
206
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
207
|
+
requirements:
|
208
|
+
- - ">="
|
209
|
+
- !ruby/object:Gem::Version
|
210
|
+
version: '0'
|
211
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - ">="
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '0'
|
216
|
+
requirements: []
|
217
|
+
rubygems_version: 3.1.4
|
218
|
+
signing_key:
|
219
|
+
specification_version: 4
|
220
|
+
summary: Create, manage, and send scheduled email sequences and drip campaigns from
|
221
|
+
your Rails app.
|
222
|
+
test_files: []
|