Extended example with graphviz updates via Websockets

Native Websockets support is a very compelling reason to use Cowboy. (Yaws also supports Websockets natively as well so it is another alternative).

I will give a simple example how to use Websockets with Nitrogen and Cowboy by extending on my blog post which showed how to render static graphviz graphs.

This time we will use Websockets to push the graph updates to the browser from the server back-end.

First, you need to tell Cowboy what handler will be processing your Websocket requests. You can do this with adding a route to the dispatch table:

dispatch_rules() ->
    cowboy_router:compile(
	%% {Host, list({Path, Handler, Opts})}
	[{'_', [
	    {["/favicon.ico"], cowboy_static, [{directory, {priv_dir, ?APP, [<<"static">>]}}, {file, "favicon.ico"}]},
	    {["/content/[...]"], cowboy_static, [{directory, {priv_dir, ?APP, [<<"content">>]}},
		{mimetypes, {fun mimetypes:path_to_mimes/2, default}}]},
	    {["/static/[...]"], cowboy_static, [{directory, {priv_dir, ?APP, [<<"static">>]}},
		{mimetypes, {fun mimetypes:path_to_mimes/2, default}}]},
	    {["/plugins/[...]"], cowboy_static, [{directory, {priv_dir, ?APP, [<<"plugins">>]}},
		{mimetypes, {fun mimetypes:path_to_mimes/2, default}}]},
	    {["/doc/[...]"], cowboy_static, [{directory, {priv_dir, ?APP, [<<"doc">>]}},
		{mimetypes, {fun mimetypes:path_to_mimes/2, default}}]},
	    {["/get_jqgrid_data/[...]"], get_jqgrid_data, []},
	    {["/websocket"], ws_handler, []},
	    {["/jqdata"], jqdata, []},
	    {'_', nitrogen_cowboy, []}
    ]}]).

In this case we are saying that requests to “websocket”, e.g. like “ws://localhost:8000/websocket”, will be handled by erlang module ws_handler.erl. Here is this module:

-module(ws_handler).
-behaviour(cowboy_websocket_handler).

-export([init/3]).
-export([websocket_init/3]).
-export([websocket_handle/3]).
-export([websocket_info/3]).
-export([websocket_terminate/3]).

-define(DATA, <<"digraph G {subgraph cluster_0 {style=filled;color=lightgrey; node [style=filled,color=yellow]; a0 -> a1 -> a2 -> a3;label = \"process #1\";} subgraph cluster_1 {node [style=filled]; b0 -> b1 -> b2 -> b3; label = \"process #2\";color=red} start -> a0; start -> b0; a1 -> b3; b2 -> a3; a3 -> a0; a3 -> end; b3 -> end; start [shape=Mdiamond]; end [shape=Msquare];}">>).

-define(DATA1, <<"digraph G {subgraph cluster_0 {style=filled;color=lightgrey; node [style=filled,color=blue]; a0 -> a1 -> a2 -> a3 -> a4;label = \"process #1\";} subgraph cluster_1 {node [style=filled]; b0 -> b1 -> b2 -> b3 -> b4; label = \"process #2\";color=red} start -> a0; start -> b0; a1 -> b3; b2 -> a3; a3 -> a0; a3 -> a4; a4 -> end; b3 -> end; start [shape=Mdiamond]; end [shape=Msquare];}">>).

-define(DATA2, <<"digraph G {subgraph cluster_0 {style=filled;color=lightgrey; node [style=filled,color=red]; a0 -> a1 -> a2 -> a3 -> a4;label = \"process #1\";} subgraph cluster_1 {node [style=filled]; b0 -> b1 -> b2 -> b3 -> b4; label = \"process #2\";color=red} start -> a0; start -> b0; a1 -> b3; b2 -> a3; a3 -> a0; a3 -> a4; a4 -> end; b3 -> end; start [shape=Mdiamond]; end [shape=Msquare];}">>).

-define(G, [?DATA, ?DATA1, ?DATA2]).

init({tcp, http}, _Req, _Opts) ->
    {upgrade, protocol, cowboy_websocket}.

websocket_init(_TransportName, Req, _Opts) ->
    erlang:start_timer(1000, self(), lists:nth(random:uniform(3), ?G)),
    {ok, Req, undefined_state}.

websocket_handle({text, Msg}, Req, State) ->
    {reply, {text, << "", Msg/binary >>}, Req, State};
websocket_handle(_Data, Req, State) ->
    {ok, Req, State}.

websocket_info({timeout, _Ref, Msg}, Req, State) ->
    erlang:start_timer(1000, self(), lists:nth(random:uniform(3), ?G)),
    {reply, {text, Msg}, Req, State};
websocket_info(_Info, Req, State) ->
    {ok, Req, State}.

websocket_terminate(_Reason, _Req, _State) ->
    ok.

here, for example purposes, we define three dot binary strings that specify the possible states that our graph could be in. Then we randomly select a state and push it to the client side in websocket_info function.

websocket_info/3 – is used to handle the events coming from the server side.
websocket_handle/3 – is used to handle messages coming from the browser side.

