123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- module DotHelper
- def render_agents_diagram(agents)
- if (command = ENV['USE_GRAPHVIZ_DOT']) &&
- (svg = IO.popen([command, *%w[-Tsvg -q1 -o/dev/stdout /dev/stdin]], 'w+') { |dot|
- dot.print agents_dot(agents, true)
- dot.close_write
- dot.read
- } rescue false)
- decorate_svg(svg, agents).html_safe
- else
- uriquery = URI.encode_www_form(cht: 'gv', chl: agents_dot(agents))
- #Get query maximum length should be under 2048 bytes with including "chart?" of google chart request url
- if uriquery.length > 2042
- "Too many agent to display, please check unused agents"
- else
- tag('img', src: URI('https://chart.googleapis.com/chart').tap { |uri|
- uri.query = uriquery
- })
- end
- end
- end
- class DotDrawer
- def initialize(vars = {})
- @dot = ''
- @vars = vars.symbolize_keys
- end
- def method_missing(var, *args)
- @vars.fetch(var) { super }
- end
- def to_s
- @dot
- end
- def self.draw(*args, &block)
- drawer = new(*args)
- drawer.instance_exec(&block)
- drawer.to_s
- end
- def raw(string)
- @dot << string
- end
- ENDL = ';'.freeze
- def endl
- @dot << ENDL
- end
- def escape(string)
- # Backslash escaping seems to work for the backslash itself,
- # though it's not documented in the DOT language docs.
- string.gsub(/[\\"\n]/,
- "\\" => "\\\\",
- "\"" => "\\\"",
- "\n" => "\\n")
- end
- def id(value)
- case string = value.to_s
- when /\A(?!\d)\w+\z/, /\A(?:\.\d+|\d+(?:\.\d*)?)\z/
- raw string
- else
- raw '"'
- raw escape(string)
- raw '"'
- end
- end
- def ids(values)
- values.each_with_index { |id, i|
- raw ' ' if i > 0
- id id
- }
- end
- def attr_list(attrs = nil)
- return if attrs.nil?
- attrs = attrs.select { |key, value| value.present? }
- return if attrs.empty?
- raw '['
- attrs.each_with_index { |(key, value), i|
- raw ',' if i > 0
- id key
- raw '='
- id value
- }
- raw ']'
- end
- def node(id, attrs = nil)
- id id
- attr_list attrs
- endl
- end
- def edge(from, to, attrs = nil, op = '->')
- id from
- raw op
- id to
- attr_list attrs
- endl
- end
- def statement(ids, attrs = nil)
- ids Array(ids)
- attr_list attrs
- endl
- end
- def block(*ids, &block)
- ids ids
- raw '{'
- block.call
- raw '}'
- end
- end
- private
- def draw(vars = {}, &block)
- DotDrawer.draw(vars, &block)
- end
- def agents_dot(agents, rich = false)
- draw(agents: agents,
- agent_id: ->agent { 'a%d' % agent.id },
- agent_label: ->agent {
- agent.name.gsub(/(.{20}\S*)\s+/) {
- # Fold after every 20+ characters
- $1 + "\n"
- }
- },
- agent_url: ->agent { agent_path(agent.id) },
- rich: rich) {
- @disabled = '#999999'
- def agent_node(agent)
- node(agent_id[agent],
- label: agent_label[agent],
- tooltip: (agent.short_type.titleize if rich),
- URL: (agent_url[agent] if rich),
- style: ('rounded,dashed' if agent.unavailable?),
- color: (@disabled if agent.unavailable?),
- fontcolor: (@disabled if agent.unavailable?))
- end
- def agent_edge(agent, receiver)
- edge(agent_id[agent],
- agent_id[receiver],
- style: ('dashed' unless receiver.propagate_immediately?),
- label: (" #{agent.control_action}s " if agent.can_control_other_agents?),
- arrowhead: ('empty' if agent.can_control_other_agents?),
- color: (@disabled if agent.unavailable? || receiver.unavailable?))
- end
- block('digraph', 'Agent Event Flow') {
- # statement 'graph', rankdir: 'LR'
- statement 'node',
- shape: 'box',
- style: 'rounded',
- target: '_blank',
- fontsize: 10,
- fontname: ('Helvetica' if rich)
- statement 'edge',
- fontsize: 10,
- fontname: ('Helvetica' if rich)
- agents.each.with_index { |agent, index|
- agent_node(agent)
- [
- *agent.receivers,
- *(agent.control_targets if agent.can_control_other_agents?)
- ].each { |receiver|
- agent_edge(agent, receiver) if agents.include?(receiver)
- }
- }
- }
- }
- end
- def decorate_svg(xml, agents)
- svg = Nokogiri::XML(xml).at('svg')
- Nokogiri::HTML::Document.new.tap { |doc|
- doc << root = Nokogiri::XML::Node.new('div', doc) { |div|
- div['class'] = 'agent-diagram'
- }
- svg['class'] = 'diagram'
- root << svg
- root << overlay_container = Nokogiri::XML::Node.new('div', doc) { |div|
- div['class'] = 'overlay-container'
- div['style'] = "width: #{svg['width']}; height: #{svg['height']}"
- }
- overlay_container << overlay = Nokogiri::XML::Node.new('div', doc) { |div|
- div['class'] = 'overlay'
- }
- svg.xpath('//xmlns:g[@class="node"]', svg.namespaces).each { |node|
- agent_id = (node.xpath('./xmlns:title/text()', svg.namespaces).to_s[/\d+/] or next).to_i
- agent = agents.find { |a| a.id == agent_id }
- count = agent.events_count
- next unless count && count > 0
- overlay << Nokogiri::XML::Node.new('a', doc) { |badge|
- badge['id'] = id = 'b%d' % agent_id
- badge['class'] = 'badge'
- badge['href'] = agent_events_path(agent)
- badge['target'] = '_blank'
- badge['title'] = "#{count} events created"
- badge.content = count.to_s
- node['data-badge-id'] = id
- badge << Nokogiri::XML::Node.new('span', doc) { |label|
- # a dummy label only to obtain the background color
- label['class'] = [
- 'label',
- if agent.unavailable?
- 'label-warning'
- elsif agent.working?
- 'label-success'
- else
- 'label-danger'
- end
- ].join(' ')
- label['style'] = 'display: none';
- }
- }
- }
- # See also: app/assets/diagram.js.coffee
- }.at('div.agent-diagram').to_s
- end
- end
|