Skip to content

Commit

Permalink
Implemented asynchronous notifications using PatternFly & ActionCable
Browse files Browse the repository at this point in the history
  • Loading branch information
skateman committed Jun 27, 2016
1 parent 6477364 commit 36b5db2
Show file tree
Hide file tree
Showing 18 changed files with 160 additions and 4 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ bin/*

# config/
config/apache
config/cable.yml
config/database.yml*
config/vmdb.yml.db

Expand Down
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ gem "rails", "~> 5.0.x", :git => "git://github.com/rai
gem "rails-controller-testing", :require => false
gem "activemodel-serializers-xml", :require => false # required by draper: https://github.com/drapergem/draper/issues/697
gem "activerecord-session_store", "~>0.1.2", :require => false
gem "actioncable"
gem "coffee-rails"
gem "websocket-driver", "~>0.6.3"

gem "config", "~>1.1.0", :git => "git://github.com/ManageIQ/config.git", :branch => "overwrite_arrays"
Expand Down
1 change: 1 addition & 0 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,4 @@
//= require resizable_sidebar
//= require xml_display
//= require miq_c3
//= require cable
5 changes: 5 additions & 0 deletions app/assets/javascripts/cable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//= require action_cable
//= require_self
//= require_tree ./channels

ManageIQ.notifications = ActionCable.createConsumer('/ws/notifications');
45 changes: 45 additions & 0 deletions app/assets/javascripts/channels/notification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
ManageIQ.notification = ManageIQ.notifications.subscriptions.create("NotificationChannel", {
connected: function () {},
disconnected: function () {},
received: function (data) {
var _this = this;
var level2class = {
error: 'danger',
warning: 'warning',
info: 'info',
success: 'success'
};

var level2icon = {
error: 'error-circle-o',
warning: 'warning-triangle-o',
info: 'info',
success: 'ok'
};

var toast = $('<div>')
.addClass('toast-pf toast-pf-max-width toast-pf-top-right alert alert-dismissable col-xs-12')
.addClass('alert-' + level2class[data.level]);
var button = $('<div>')
.addClass('close')
.attr('type', 'button')
.data('dismiss', 'alert')
.attr('aria-hidden', true)
.append($('<span>').addClass('pficon pficon-close'));
var icon = $('<span>').addClass('pficon pficon-' + level2icon[data.level]);

toast.append(button, icon, data.message);
$('body').prepend(toast);

var dismissMessage = function () {
toast.remove();
};

button.click(function () {
dismissMessage();
_this.perform('mark', data);
});

setTimeout(dismissMessage, 3000);
}
});
1 change: 1 addition & 0 deletions app/assets/javascripts/miq_global.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,6 @@ if (! window.ManageIQ) {
processing: false, // is a request currently being processed?
queue: [], // a queue of pending requests
},
notifications: null, // asynchronous notifications endpoint
};
}
4 changes: 4 additions & 0 deletions app/channels/application_cable/channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
26 changes: 26 additions & 0 deletions app/channels/application_cable/connection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user

# TODO: what if the user is not logged in
def connect
self.current_user = find_verified_user
end

protected

# TODO: Do we need really to enter to the database?
def find_verified_user
User.find_by(:userid => userid_from_session)
end

# TODO: What if the session store is different?
def userid_from_session
cache = Vmdb::Application.config.session_options[:cache]
servers = cache.instance_variable_get(:@servers)
options = cache.instance_variable_get(:@options)
client = Dalli::Client.new(servers, options)
client.get(cookies['_vmdb_session'])['userid']
end
end
end
13 changes: 13 additions & 0 deletions app/channels/notification_channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class NotificationChannel < ApplicationCable::Channel
def subscribed
stream_from "notifications_#{current_user.id}" if current_user
end

def unsubscribed
# Any cleanup needed when channel is unsubscribed
end

def mark(data)
current_user.notifications.find(data['id'].to_i).update_attribute(:seen, true)
end
end
7 changes: 7 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,15 @@ class ApplicationController < ActionController::Base
before_action :get_global_session_data, :except => [:resize_layout, :window_sizes, :authenticate]
before_action :set_user_time_zone, :except => [:window_sizes]
before_action :set_gettext_locale, :except => [:window_sizes]
before_action :allow_websocket
after_action :set_global_session_data, :except => [:resize_layout, :window_sizes]

def allow_websocket
proto = request.ssl? ? 'wss' : 'ws'
override_content_security_policy_directives(:connect_src => ["'self'", "#{proto}://#{request.env['HTTP_HOST']}"])
end
private :allow_websocket

def reset_toolbar
@toolbars = {}
end
Expand Down
18 changes: 18 additions & 0 deletions app/models/notification.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class Notification < ApplicationRecord
belongs_to :user

after_commit :push_async, :on => :create

validates :level, :inclusion => %w(success info warning error), :presence => true
validates :message, :presence => true

scope :unread, -> { where(:seen => false) }

default_value_for :seen, false

private

def push_async
ActionCable.server.broadcast("notifications_#{user_id}", :id => id, :level => level, :message => message)
end
end
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class User < ApplicationRecord
has_many :miq_widget_sets, :as => :owner, :dependent => :destroy
has_many :miq_reports, :dependent => :nullify
has_many :service_orders, :dependent => :nullify
has_many :notifications, :dependent => :destroy
belongs_to :current_group, :class_name => "MiqGroup"
has_and_belongs_to_many :miq_groups
scope :admin, -> { where(:userid => "admin") }
Expand Down
3 changes: 3 additions & 0 deletions bin/setup
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ Dir.chdir APP_ROOT do
unless File.exist?("config/database.yml")
system "cp config/database.pg.yml config/database.yml"
end
unless File.exist?("config/cable.yml")
system "cp config/cable.yml.sample config/cable.yml"
end
unless File.exist?("certs/v2_key")
system "cp certs/v2_key.dev certs/v2_key"
end
Expand Down
1 change: 1 addition & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
require 'action_mailer/railtie'
require 'active_job/railtie'
require 'sprockets/railtie'
require 'action_cable/engine'

if defined?(Bundler)
Bundler.require *Rails.groups(:assets => %w(development test))
Expand Down
8 changes: 8 additions & 0 deletions config/cable.yml.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
production:
adapter: postgresql

development:
adapter: postgresql

test:
adapter: postgresql
12 changes: 12 additions & 0 deletions db/migrate/20160623080805_create_notifications.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class CreateNotifications < ActiveRecord::Migration[5.0]
def change
create_table :notifications do |t|
t.references :user, :foreign_key => true, :type => :bigint
t.string :level
t.text :message
t.boolean :seen

t.timestamps
end
end
end
11 changes: 7 additions & 4 deletions lib/websocket_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,13 @@ def initialize(options = {})

def call(env)
if WebSocket::Driver.websocket?(env)
exp = %r{^/ws/console/([a-zA-Z0-9]+)/?$}.match(env['REQUEST_URI'])
return not_found if exp.nil?

init_proxy(env, exp[1])
if env['REQUEST_URI'] =~ %r{^/ws/notifications}
ActionCable.server.call(env)
else
exp = %r{^/ws/console/([a-zA-Z0-9]+)/?$}.match(env['REQUEST_URI'])
return not_found if exp.nil?
init_proxy(env, exp[1])
end

[-1, {}, []]
else
Expand Down
5 changes: 5 additions & 0 deletions spec/models/notification_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
require 'rails_helper'

RSpec.describe Notification, :type => :model do
pending "add some examples to (or delete) #{__FILE__}"
end

0 comments on commit 36b5db2

Please sign in to comment.