Skip to content

Commit 406a525

Browse files
authored
Merge pull request #5308 from rmosolgo/sub-dashboard
GraphQL::Dashboard - add subscription views
2 parents 31fdfe7 + 23cb56e commit 406a525

File tree

13 files changed

+438
-19
lines changed

13 files changed

+438
-19
lines changed

lib/graphql/dashboard.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ class Dashboard < Rails::Engine
6060
end
6161
resources :index_entries, only: [:index, :show], param: :name, constraints: { name: /[A-Za-z0-9_.]+/}
6262
end
63+
64+
namespace :subscriptions do
65+
resources :topics, only: [:index, :show], param: :name, constraints: { name: /.*/ }
66+
resources :subscriptions, only: [:show], constraints: { id: /[a-zA-Z0-9\-]+/ }
67+
post "/subscriptions/clear_all", to: "subscriptions#clear_all", as: :clear_all
68+
end
6369
end
6470

6571
class ApplicationController < ActionController::Base
@@ -155,6 +161,7 @@ def show
155161
end
156162

157163
require 'graphql/dashboard/operation_store'
164+
require 'graphql/dashboard/subscriptions'
158165

159166
# Rails expects the engine to be called `Graphql::Dashboard`,
160167
# but `GraphQL::Dashboard` is consistent with this gem's naming.

lib/graphql/dashboard/installable.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
module Graphql
3+
class Dashboard < Rails::Engine
4+
module Installable
5+
def self.included(child_module)
6+
child_module.before_action(:check_installed)
7+
end
8+
9+
def feature_installed?
10+
raise "Implement #{self.class}#feature_installed? to check whether this should render `not_installed` or not."
11+
end
12+
13+
def check_installed
14+
if !feature_installed?
15+
dashboard_module = self.class.name.split("::")[-2]
16+
render "graphql/dashboard/#{dashboard_module.underscore}/not_installed"
17+
end
18+
end
19+
end
20+
end
21+
end

lib/graphql/dashboard/operation_store.rb

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
# frozen_string_literal: true
2+
require_relative "./installable"
23
module Graphql
34
class Dashboard < Rails::Engine
45
module OperationStore
5-
module CheckInstalled
6-
def self.included(child_module)
7-
child_module.before_action(:check_installed)
8-
end
6+
class BaseController < Dashboard::ApplicationController
7+
include Installable
98

10-
def check_installed
11-
if !schema_class.respond_to?(:operation_store) || schema_class.operation_store.nil?
12-
render "graphql/dashboard/operation_store/not_installed"
13-
end
9+
private
10+
11+
def feature_installed?
12+
schema_class.respond_to?(:operation_store) && schema_class.operation_store.is_a?(GraphQL::Pro::OperationStore)
1413
end
1514
end
16-
class ClientsController < Dashboard::ApplicationController
17-
include CheckInstalled
1815

16+
class ClientsController < BaseController
1917
def index
2018
@order_by = params[:order_by] || "name"
2119
@order_dir = params[:order_dir].presence || "asc"
@@ -74,9 +72,7 @@ def init_client(name: nil, secret: nil)
7472
end
7573
end
7674

77-
class OperationsController < Dashboard::ApplicationController
78-
include CheckInstalled
79-
75+
class OperationsController < BaseController
8076
def index
8177
@client_operations = client_name = params[:client_name]
8278
per_page = params[:per_page]&.to_i || 25
@@ -170,7 +166,7 @@ def update
170166
end
171167
end
172168

