1
0

dot_helper.rb 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. module DotHelper
  2. def render_agents_diagram(agents, layout: nil)
  3. if (command = ENV['USE_GRAPHVIZ_DOT']) &&
  4. (svg = IO.popen([command, *%w[-Tsvg -q1 -o/dev/stdout /dev/stdin]], 'w+') { |dot|
  5. dot.print agents_dot(agents, rich: true, layout: layout)
  6. dot.close_write
  7. dot.read
  8. } rescue false)
  9. decorate_svg(svg, agents).html_safe
  10. else
  11. # Google chart request url
  12. faraday = Faraday.new { |builder|
  13. builder.request :url_encoded
  14. builder.adapter Faraday.default_adapter
  15. }
  16. response = faraday.post('https://chart.googleapis.com/chart', { cht: 'gv', chl: agents_dot(agents) })
  17. case response.status
  18. when 200
  19. # Display Base64-Encoded images
  20. tag('img', src: 'data:image/jpg;base64,'+Base64.encode64(response.body))
  21. when 400
  22. "The diagram can't be displayed because it has too many nodes. Max allowed is 80."
  23. when 413
  24. "The diagram can't be displayed because it is too large."
  25. else
  26. "Unknow error. Response code is #{response.status}."
  27. end
  28. end
  29. end
  30. class DotDrawer
  31. def initialize(vars = {})
  32. @dot = ''
  33. @vars = vars.symbolize_keys
  34. end
  35. def method_missing(var, *args)
  36. @vars.fetch(var) { super }
  37. end
  38. def to_s
  39. @dot
  40. end
  41. def self.draw(*args, &block)
  42. drawer = new(*args)
  43. drawer.instance_exec(&block)
  44. drawer.to_s
  45. end
  46. def raw(string)
  47. @dot << string
  48. end
  49. ENDL = ';'.freeze
  50. def endl
  51. @dot << ENDL
  52. end
  53. def escape(string)
  54. # Backslash escaping seems to work for the backslash itself,
  55. # though it's not documented in the DOT language docs.
  56. string.gsub(/[\\"\n]/,
  57. "\\" => "\\\\",
  58. "\"" => "\\\"",
  59. "\n" => "\\n")
  60. end
  61. def id(value)
  62. case string = value.to_s
  63. when /\A(?!\d)\w+\z/, /\A(?:\.\d+|\d+(?:\.\d*)?)\z/
  64. raw string
  65. else
  66. raw '"'
  67. raw escape(string)
  68. raw '"'
  69. end
  70. end
  71. def ids(values)
  72. values.each_with_index { |id, i|
  73. raw ' ' if i > 0
  74. id id
  75. }
  76. end
  77. def attr_list(attrs = nil)
  78. return if attrs.nil?
  79. attrs = attrs.select { |key, value| value.present? }
  80. return if attrs.empty?
  81. raw '['
  82. attrs.each_with_index { |(key, value), i|
  83. raw ',' if i > 0
  84. id key
  85. raw '='
  86. id value
  87. }
  88. raw ']'
  89. end
  90. def node(id, attrs = nil)
  91. id id
  92. attr_list attrs
  93. endl
  94. end
  95. def edge(from, to, attrs = nil, op = '->')
  96. id from
  97. raw op
  98. id to
  99. attr_list attrs
  100. endl
  101. end
  102. def statement(ids, attrs = nil)
  103. ids Array(ids)
  104. attr_list attrs
  105. endl
  106. end
  107. def block(*ids, &block)
  108. ids ids
  109. raw '{'
  110. block.call
  111. raw '}'
  112. end
  113. end
  114. private
  115. def draw(vars = {}, &block)
  116. DotDrawer.draw(vars, &block)
  117. end
  118. def agents_dot(agents, rich: false, layout: nil)
  119. draw(agents: agents,
  120. agent_id: ->agent { 'a%d' % agent.id },
  121. agent_label: ->agent {
  122. agent.name.gsub(/(.{20}\S*)\s+/) {
  123. # Fold after every 20+ characters
  124. $1 + "\n"
  125. }
  126. },
  127. agent_url: ->agent { agent_path(agent.id) },
  128. rich: rich) {
  129. @disabled = '#999999'
  130. def agent_node(agent)
  131. node(agent_id[agent],
  132. label: agent_label[agent],
  133. tooltip: (agent.short_type.titleize if rich),
  134. URL: (agent_url[agent] if rich),
  135. style: ('rounded,dashed' if agent.unavailable?),
  136. color: (@disabled if agent.unavailable?),
  137. fontcolor: (@disabled if agent.unavailable?))
  138. end
  139. def agent_edge(agent, receiver)
  140. edge(agent_id[agent],
  141. agent_id[receiver],
  142. style: ('dashed' unless receiver.propagate_immediately?),
  143. label: (" #{agent.control_action}s " if agent.can_control_other_agents?),
  144. arrowhead: ('empty' if agent.can_control_other_agents?),
  145. color: (@disabled if agent.unavailable? || receiver.unavailable?))
  146. end
  147. block('digraph', 'Agent Event Flow') {
  148. layout ||= ENV['DIAGRAM_DEFAULT_LAYOUT'].presence
  149. if rich && /\A[a-z]+\z/ === layout
  150. statement 'graph', layout: layout, overlap: 'false'
  151. end
  152. statement 'node',
  153. shape: 'box',
  154. style: 'rounded',
  155. target: '_blank',
  156. fontsize: 10,
  157. fontname: ('Helvetica' if rich)
  158. statement 'edge',
  159. fontsize: 10,
  160. fontname: ('Helvetica' if rich)
  161. agents.each.with_index { |agent, index|
  162. agent_node(agent)
  163. [
  164. *agent.receivers,
  165. *(agent.control_targets if agent.can_control_other_agents?)
  166. ].each { |receiver|
  167. agent_edge(agent, receiver) if agents.include?(receiver)
  168. }
  169. }
  170. }
  171. }
  172. end
  173. def decorate_svg(xml, agents)
  174. svg = Nokogiri::XML(xml).at('svg')
  175. Nokogiri::HTML::Document.new.tap { |doc|
  176. doc << root = Nokogiri::XML::Node.new('div', doc) { |div|
  177. div['class'] = 'agent-diagram'
  178. }
  179. svg['class'] = 'diagram'
  180. root << svg
  181. root << overlay_container = Nokogiri::XML::Node.new('div', doc) { |div|
  182. div['class'] = 'overlay-container'
  183. }
  184. overlay_container << overlay = Nokogiri::XML::Node.new('div', doc) { |div|
  185. div['class'] = 'overlay'
  186. }
  187. svg.xpath('//xmlns:g[@class="node"]', svg.namespaces).each { |node|
  188. agent_id = (node.xpath('./xmlns:title/text()', svg.namespaces).to_s[/\d+/] or next).to_i
  189. agent = agents.find { |a| a.id == agent_id }
  190. count = agent.events_count
  191. next unless count && count > 0
  192. overlay << Nokogiri::XML::Node.new('a', doc) { |badge|
  193. badge['id'] = id = 'b%d' % agent_id
  194. badge['class'] = 'badge'
  195. badge['href'] = agent_events_path(agent)
  196. badge['target'] = '_blank'
  197. badge['title'] = "#{count} events created"
  198. badge.content = count.to_s
  199. node['data-badge-id'] = id
  200. badge << Nokogiri::XML::Node.new('span', doc) { |label|
  201. # a dummy label only to obtain the background color
  202. label['class'] = [
  203. 'label',
  204. if agent.unavailable?
  205. 'label-warning'
  206. elsif agent.working?
  207. 'label-success'
  208. else
  209. 'label-danger'
  210. end
  211. ].join(' ')
  212. label['style'] = 'display: none';
  213. }
  214. }
  215. }
  216. # See also: app/assets/diagram.js.coffee
  217. }.at('div.agent-diagram').to_s
  218. end
  219. end