@@ -0,0 +1,20 @@ |
||
1 |
+$ -> |
|
2 |
+ svg = document.querySelector('.agent-diagram svg.diagram') |
|
3 |
+ overlay = document.querySelector('.agent-diagram .overlay') |
|
4 |
+ getTopLeft = (node) -> |
|
5 |
+ bbox = node.getBBox() |
|
6 |
+ point = svg.createSVGPoint() |
|
7 |
+ point.x = bbox.x + bbox.width |
|
8 |
+ point.y = bbox.y |
|
9 |
+ point.matrixTransform(node.getCTM()) |
|
10 |
+ $(svg).find('g.node[data-badge-id]').each -> |
|
11 |
+ tl = getTopLeft(this) |
|
12 |
+ $('#' + this.getAttribute('data-badge-id'), overlay).each -> |
|
13 |
+ badge = $(this) |
|
14 |
+ badge.css |
|
15 |
+ left: tl.x - badge.outerWidth() * (2/3) |
|
16 |
+ top: tl.y - badge.outerHeight() * (1/3) |
|
17 |
+ 'background-color': badge.find('.label').css('background-color') |
|
18 |
+ .show() |
|
19 |
+ return |
|
20 |
+ return |
@@ -0,0 +1,30 @@ |
||
1 |
+.agent-diagram { |
|
2 |
+ position: relative; |
|
3 |
+ z-index: auto; |
|
4 |
+ |
|
5 |
+ svg.diagram { |
|
6 |
+ position: absolute; |
|
7 |
+ z-index: 1; |
|
8 |
+ } |
|
9 |
+ |
|
10 |
+ .overlay-container { |
|
11 |
+ position: absolute; |
|
12 |
+ top: 0; |
|
13 |
+ left: 0; |
|
14 |
+ z-index: auto; |
|
15 |
+ |
|
16 |
+ .overlay { |
|
17 |
+ position: relative; |
|
18 |
+ z-index: auto; |
|
19 |
+ width: 100%; |
|
20 |
+ height: 100%; |
|
21 |
+ |
|
22 |
+ .badge { |
|
23 |
+ position: absolute; |
|
24 |
+ display: none; |
|
25 |
+ color: white !important; |
|
26 |
+ z-index: 2; |
|
27 |
+ } |
|
28 |
+ } |
|
29 |
+ } |
|
30 |
+} |
@@ -6,7 +6,7 @@ module DotHelper |
||
6 | 6 |
dot.close_write |
7 | 7 |
dot.read |
8 | 8 |
} rescue false) |
9 |
- svg.html_safe |
|
9 |
+ decorate_svg(svg, agents).html_safe |
|
10 | 10 |
else |
11 | 11 |
tag('img', src: URI('https://chart.googleapis.com/chart').tap { |uri| |
12 | 12 |
uri.query = URI.encode_www_form(cht: 'gv', chl: agents_dot(agents)) |
@@ -57,6 +57,13 @@ module DotHelper |
||
57 | 57 |
end |
58 | 58 |
end |
59 | 59 |
|
60 |
+ def ids(values) |
|
61 |
+ values.each_with_index { |id, i| |
|
62 |
+ raw ' ' if i > 0 |
|
63 |
+ id id |
|
64 |
+ } |
|
65 |
+ end |
|
66 |
+ |
|
60 | 67 |
def attr_list(attrs = nil) |
61 | 68 |
return if attrs.nil? |
62 | 69 |
attrs = attrs.select { |key, value| value.present? } |
@@ -86,16 +93,13 @@ module DotHelper |
||
86 | 93 |
end |
87 | 94 |
|
88 | 95 |
def statement(ids, attrs = nil) |
89 |
- Array(ids).each_with_index { |id, i| |
|
90 |
- raw ' ' if i > 0 |
|
91 |
- id id |
|
92 |
- } |
|
96 |
+ ids Array(ids) |
|
93 | 97 |
attr_list attrs |
94 | 98 |
raw ';' |
95 | 99 |
end |
96 | 100 |
|
97 |
- def block(title, &block) |
|
98 |
- raw title |
|
101 |
+ def block(*ids, &block) |
|
102 |
+ ids ids |
|
99 | 103 |
raw '{' |
100 | 104 |
block.call |
101 | 105 |
raw '}' |
@@ -112,11 +116,7 @@ module DotHelper |
||
112 | 116 |
draw(agents: agents, |
113 | 117 |
agent_id: ->agent { 'a%d' % agent.id }, |
114 | 118 |
agent_label: ->agent { |
115 |
- if agent.disabled? |
|
116 |
- '%s (Disabled)' % agent.name |
|
117 |
- else |
|
118 |
- agent.name |
|
119 |
- end.gsub(/(.{20}\S*)\s+/) { |
|
119 |
+ agent.name.gsub(/(.{20}\S*)\s+/) { |
|
120 | 120 |
# Fold after every 20+ characters |
121 | 121 |
$1 + "\n" |
122 | 122 |
} |
@@ -128,6 +128,7 @@ module DotHelper |
||
128 | 128 |
def agent_node(agent) |
129 | 129 |
node(agent_id[agent], |
130 | 130 |
label: agent_label[agent], |
131 |
+ tooltip: (agent.short_type.titleize if rich), |
|
131 | 132 |
URL: (agent_url[agent] if rich), |
132 | 133 |
style: ('rounded,dashed' if agent.disabled?), |
133 | 134 |
color: (@disabled if agent.disabled?), |
@@ -141,7 +142,7 @@ module DotHelper |
||
141 | 142 |
color: (@disabled if agent.disabled? || receiver.disabled?)) |
142 | 143 |
end |
143 | 144 |
|
144 |
- block('digraph foo') { |
|
145 |
+ block('digraph', 'Agent Event Flow') { |
|
145 | 146 |
# statement 'graph', rankdir: 'LR' |
146 | 147 |
statement 'node', |
147 | 148 |
shape: 'box', |
@@ -160,4 +161,60 @@ module DotHelper |
||
160 | 161 |
} |
161 | 162 |
} |
162 | 163 |
end |
164 |
+ |
|
165 |
+ def decorate_svg(xml, agents) |
|
166 |
+ svg = Nokogiri::XML(xml).at('svg') |
|
167 |
+ |
|
168 |
+ Nokogiri::HTML::Document.new.tap { |doc| |
|
169 |
+ doc << root = Nokogiri::XML::Node.new('div', doc) { |div| |
|
170 |
+ div['class'] = 'agent-diagram' |
|
171 |
+ } |
|
172 |
+ |
|
173 |
+ svg['class'] = 'diagram' |
|
174 |
+ |
|
175 |
+ root << svg |
|
176 |
+ root << overlay_container = Nokogiri::XML::Node.new('div', doc) { |div| |
|
177 |
+ div['class'] = 'overlay-container' |
|
178 |
+ div['style'] = "width: #{svg['width']}; height: #{svg['height']}" |
|
179 |
+ } |
|
180 |
+ overlay_container << overlay = Nokogiri::XML::Node.new('div', doc) { |div| |
|
181 |
+ div['class'] = 'overlay' |
|
182 |
+ } |
|
183 |
+ |
|
184 |
+ svg.xpath('//xmlns:g[@class="node"]', svg.namespaces).each { |node| |
|
185 |
+ agent_id = (node.xpath('./xmlns:title/text()', svg.namespaces).to_s[/\d+/] or next).to_i |
|
186 |
+ agent = agents.find { |a| a.id == agent_id } |
|
187 |
+ |
|
188 |
+ count = agent.events_count |
|
189 |
+ next unless count && count > 0 |
|
190 |
+ |
|
191 |
+ overlay << Nokogiri::XML::Node.new('a', doc) { |badge| |
|
192 |
+ badge['id'] = id = 'b%d' % agent_id |
|
193 |
+ badge['class'] = 'badge' |
|
194 |
+ badge['href'] = events_path(agent: agent) |
|
195 |
+ badge['target'] = '_blank' |
|
196 |
+ badge['title'] = "#{count} events created" |
|
197 |
+ badge.content = count.to_s |
|
198 |
+ |
|
199 |
+ node['data-badge-id'] = id |
|
200 |
+ |
|
201 |
+ badge << Nokogiri::XML::Node.new('span', doc) { |label| |
|
202 |
+ # a dummy label only to obtain the background color |
|
203 |
+ label['class'] = [ |
|
204 |
+ 'label', |
|
205 |
+ if agent.disabled? |
|
206 |
+ 'label-warning' |
|
207 |
+ elsif agent.working? |
|
208 |
+ 'label-success' |
|
209 |
+ else |
|
210 |
+ 'label-danger' |
|
211 |
+ end |
|
212 |
+ ].join(' ') |
|
213 |
+ label['style'] = 'display: none'; |
|
214 |
+ } |
|
215 |
+ } |
|
216 |
+ } |
|
217 |
+ # See also: app/assets/diagram.js.coffee |
|
218 |
+ }.at('div.agent-diagram').to_s |
|
219 |
+ end |
|
163 | 220 |
end |
@@ -1,3 +1,7 @@ |
||
1 |
+<% content_for :head do %> |
|
2 |
+ <%= javascript_include_tag "diagram" %> |
|
3 |
+<% end %> |
|
4 |
+ |
|
1 | 5 |
<div class='container'> |
2 | 6 |
<div class='row'> |
3 | 7 |
<div class='col-md-12'> |
@@ -61,7 +61,7 @@ Huginn::Application.configure do |
||
61 | 61 |
end |
62 | 62 |
|
63 | 63 |
# Precompile additional assets (application.js.coffee.erb, application.css, and all non-JS/CSS are already added) |
64 |
- config.assets.precompile += %w( graphing.js user_credentials.js ) |
|
64 |
+ config.assets.precompile += %w( diagram.js graphing.js user_credentials.js ) |
|
65 | 65 |
|
66 | 66 |
# Ignore bad email addresses and do not raise email delivery errors. |
67 | 67 |
# Set this to true and configure the email server for immediate delivery to raise delivery errors. |
@@ -56,13 +56,13 @@ describe DotHelper do |
||
56 | 56 |
it "generates a DOT script" do |
57 | 57 |
agents_dot(@agents).should =~ %r{ |
58 | 58 |
\A |
59 |
- digraph \s foo \{ |
|
59 |
+ digraph \x20 "Agent \x20 Event \x20 Flow" \{ |
|
60 | 60 |
node \[ [^\]]+ \]; |
61 | 61 |
(?<foo>\w+) \[label=foo\]; |
62 | 62 |
\k<foo> -> (?<bar1>\w+) \[style=dashed\]; |
63 | 63 |
\k<foo> -> (?<bar2>\w+) \[color="\#999999"\]; |
64 | 64 |
\k<bar1> \[label=bar1\]; |
65 |
- \k<bar2> \[label="bar2 \s \(Disabled\)",style="rounded,dashed",color="\#999999",fontcolor="\#999999"\]; |
|
65 |
+ \k<bar2> \[label=bar2,style="rounded,dashed",color="\#999999",fontcolor="\#999999"\]; |
|
66 | 66 |
\k<bar2> -> (?<bar3>\w+) \[style=dashed,color="\#999999"\]; |
67 | 67 |
\k<bar3> \[label=bar3\]; |
68 | 68 |
\} |
@@ -73,15 +73,15 @@ describe DotHelper do |
||
73 | 73 |
it "generates a richer DOT script" do |
74 | 74 |
agents_dot(@agents, true).should =~ %r{ |
75 | 75 |
\A |
76 |
- digraph \s foo \{ |
|
76 |
+ digraph \x20 "Agent \x20 Event \x20 Flow" \{ |
|
77 | 77 |
node \[ [^\]]+ \]; |
78 |
- (?<foo>\w+) \[label=foo,URL="#{Regexp.quote(agent_path(@foo))}"\]; |
|
78 |
+ (?<foo>\w+) \[label=foo,tooltip="Dot \x20 Foo",URL="#{Regexp.quote(agent_path(@foo))}"\]; |
|
79 | 79 |
\k<foo> -> (?<bar1>\w+) \[style=dashed\]; |
80 | 80 |
\k<foo> -> (?<bar2>\w+) \[color="\#999999"\]; |
81 |
- \k<bar1> \[label=bar1,URL="#{Regexp.quote(agent_path(@bar1))}"\]; |
|
82 |
- \k<bar2> \[label="bar2 \s \(Disabled\)",URL="#{Regexp.quote(agent_path(@bar2))}",style="rounded,dashed",color="\#999999",fontcolor="\#999999"\]; |
|
81 |
+ \k<bar1> \[label=bar1,tooltip="Dot \x20 Bar",URL="#{Regexp.quote(agent_path(@bar1))}"\]; |
|
82 |
+ \k<bar2> \[label=bar2,tooltip="Dot \x20 Bar",URL="#{Regexp.quote(agent_path(@bar2))}",style="rounded,dashed",color="\#999999",fontcolor="\#999999"\]; |
|
83 | 83 |
\k<bar2> -> (?<bar3>\w+) \[style=dashed,color="\#999999"\]; |
84 |
- \k<bar3> \[label=bar3,URL="#{Regexp.quote(agent_path(@bar3))}"\]; |
|
84 |
+ \k<bar3> \[label=bar3,tooltip="Dot \x20 Bar",URL="#{Regexp.quote(agent_path(@bar3))}"\]; |
|
85 | 85 |
\} |
86 | 86 |
\z |
87 | 87 |
}x |