dot_helper.rb 6.1 KB

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