dot_helper.rb 6.3 KB

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