dot_helper.rb 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. module DotHelper
  2. def render_agents_diagram(agents)
  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, true)
  6. dot.close_write
  7. dot.read
  8. } rescue false)
  9. decorate_svg(svg, agents).html_safe
  10. else
  11. tag('img', src: URI('https://chart.googleapis.com/chart').tap { |uri|
  12. uri.query = URI.encode_www_form(cht: 'gv', chl: agents_dot(agents))
  13. })
  14. end
  15. end
  16. class DotDrawer
  17. def initialize(vars = {})
  18. @dot = ''
  19. vars.each { |name, value|
  20. # Import variables as methods
  21. define_singleton_method(name) { value }
  22. }
  23. end
  24. def to_s
  25. @dot
  26. end
  27. def self.draw(*args, &block)
  28. drawer = new(*args)
  29. drawer.instance_exec(&block)
  30. drawer.to_s
  31. end
  32. def raw(string)
  33. @dot << string
  34. end
  35. def escape(string)
  36. # Backslash escaping seems to work for the backslash itself,
  37. # though it's not documented in the DOT language docs.
  38. string.gsub(/[\\"\n]/,
  39. "\\" => "\\\\",
  40. "\"" => "\\\"",
  41. "\n" => "\\n")
  42. end
  43. def id(value)
  44. case string = value.to_s
  45. when /\A(?!\d)\w+\z/, /\A(?:\.\d+|\d+(?:\.\d*)?)\z/
  46. raw string
  47. else
  48. raw '"'
  49. raw escape(string)
  50. raw '"'
  51. end
  52. end
  53. def ids(values)
  54. values.each_with_index { |id, i|
  55. raw ' ' if i > 0
  56. id id
  57. }
  58. end
  59. def attr_list(attrs = nil)
  60. return if attrs.nil?
  61. attrs = attrs.select { |key, value| value.present? }
  62. return if attrs.empty?
  63. raw '['
  64. attrs.each_with_index { |(key, value), i|
  65. raw ',' if i > 0
  66. id key
  67. raw '='
  68. id value
  69. }
  70. raw ']'
  71. end
  72. def node(id, attrs = nil)
  73. id id
  74. attr_list attrs
  75. raw ';'
  76. end
  77. def edge(from, to, attrs = nil, op = '->')
  78. id from
  79. raw op
  80. id to
  81. attr_list attrs
  82. raw ';'
  83. end
  84. def statement(ids, attrs = nil)
  85. ids Array(ids)
  86. attr_list attrs
  87. raw ';'
  88. end
  89. def block(*ids, &block)
  90. ids ids
  91. raw '{'
  92. block.call
  93. raw '}'
  94. end
  95. end
  96. private
  97. def draw(vars = {}, &block)
  98. DotDrawer.draw(vars, &block)
  99. end
  100. def agents_dot(agents, rich = false)
  101. draw(agents: agents,
  102. agent_id: ->agent { 'a%d' % agent.id },
  103. agent_label: ->agent {
  104. agent.name.gsub(/(.{20}\S*)\s+/) {
  105. # Fold after every 20+ characters
  106. $1 + "\n"
  107. }
  108. },
  109. agent_url: ->agent { agent_path(agent.id) },
  110. rich: rich) {
  111. @disabled = '#999999'
  112. def agent_node(agent)
  113. node(agent_id[agent],
  114. label: agent_label[agent],
  115. tooltip: (agent.short_type.titleize if rich),
  116. URL: (agent_url[agent] if rich),
  117. style: ('rounded,dashed' if agent.disabled?),
  118. color: (@disabled if agent.disabled?),
  119. fontcolor: (@disabled if agent.disabled?))
  120. end
  121. def agent_edge(agent, receiver)
  122. edge(agent_id[agent],
  123. agent_id[receiver],
  124. style: ('dashed' unless agent.can_run_other_agents? || !receiver.propagate_immediately?),
  125. label: (' runs ' if agent.can_run_other_agents?),
  126. arrowhead: ('empty' if agent.can_run_other_agents?),
  127. color: (@disabled if agent.disabled? || receiver.disabled?))
  128. end
  129. block('digraph', 'Agent Event Flow') {
  130. # statement 'graph', rankdir: 'LR'
  131. statement 'node',
  132. shape: 'box',
  133. style: 'rounded',
  134. target: '_blank',
  135. fontsize: 10,
  136. fontname: ('Helvetica' if rich)
  137. statement 'edge',
  138. fontsize: 10,
  139. fontname: ('Helvetica' if rich)
  140. agents.each.with_index { |agent, index|
  141. agent_node(agent)
  142. [
  143. *agent.receivers,
  144. *(agent.targets if agent.can_run_other_agents?)
  145. ].each { |receiver|
  146. agent_edge(agent, receiver) if agents.include?(receiver)
  147. }
  148. }
  149. }
  150. }
  151. end
  152. def decorate_svg(xml, agents)
  153. svg = Nokogiri::XML(xml).at('svg')
  154. Nokogiri::HTML::Document.new.tap { |doc|
  155. doc << root = Nokogiri::XML::Node.new('div', doc) { |div|
  156. div['class'] = 'agent-diagram'
  157. }
  158. svg['class'] = 'diagram'
  159. root << svg
  160. root << overlay_container = Nokogiri::XML::Node.new('div', doc) { |div|
  161. div['class'] = 'overlay-container'
  162. div['style'] = "width: #{svg['width']}; height: #{svg['height']}"
  163. }
  164. overlay_container << overlay = Nokogiri::XML::Node.new('div', doc) { |div|
  165. div['class'] = 'overlay'
  166. }
  167. svg.xpath('//xmlns:g[@class="node"]', svg.namespaces).each { |node|
  168. agent_id = (node.xpath('./xmlns:title/text()', svg.namespaces).to_s[/\d+/] or next).to_i
  169. agent = agents.find { |a| a.id == agent_id }
  170. count = agent.events_count
  171. next unless count && count > 0
  172. overlay << Nokogiri::XML::Node.new('a', doc) { |badge|
  173. badge['id'] = id = 'b%d' % agent_id
  174. badge['class'] = 'badge'
  175. badge['href'] = agent_events_path(agent)
  176. badge['target'] = '_blank'
  177. badge['title'] = "#{count} events created"
  178. badge.content = count.to_s
  179. node['data-badge-id'] = id
  180. badge << Nokogiri::XML::Node.new('span', doc) { |label|
  181. # a dummy label only to obtain the background color
  182. label['class'] = [
  183. 'label',
  184. if agent.disabled?
  185. 'label-warning'
  186. elsif agent.working?
  187. 'label-success'
  188. else
  189. 'label-danger'
  190. end
  191. ].join(' ')
  192. label['style'] = 'display: none';
  193. }
  194. }
  195. }
  196. # See also: app/assets/diagram.js.coffee
  197. }.at('div.agent-diagram').to_s
  198. end
  199. end