On the browser side we need to implement the callbacks to WebSocket API.
In general case, in javascript, you need to create an instance of WebSocket object and pass a connection string to it (e.g. “ws://localhost:8000/websocket”):

 websocket = new WebSocket(wsHost);
 websocket.onopen = function(evt) { onOpen(evt) }; 
 websocket.onclose = function(evt) { onClose(evt) }; 
 websocket.onmessage = function(evt) { onMessage(evt) }; 
 websocket.onerror = function(evt) { onError(evt) }; 

You can find a full pure javascript websocket example in the “Examples” dir which comes with Cowboy repo.

For Nitrogen I added a simple action_ws_api.erl module which implements a simple API for onopen / onclose / onmessage / onerror Websockets calls in Nitrogen style:

% simple websocket api
-module(action_ws_api).
-include("nitrogen_elements.hrl").
-compile(export_all).

render_action(#ws_open{server = Server, func = OnOpen}) ->
    [
     wf:f("$(function() { var websocket;
           websocket = new WebSocket('~s');window.websocket = websocket;", [Server]),
           on_open_script(OnOpen),
           ";})"
    ];
render_action(#ws_message{func = OnMessage}) -> on_message_script(OnMessage);
render_action(#ws_error{func = OnError}) -> on_error_script(OnError);
render_action(#ws_close{func = OnClose}) -> on_close_script(OnClose).

on_open_script("") ->
    "websocket.onopen = function(event){console.log('close');};";
on_open_script(OnOpen) ->
    wf:f("websocket.onopen = ~s", [OnOpen]).

on_close_script("") ->
    wf:f("$(function(){window.websocket.close();console.log('close');});");
on_close_script(OnClose) ->
    wf:f("websocket.onclose = ~s", [OnClose]).

on_message_script("") ->
    "window.websocket.onmessage = function(event){console.log(event.data)};";
on_message_script(OnMessage) ->
    wf:f("websocket.onmessage = ~s", [OnMessage]).

on_error_script("") ->
    "websocket.onerror = function(event){console.log(event.data)};";
on_error_script(OnError) ->
    wf:f("websocket.onerror = ~s", [OnError]).

The final part is adding all this to the erlang module which implements Nitrogen page:

-module(viz).

-include_lib("nitrogen_elements/include/nitrogen_elements.hrl").
-compile(export_all).

-define(DATA, <<"digraph G {subgraph cluster_0 {style=filled;color=lightgrey; node [style=filled,color=white]; a0 -> a1 -> a2 -> a3;label = \"process #1\";} subgraph cluster_1 {node [style=filled]; b0 -> b1 -> b2 -> b3; label = \"process #2\";color=blue} start -> a0; start -> b0; a1 -> b3; b2 -> a3; a3 -> a0; a3 -> end; b3 -> end; start [shape=Mdiamond]; end [shape=Msquare];}">>).

body(_Tag) ->
    %% output html markup
    [
	#viz{id = viz, data = ?DATA}
    ].

control_panel(Tag) ->
    #panel{id = control_panel, body = [
	#textbox{id = server_txt, style = "width: 100%", text = "ws://localhost:8000/websocket"},
	#p{},
	%% postback is to the index page, event from index.erl will call event(connect)
	#button{id = conn, text = "Connect", actions = [#event{type = click, postback = {Tag, connect}}]}
    ]}.

event({Tag, connect}) ->
    Server = wf:q(server_txt),
    ?PRINT({viz_server, Server}),
    wf:wire(#ws_open{server = Server, func = "function(event){console.log('open')};"}),
    wf:wire(#ws_message{func = wf:f("function(event){var g = jQuery(obj('~s'));
                                               g.html(Viz(event.data, \"svg\"));
	                                       g.find(\"svg\").width('100%');
	                                       g.find(\"svg\").graphviz({status: true});};", [viz])}),
    wf:wire(#ws_error{func = "function(event){console.log('error')};"}),
    wf:replace(conn, #button{id = conn, text = "Disconnect", actions = [#event{type = click, postback = {Tag, disconnect}}]});
event({Tag, disconnect}) ->
    wf:wire(#ws_close{}),
    wf:replace(conn, #button{id = conn, text = "Connect", actions = [#event{type = click, postback = {Tag, connect}}]});
event(Event) ->
    ?PRINT({viz_event, Event}).

Here, we are opening Websocket connection to the server and adding javascript onmessage handler which updates the graph:

  wf:wire(#ws_open{server = Server, func = "function(event){console.log('open')};"}),
    wf:wire(#ws_message{func = wf:f("function(event){var g = jQuery(obj('~s'));
                                               g.html(Viz(event.data, \"svg\"));
	                                       g.find(\"svg\").width('100%');
	                                       g.find(\"svg\").graphviz({status: true});};", [viz])}),

This is it, if you run this example you will see the initial graph state:

Screen Shot 2013-03-17 at 11.48.38

but if you press “Connect” button, you will open Websocket connection to the server and server will start generating new states for the graph and pushing them to the browser. This will trigger onMessage event for which we have a handler. This handler will get a dot string, calculate a new graph layout with Viz.js, generate a new svg from it and update a “svg” element with a new state. It will continue doing this until you press “Disconnect” which will close Websocket connection.

Screen Shot 2013-03-17 at 11.53.26

The full example is here.

Advertisements
This entry was posted in Cowboy, custom Nitrogen elements, Erlang, Nitrogen, Nitrogen_Elements and tagged , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s