173-
class IndexEntriesController < Dashboard::ApplicationController
169+
class IndexEntriesController < BaseController
174170
def index
175171
@search_term = if request.params["q"] && request.params["q"].length > 0
176172
request.params["q"]
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# frozen_string_literal: true
2+
module Graphql
3+
class Dashboard < Rails::Engine
4+
module Subscriptions
5+
class BaseController < Graphql::Dashboard::ApplicationController
6+
include Installable
7+
8+
def feature_installed?
9+
schema_class.subscriptions.is_a?(GraphQL::Pro::Subscriptions)
10+
end
11+
end
12+
13+
class TopicsController < BaseController
14+
def show
15+
topic_name = params[:name]
16+
all_subscription_ids = []
17+
schema_class.subscriptions.each_subscription_id(topic_name) do |sid|
18+
all_subscription_ids << sid
19+
end
20+
21+
page = params[:page]&.to_i || 1
22+
limit = params[:per_page]&.to_i || 20
23+
offset = limit * (page - 1)
24+
subscription_ids = all_subscription_ids[offset, limit]
25+
subs = schema_class.subscriptions.read_subscriptions(subscription_ids)
26+
show_broadcast_subscribers_count = schema_class.subscriptions.show_broadcast_subscribers_count?
27+
subs.each do |sub|
28+
sub[:is_broadcast] = is_broadcast = schema_class.subscriptions.broadcast_subscription_id?(sub[:id])
29+
if is_broadcast && show_broadcast_subscribers_count
30+
sub[:subscribers_count] = sub_count =schema_class.subscriptions.count_broadcast_subscribed(sub[:id])
31+
sub[:still_subscribed] = sub_count > 0
32+
else
33+
sub[:still_subscribed] = schema_class.subscriptions.still_subscribed?(sub[:id])
34+
sub[:subscribers_count] = nil
35+
end
36+
end
37+
38+
@topic_last_triggered_at = schema_class.subscriptions.topic_last_triggered_at(topic_name)
39+
@subscriptions = subs
40+
@subscriptions_count = all_subscription_ids.size
41+
@show_broadcast_subscribers_count = show_broadcast_subscribers_count
42+
@has_next_page = all_subscription_ids.size > offset + limit ? page + 1 : false
43+
end
44+
45+
def index
46+
page = params[:page]&.to_i || 1
47+
per_page = params[:per_page]&.to_i || 20
48+
offset = per_page * (page - 1)
49+
limit = per_page
50+
topics, all_topics_count, has_next_page = schema_class.subscriptions.topics(offset: offset, limit: limit)
51+
52+
@topics = topics
53+
@all_topics_count = all_topics_count
54+
@has_next_page = has_next_page
55+
@page = page
56+
end
57+
end
58+
59+
class SubscriptionsController < BaseController
60+
def show
61+
subscription_id = params[:id]
62+
subscriptions = schema_class.subscriptions
63+
query_data = subscriptions.read_subscription(subscription_id)
64+
is_broadcast = subscriptions.broadcast_subscription_id?(subscription_id)
65+
66+
if is_broadcast && subscriptions.show_broadcast_subscribers_count?
67+
subscribers_count = subscriptions.count_broadcast_subscribed(subscription_id)
68+
is_still_subscribed = subscribers_count > 0
69+
else
70+
subscribers_count = nil
71+
is_still_subscribed = subscriptions.still_subscribed?(subscription_id)
72+
end
73+
74+
@query_data = query_data
75+
@still_subscribed = is_still_subscribed
76+
@is_broadcast = is_broadcast
77+
@subscribers_count = subscribers_count
78+
end
79+
80+
def clear_all
81+
schema_class.subscriptions.clear
82+
flash[:success] = "All subscription data cleared."
83+
redirect_to graphql_dashboard.subscriptions_topics_path
84+
end
85+
end
86+
end
87+
end
88+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<% content_for(:title, "Subscriptions") %>
2+
3+
<div class="row">
4+
<div class="col-md col-lg-8 mx-auto pt-4">
5+
<div class="card mt-4">
6+
<div class="card-body">
7+
<div class="card-title">
8+
<h2>
9+
GraphQL-Pro Subscriptions aren't installed on this schema yet.
10+
</h2>
11+
</div>
12+
<p class="card-text">
13+
Deliver live updates over <%= link_to "Pusher", "https://graphql-ruby.org/subscriptions/pusher_implementation.html" %> or <%= link_to "Ably", "https://graphql-ruby.org/subscriptions/ably_implementation.html" %>
14+
with GraphQL-Pro's subscription integrations.
15+
</p>
16+
</div>
17+
</div>
18+
</div>
19+
</div>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<% content_for(:title, "Subscription #{params[:id]}") %>
2+
<div class="row mt-3">
3+
<div class="col">
4+
<h3>Subscription: <code><%= params[:id] %></code></h3>
5+
</div>
6+
</div>
7+
8+
<% if @query_data.nil? %>
9+
<div class="row">
10+
<div class="col">
11+
<p class="muted"><i>This subscription was not found or is no longer active.</i></p>
12+
</div>
13+
</div>
14+
<% else %>
15+
<div class="row">
16+
<div class="col">
17+
<p>Created at <%= @query_data[:created_at] %>, last triggered at <%= @query_data[:last_triggered_at] || "--" %></p>
18+
19+
<p>Subscribed? <code><%= @still_subscribed ? "YES" : "NO" %></code></p>
20+
<p>Broadcast? <code><%= @is_broadcast ? "YES" : "NO" %></code> <% if @is_broadcast %>
21+
<small class="muted"><% if @subscribers_count.nil? %>
22+
This subscription may have multiple subscribers.
23+
<% else %>
24+
(<%= pluralize(@subscribers_count, "subscriber") %>)
25+
<% end %></small>
26+
<% end %></p>
27+
28+
<p>Context:</p>
29+
<pre><%= @query_data[:context].inspect %></pre>
30+
31+
<p>Variables:</p>
32+
<pre><%= @query_data[:variables].inspect %></pre>
33+
34+
<p>Operation Name:</p>
35+
<pre><%= @query_data[:operation_name].inspect %></pre>
36+
37+
<p>Query String:</p>
38+
<%= textarea_tag "_source", @query_data[:query_string], class: "graphql-highlight form-control", disabled: true, rows: @query_data[:query_string].count("\n") + 1 %>
39+
</div>
40+
</div>
41+
<% end %>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<% content_for(:title, "Subscriptions - Topics") %>
2+
<div class="row mt-3 justify-content-between">
3+
<div class="col-auto">
4+
<h3>
5+
<%= pluralize(@all_topics_count, "Subscription Topic") %>
6+
</h3>
7+
</div>
8+
<div class="col-auto">
9+
<form style="margin-left: auto;" action="#" method="post">
10+
<button type="submit" class="btn btn-outline-danger"
11+
onClick='return confirm("This will:\n\n- Remove all subscriptions from the database\n- Stop updates to all current subscribers\n\nAre you sure?")'
12+
>
13+
Reset
14+
</button>
15+
</form>
16+
</div>
17+
</div>
18+
19+
<table class="table table-striped">
20+
<thead>
21+
<tr>
22+
<th>Name</th>
23+
<th># Subscriptions</th>
24+
<th>Last Triggered At</th>
25+
</tr>
26+
</thead>
27+
<tbody>
28+
<% if @all_topics_count == 0 %>
29+
<tr>
30+
<td colspan="3" class="text-center">
31+
<em>There aren't any subscriptions right now.</em>
32+
</td>
33+
</tr>
34+
<% else %>
35+
<% @topics.each do |topic| %>
36+
<tr>
37+
<td><%= link_to(topic.name, graphql_dashboard.subscriptions_topic_path(name: topic.name)) %></td>
38+
<td><%= topic.subscriptions_count %></td>
39+
<td><%= topic.last_triggered_at || "--" %></td>
40+
</tr>
41+
<% end %>
42+
<% end %>
43+
</tbody>
44+
</table>
45+
46+
<div class="row">
47+
<div class="col-auto">
48+
<% if @page > 1 %>
49+
<%= link_to("« prev", graphql_dashboard.subscriptions_topics_path(per_page: params[:per_page], page: @page - 1), class: "btn btn-outline-secondary") %>
50+
<% else %>
51+
<button class="btn btn-outline-secondary" disabled>« prev</button>
52+
<% end %>
53+
</div>
54+
<div class="col-auto">
55+
<% if @has_next_page %>
56+
<%= link_to("next »", graphql_dashboard.subscriptions_topics_path(per_page: params[:per_page], page: @page + 1), class: "btn btn-outline-secondary") %>
57+
<% else %>
58+
<button class="btn btn-outline-secondary" disabled>next »</button>
59+
<% end %>
60+
</div>
61+
</div>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<%= content_for(:title, "Subscriptions - #{params[:name]}") %>
2+
<div class="row mt-3">
3+
<div class="col">
4+
<h3>Topic: <code><%= params[:name] %></code></h3>
5+
</div>
6+
</div>
7+
8+
<div class="row">
9+
<div class="col">
10+
<p>Last triggered: <%= @topic_last_triggered_at || "none" %></p>
11+
<p><%= pluralize(@subscriptions_count, "Subscription") %></p>
12+
<div>
13+
</div>
14+
15+
<div class="row">
16+
<div class="col">
17+
<table class="table table-striped">
18+
<thead>
19+
<tr>
20+
<th>Subscription ID</th>
21+
<th>Created At</th>
22+
<th>Subscribed?</th>
23+
<th>Broadcast?</th>
24+
<% if @show_broadcast_subscribers_count %><th>Subscribers</th><% end %>
25+
</tr>
26+
</thead>
27+
<tbody>
28+
<% @subscriptions.each do |subscription| %>
29+
<tr>
30+
<td><%= link_to(subscription[:id], graphql_dashboard.subscriptions_subscription_path(subscription[:id])) %></td>
31+
<td><%= subscription[:created_at] %></td>
32+
<td><code><%= subscription[:still_subscribed] ? "YES" : "NO" %></code></td>
33+
<td><code><%= subscription[:is_broadcast] ? "YES" : "NO" %></code></td>
34+
<% if @show_broadcast_subscribers_count %><td><%= subscription[:subscribers_count] %></td><% end %>
35+
</tr>
36+
<% end %>
37+
</tbody>
38+
</table>
39+
</div>
40+
</div>

lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@
2929
<li class="nav-item">
3030
<%= link_to "Traces", graphql_dashboard.traces_path, class: "nav-link #{params[:controller] == "graphql/dashboard/traces" ? "active" : ""}" %>
3131
</li>
32-
<li class="nav-item">
33-
</li>
3432
<li class="nav-item dropdown">
3533
<a class="nav-link dropdown-toggle <%= params[:controller].include?("operation_store") ? "active" : "" %>" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
3634
OperationStore
@@ -47,6 +45,9 @@
4745
</li>
4846
</ul>
4947
</li>
48+
<li class="nav-item">
49+
<%= link_to "Subscriptions", graphql_dashboard.subscriptions_topics_path, class: "nav-link #{params[:controller] == "graphql/dashboard/subscriptions" ? "active" : ""}" %>
50+
</li>
5051

5152
</ul>
5253
<span class="navbar-text pe-2">

spec/dummy/app/graphql/dummy_schema.rb

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,30 @@ class DummySchema < GraphQL::Schema
99
class Query < GraphQL::Schema::Object
1010
field :str, String, fallback_value: "hello"
1111
end
12-
1312
query(Query)
13+
14+
class Subscription < GraphQL::Schema::Object
15+
field :message, String do
16+
argument :channel, String
17+
end
18+
end
19+
subscription(Subscription)
20+
1421
use GraphQL::Tracing::DetailedTrace, memory: true
1522

1623
if defined?(GraphQL::Pro)
17-
use GraphQL::Pro::OperationStore, redis: Redis.new(db: Rails.env.test? ? 1 : 0)
24+
DB_NUMBER = Rails.env.test? ? 1 : 0
25+
use GraphQL::Pro::OperationStore, redis: Redis.new(db: DB_NUMBER)
26+
use GraphQL::Pro::PusherSubscriptions, redis: Redis.new(db: DummySchema::DB_NUMBER), pusher: MockPusher.new
1827
end
1928

2029
def self.detailed_trace?(query)
2130
query.context[:profile]
2231
end
2332
end
33+
34+
# To preview subscription data in the dashboard:
35+
# DummySchema.subscriptions.clear
36+
# res1 = DummySchema.execute("subscription { message(channel: \"cats\") }")
37+
# res2 = DummySchema.execute("subscription { message(channel: \"dogs\") }")
38+
# DummySchema.subscriptions.trigger(:message, { channel: "cats" }, "meow")

0 commit comments

Comments
 (0)