GUI -- TopoView - Implemented much of the node selection logic. (WIP)
- introduced topoSelect.js. Change-Id: Ic843c7d8dc2249fe0cb8c33de60dce12c07aea44
Showing
13 changed files
with
591 additions
and
162 deletions
... | @@ -77,6 +77,7 @@ | ... | @@ -77,6 +77,7 @@ |
77 | width: panelWidth, | 77 | width: panelWidth, |
78 | height: panelHeight, | 78 | height: panelHeight, |
79 | isVisible: panelIsVisible, | 79 | isVisible: panelIsVisible, |
80 | + classed: classed, | ||
80 | el: panelEl | 81 | el: panelEl |
81 | }; | 82 | }; |
82 | 83 | ||
... | @@ -146,6 +147,10 @@ | ... | @@ -146,6 +147,10 @@ |
146 | return p.on; | 147 | return p.on; |
147 | } | 148 | } |
148 | 149 | ||
150 | + function classed(cls, bool) { | ||
151 | + return p.el.classed(cls, bool); | ||
152 | + } | ||
153 | + | ||
149 | function panelEl() { | 154 | function panelEl() { |
150 | return p.el; | 155 | return p.el; |
151 | } | 156 | } | ... | ... |
... | @@ -34,6 +34,7 @@ | ... | @@ -34,6 +34,7 @@ |
34 | $log = _$log_; | 34 | $log = _$log_; |
35 | fs = _fs_; | 35 | fs = _fs_; |
36 | 36 | ||
37 | + // TODO: change 'force' ref to be 'force.alpha' ref. | ||
37 | function createDragBehavior(force, selectCb, atDragEnd, | 38 | function createDragBehavior(force, selectCb, atDragEnd, |
38 | dragEnabled, clickEnabled) { | 39 | dragEnabled, clickEnabled) { |
39 | var draggedThreshold = d3.scale.linear() | 40 | var draggedThreshold = d3.scale.linear() | ... | ... |
... | @@ -82,9 +82,10 @@ | ... | @@ -82,9 +82,10 @@ |
82 | <script src="view/topo/topo.js"></script> | 82 | <script src="view/topo/topo.js"></script> |
83 | <script src="view/topo/topoEvent.js"></script> | 83 | <script src="view/topo/topoEvent.js"></script> |
84 | <script src="view/topo/topoForce.js"></script> | 84 | <script src="view/topo/topoForce.js"></script> |
85 | + <script src="view/topo/topoInst.js"></script> | ||
85 | <script src="view/topo/topoModel.js"></script> | 86 | <script src="view/topo/topoModel.js"></script> |
86 | <script src="view/topo/topoPanel.js"></script> | 87 | <script src="view/topo/topoPanel.js"></script> |
87 | - <script src="view/topo/topoInst.js"></script> | 88 | + <script src="view/topo/topoSelect.js"></script> |
88 | <script src="view/device/device.js"></script> | 89 | <script src="view/device/device.js"></script> |
89 | <!-- TODO: inject javascript refs server-side --> | 90 | <!-- TODO: inject javascript refs server-side --> |
90 | 91 | ... | ... |
... | @@ -72,71 +72,121 @@ | ... | @@ -72,71 +72,121 @@ |
72 | 72 | ||
73 | #topo-p-summary { | 73 | #topo-p-summary { |
74 | /* Base css from panel.css */ | 74 | /* Base css from panel.css */ |
75 | +} | ||
76 | + | ||
77 | +/* --- Topo Detail Panel --- */ | ||
75 | 78 | ||
79 | +#topo-p-detail { | ||
80 | + /* Base css from panel.css */ | ||
81 | + top: 320px; | ||
76 | } | 82 | } |
77 | 83 | ||
78 | -#topo-p-summary svg { | 84 | +/* --- general topo-panel styling --- */ |
85 | + | ||
86 | +.topo-p svg { | ||
79 | display: inline-block; | 87 | display: inline-block; |
80 | width: 42px; | 88 | width: 42px; |
81 | height: 42px; | 89 | height: 42px; |
82 | } | 90 | } |
83 | 91 | ||
84 | -#topo-p-summary h2 { | 92 | +.light .topo-p svg .glyph { |
93 | + fill: #222; | ||
94 | +} | ||
95 | + | ||
96 | +.dark .topo-p svg .glyph.overlay { | ||
97 | + fill: #222; | ||
98 | +} | ||
99 | + | ||
100 | +.dark .topo-p svg .glyph { | ||
101 | + fill: #ddd; | ||
102 | +} | ||
103 | +.light .topo-p svg .glyph.overlay { | ||
104 | + fill: #fff; | ||
105 | +} | ||
106 | + | ||
107 | + | ||
108 | +.topo-p h2 { | ||
85 | position: absolute; | 109 | position: absolute; |
86 | margin: 0 4px; | 110 | margin: 0 4px; |
87 | top: 20px; | 111 | top: 20px; |
88 | left: 50px; | 112 | left: 50px; |
89 | } | 113 | } |
90 | -.light #topo-p-summary h2 { | 114 | +.light .topo-p h2 { |
91 | color: black; | 115 | color: black; |
92 | } | 116 | } |
93 | -.dark #topo-p-summary h2 { | 117 | +.dark .topo-p h2 { |
94 | color: #ddd; | 118 | color: #ddd; |
95 | } | 119 | } |
96 | 120 | ||
97 | -#topo-p-summary h3 { | 121 | +.topo-p h3 { |
98 | margin: 0 4px; | 122 | margin: 0 4px; |
99 | top: 20px; | 123 | top: 20px; |
100 | left: 50px; | 124 | left: 50px; |
101 | } | 125 | } |
102 | -.light #topo-p-summary h3 { | 126 | +.light .topo-p h3 { |
103 | color: black; | 127 | color: black; |
104 | } | 128 | } |
105 | -.dark #topo-p-summary h3 { | 129 | +.dark .topo-p h3 { |
106 | color: #ddd; | 130 | color: #ddd; |
107 | } | 131 | } |
108 | 132 | ||
109 | -#topo-p-summary p, table { | 133 | +.topo-p p, table { |
110 | margin: 4px 4px; | 134 | margin: 4px 4px; |
111 | } | 135 | } |
112 | 136 | ||
113 | -#topo-p-summary td.label { | 137 | +.topo-p td.label { |
114 | font-style: italic; | 138 | font-style: italic; |
115 | padding-right: 12px; | 139 | padding-right: 12px; |
116 | /* works for both light and dark themes ... */ | 140 | /* works for both light and dark themes ... */ |
117 | color: #777; | 141 | color: #777; |
118 | } | 142 | } |
119 | 143 | ||
120 | -#topo-p-summary td.value { | 144 | +.topo-p td.value { |
121 | } | 145 | } |
122 | 146 | ||
123 | -#topo-p-summary hr { | 147 | +.topo-p hr { |
124 | height: 1px; | 148 | height: 1px; |
125 | border: 0; | 149 | border: 0; |
126 | } | 150 | } |
127 | -.light #topo-p-summary hr { | 151 | +.light .topo-p hr { |
128 | background-color: #ccc; | 152 | background-color: #ccc; |
129 | color: #ccc; | 153 | color: #ccc; |
130 | } | 154 | } |
131 | -.dark #topo-p-summary hr { | 155 | +.dark .topo-p hr { |
132 | background-color: #888; | 156 | background-color: #888; |
133 | color: #888; | 157 | color: #888; |
134 | } | 158 | } |
135 | 159 | ||
136 | 160 | ||
137 | -/* --- Topo Detail Panel --- */ | 161 | +.topo-p .actionBtn { |
162 | + margin: 6px 12px; | ||
163 | + padding: 2px 6px; | ||
164 | + font-size: 9pt; | ||
165 | + cursor: pointer; | ||
166 | + width: 200px; | ||
167 | + text-align: center; | ||
168 | + border-radius: 4px; | ||
169 | +} | ||
170 | +.light .topo-p .actionBtn { | ||
171 | + border: 2px solid #ddd; | ||
172 | + color: #eee; | ||
173 | + background: #888; | ||
174 | +} | ||
175 | +.dark .topo-p .actionBtn { | ||
176 | + border: 2px solid #222; | ||
177 | + color: #888; | ||
178 | + background: #444; | ||
179 | +} | ||
180 | + | ||
181 | +.light .topo-p .actionBtn:hover { | ||
182 | + color: #eee; | ||
183 | + background: #444; | ||
184 | +} | ||
185 | +.dark .topo-p .actionBtn:hover { | ||
186 | + color: #eee; | ||
187 | + background: #666; | ||
188 | +} | ||
138 | 189 | ||
139 | -/* TODO: add CSS rules */ | ||
140 | 190 | ||
141 | 191 | ||
142 | /* --- Topo Instance Panel --- */ | 192 | /* --- Topo Instance Panel --- */ | ... | ... |
... | @@ -66,7 +66,7 @@ | ... | @@ -66,7 +66,7 @@ |
66 | 66 | ||
67 | //E: [equalizeMasters, 'Equalize mastership roles'], | 67 | //E: [equalizeMasters, 'Equalize mastership roles'], |
68 | 68 | ||
69 | - //esc: handleEscape, | 69 | + esc: handleEscape, |
70 | 70 | ||
71 | _helpFormat: [ | 71 | _helpFormat: [ |
72 | ['O', 'I', 'D', '-', 'H', 'M', 'B', 'P' ], | 72 | ['O', 'I', 'D', '-', 'H', 'M', 'B', 'P' ], |
... | @@ -85,12 +85,29 @@ | ... | @@ -85,12 +85,29 @@ |
85 | ]; | 85 | ]; |
86 | } | 86 | } |
87 | 87 | ||
88 | + // --- Keystroke functions ------------------------------------------- | ||
88 | 89 | ||
89 | function toggleInstances() { | 90 | function toggleInstances() { |
90 | tis.toggle(); | 91 | tis.toggle(); |
91 | tfs.updateDeviceColors(); | 92 | tfs.updateDeviceColors(); |
92 | } | 93 | } |
93 | 94 | ||
95 | + function resetZoom() { | ||
96 | + zoomer.reset(); | ||
97 | + } | ||
98 | + | ||
99 | + function handleEscape() { | ||
100 | + $log.debug("TODO: handle-ESCAPE..."); | ||
101 | + // if showingAffinity: cancelAffinity | ||
102 | + | ||
103 | + // else if showingDetails: deselectAll | ||
104 | + | ||
105 | + // else if oiBox visible: hide oiBox | ||
106 | + | ||
107 | + // else if summary panel visible: cancel Summary | ||
108 | + | ||
109 | + // else: hoverMode = hoverModeNone | ||
110 | + } | ||
94 | 111 | ||
95 | // --- Glyphs, Icons, and the like ----------------------------------- | 112 | // --- Glyphs, Icons, and the like ----------------------------------- |
96 | 113 | ||
... | @@ -124,10 +141,6 @@ | ... | @@ -124,10 +141,6 @@ |
124 | }); | 141 | }); |
125 | } | 142 | } |
126 | 143 | ||
127 | - function resetZoom() { | ||
128 | - zoomer.reset(); | ||
129 | - } | ||
130 | - | ||
131 | 144 | ||
132 | // callback invoked when the SVG view has been resized.. | 145 | // callback invoked when the SVG view has been resized.. |
133 | function svgResized(s) { | 146 | function svgResized(s) { | ... | ... |
... | @@ -27,7 +27,7 @@ | ... | @@ -27,7 +27,7 @@ |
27 | 'use strict'; | 27 | 'use strict'; |
28 | 28 | ||
29 | // injected refs | 29 | // injected refs |
30 | - var $log, wss, wes, tps, tis, tfs; | 30 | + var $log, wss, wes, tps, tis, tfs, tss; |
31 | 31 | ||
32 | // internal state | 32 | // internal state |
33 | var wsock, evApis; | 33 | var wsock, evApis; |
... | @@ -37,9 +37,13 @@ | ... | @@ -37,9 +37,13 @@ |
37 | function bindApis() { | 37 | function bindApis() { |
38 | evApis = { | 38 | evApis = { |
39 | showSummary: tps, | 39 | showSummary: tps, |
40 | + | ||
41 | + showDetails: tss, | ||
42 | + | ||
40 | addInstance: tis, | 43 | addInstance: tis, |
41 | updateInstance: tis, | 44 | updateInstance: tis, |
42 | removeInstance: tis, | 45 | removeInstance: tis, |
46 | + | ||
43 | addDevice: tfs, | 47 | addDevice: tfs, |
44 | updateDevice: tfs, | 48 | updateDevice: tfs, |
45 | removeDevice: tfs, | 49 | removeDevice: tfs, |
... | @@ -100,14 +104,16 @@ | ... | @@ -100,14 +104,16 @@ |
100 | .factory('TopoEventService', | 104 | .factory('TopoEventService', |
101 | ['$log', '$location', 'WebSocketService', 'WsEventService', | 105 | ['$log', '$location', 'WebSocketService', 'WsEventService', |
102 | 'TopoPanelService', 'TopoInstService', 'TopoForceService', | 106 | 'TopoPanelService', 'TopoInstService', 'TopoForceService', |
107 | + 'TopoSelectService', | ||
103 | 108 | ||
104 | - function (_$log_, $loc, _wss_, _wes_, _tps_, _tis_, _tfs_) { | 109 | + function (_$log_, $loc, _wss_, _wes_, _tps_, _tis_, _tfs_, _tss_) { |
105 | $log = _$log_; | 110 | $log = _$log_; |
106 | wss = _wss_; | 111 | wss = _wss_; |
107 | wes = _wes_; | 112 | wes = _wes_; |
108 | tps = _tps_; | 113 | tps = _tps_; |
109 | tis = _tis_; | 114 | tis = _tis_; |
110 | tfs = _tfs_; | 115 | tfs = _tfs_; |
116 | + tss = _tss_; | ||
111 | 117 | ||
112 | bindApis(); | 118 | bindApis(); |
113 | 119 | ... | ... |
... | @@ -23,7 +23,7 @@ | ... | @@ -23,7 +23,7 @@ |
23 | 'use strict'; | 23 | 'use strict'; |
24 | 24 | ||
25 | // injected refs | 25 | // injected refs |
26 | - var $log, fs, sus, is, ts, flash, tis, tms, icfg, uplink; | 26 | + var $log, fs, sus, is, ts, flash, tis, tms, tss, icfg, uplink; |
27 | 27 | ||
28 | // configuration | 28 | // configuration |
29 | var labelConfig = { | 29 | var labelConfig = { |
... | @@ -77,10 +77,7 @@ | ... | @@ -77,10 +77,7 @@ |
77 | showOffline = true, // whether offline devices are displayed | 77 | showOffline = true, // whether offline devices are displayed |
78 | oblique = false, // whether we are in the oblique view | 78 | oblique = false, // whether we are in the oblique view |
79 | nodeLock = false, // whether nodes can be dragged or not (locked) | 79 | nodeLock = false, // whether nodes can be dragged or not (locked) |
80 | - dim, // the dimensions of the force layout [w,h] | 80 | + dim; // the dimensions of the force layout [w,h] |
81 | - hovered, // the node over which the mouse is hovering | ||
82 | - selections = {}, // what is currently selected | ||
83 | - selectOrder = []; // the order in which we made selections | ||
84 | 81 | ||
85 | // SVG elements; | 82 | // SVG elements; |
86 | var linkG, linkLabelG, nodeG; | 83 | var linkG, linkLabelG, nodeG; |
... | @@ -311,8 +308,6 @@ | ... | @@ -311,8 +308,6 @@ |
311 | .attr('stroke', linkConfig[th].baseColor); | 308 | .attr('stroke', linkConfig[th].baseColor); |
312 | } | 309 | } |
313 | 310 | ||
314 | - | ||
315 | - | ||
316 | function removeLinkElement(d) { | 311 | function removeLinkElement(d) { |
317 | var idx = fs.find(d.key, network.links, 'key'), | 312 | var idx = fs.find(d.key, network.links, 'key'), |
318 | removed; | 313 | removed; |
... | @@ -418,34 +413,10 @@ | ... | @@ -418,34 +413,10 @@ |
418 | }); | 413 | }); |
419 | } | 414 | } |
420 | 415 | ||
421 | - function requestTrafficForMode() { | ||
422 | - $log.debug('TODO: requestTrafficForMode()...'); | ||
423 | - } | ||
424 | - | ||
425 | 416 | ||
426 | // ========================== | 417 | // ========================== |
427 | // === Devices and hosts - D3 rendering | 418 | // === Devices and hosts - D3 rendering |
428 | 419 | ||
429 | - function nodeMouseOver(m) { | ||
430 | - if (!m.dragStarted) { | ||
431 | - $log.debug("MouseOver()...", m); | ||
432 | - if (hovered != m) { | ||
433 | - hovered = m; | ||
434 | - requestTrafficForMode(); | ||
435 | - } | ||
436 | - } | ||
437 | - } | ||
438 | - | ||
439 | - function nodeMouseOut(m) { | ||
440 | - if (!m.dragStarted) { | ||
441 | - if (hovered) { | ||
442 | - hovered = null; | ||
443 | - requestTrafficForMode(); | ||
444 | - } | ||
445 | - $log.debug("MouseOut()...", m); | ||
446 | - } | ||
447 | - } | ||
448 | - | ||
449 | 420 | ||
450 | // Returns the newly computed bounding box of the rectangle | 421 | // Returns the newly computed bounding box of the rectangle |
451 | function adjustRectToFitText(n) { | 422 | function adjustRectToFitText(n) { |
... | @@ -568,10 +539,11 @@ | ... | @@ -568,10 +539,11 @@ |
568 | } | 539 | } |
569 | 540 | ||
570 | function unpin() { | 541 | function unpin() { |
571 | - if (hovered) { | 542 | + var hov = tss.hovered(); |
572 | - sendUpdateMeta(hovered, true); | 543 | + if (hov) { |
573 | - hovered.fixed = false; | 544 | + sendUpdateMeta(hov, true); |
574 | - hovered.el.classed('fixed', false); | 545 | + hov.fixed = false; |
546 | + hov.el.classed('fixed', false); | ||
575 | fResume(); | 547 | fResume(); |
576 | } | 548 | } |
577 | } | 549 | } |
... | @@ -668,8 +640,8 @@ | ... | @@ -668,8 +640,8 @@ |
668 | opacity: 0 | 640 | opacity: 0 |
669 | }) | 641 | }) |
670 | .call(drag) | 642 | .call(drag) |
671 | - .on('mouseover', nodeMouseOver) | 643 | + .on('mouseover', tss.nodeMouseOver) |
672 | - .on('mouseout', nodeMouseOut) | 644 | + .on('mouseout', tss.nodeMouseOut) |
673 | .transition() | 645 | .transition() |
674 | .attr('opacity', 1); | 646 | .attr('opacity', 1); |
675 | 647 | ||
... | @@ -998,72 +970,6 @@ | ... | @@ -998,72 +970,6 @@ |
998 | } | 970 | } |
999 | 971 | ||
1000 | 972 | ||
1001 | - function updateDetailPanel() { | ||
1002 | - // TODO update detail panel | ||
1003 | - $log.debug("TODO: updateDetailPanel() ..."); | ||
1004 | - } | ||
1005 | - | ||
1006 | - | ||
1007 | - // ========================== | ||
1008 | - // === SELECTION / DESELECTION | ||
1009 | - | ||
1010 | - function selectObject(obj) { | ||
1011 | - var el = this, | ||
1012 | - ev = d3.event.sourceEvent, | ||
1013 | - n; | ||
1014 | - | ||
1015 | - if (zoomingOrPanning(ev)) { | ||
1016 | - return; | ||
1017 | - } | ||
1018 | - | ||
1019 | - if (el) { | ||
1020 | - n = d3.select(el); | ||
1021 | - } else { | ||
1022 | - node.each(function (d) { | ||
1023 | - if (d == obj) { | ||
1024 | - n = d3.select(el = this); | ||
1025 | - } | ||
1026 | - }); | ||
1027 | - } | ||
1028 | - if (!n) return; | ||
1029 | - | ||
1030 | - if (ev.shiftKey && n.classed('selected')) { | ||
1031 | - deselectObject(obj.id); | ||
1032 | - updateDetailPanel(); | ||
1033 | - return; | ||
1034 | - } | ||
1035 | - | ||
1036 | - if (!ev.shiftKey) { | ||
1037 | - deselectAll(); | ||
1038 | - } | ||
1039 | - | ||
1040 | - selections[obj.id] = { obj: obj, el: el }; | ||
1041 | - selectOrder.push(obj.id); | ||
1042 | - | ||
1043 | - n.classed('selected', true); | ||
1044 | - updateDeviceColors(obj); | ||
1045 | - updateDetailPanel(); | ||
1046 | - } | ||
1047 | - | ||
1048 | - function deselectObject(id) { | ||
1049 | - var obj = selections[id]; | ||
1050 | - if (obj) { | ||
1051 | - d3.select(obj.el).classed('selected', false); | ||
1052 | - delete selections[id]; | ||
1053 | - fs.removeFromArray(id, selectOrder); | ||
1054 | - updateDeviceColors(obj.obj); | ||
1055 | - } | ||
1056 | - } | ||
1057 | - | ||
1058 | - function deselectAll() { | ||
1059 | - // deselect all nodes in the network... | ||
1060 | - node.classed('selected', false); | ||
1061 | - selections = {}; | ||
1062 | - selectOrder = []; | ||
1063 | - updateDeviceColors(); | ||
1064 | - updateDetailPanel(); | ||
1065 | - } | ||
1066 | - | ||
1067 | // ========================== | 973 | // ========================== |
1068 | // === MOUSE GESTURE HANDLERS | 974 | // === MOUSE GESTURE HANDLERS |
1069 | 975 | ||
... | @@ -1103,12 +1009,22 @@ | ... | @@ -1103,12 +1009,22 @@ |
1103 | }; | 1009 | }; |
1104 | } | 1010 | } |
1105 | 1011 | ||
1012 | + function mkSelectApi(uplink) { | ||
1013 | + return { | ||
1014 | + node: function () { return node; }, | ||
1015 | + zoomingOrPanning: zoomingOrPanning, | ||
1016 | + updateDeviceColors: updateDeviceColors, | ||
1017 | + sendEvent: uplink.sendEvent | ||
1018 | + }; | ||
1019 | + } | ||
1020 | + | ||
1106 | angular.module('ovTopo') | 1021 | angular.module('ovTopo') |
1107 | .factory('TopoForceService', | 1022 | .factory('TopoForceService', |
1108 | ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService', | 1023 | ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService', |
1109 | 'FlashService', 'TopoInstService', 'TopoModelService', | 1024 | 'FlashService', 'TopoInstService', 'TopoModelService', |
1025 | + 'TopoSelectService', | ||
1110 | 1026 | ||
1111 | - function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_, _tis_, _tms_) { | 1027 | + function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_, _tis_, _tms_, _tss_) { |
1112 | $log = _$log_; | 1028 | $log = _$log_; |
1113 | fs = _fs_; | 1029 | fs = _fs_; |
1114 | sus = _sus_; | 1030 | sus = _sus_; |
... | @@ -1117,6 +1033,7 @@ | ... | @@ -1117,6 +1033,7 @@ |
1117 | flash = _flash_; | 1033 | flash = _flash_; |
1118 | tis = _tis_; | 1034 | tis = _tis_; |
1119 | tms = _tms_; | 1035 | tms = _tms_; |
1036 | + tss = _tss_; | ||
1120 | 1037 | ||
1121 | icfg = is.iconConfig(); | 1038 | icfg = is.iconConfig(); |
1122 | 1039 | ||
... | @@ -1131,6 +1048,7 @@ | ... | @@ -1131,6 +1048,7 @@ |
1131 | $log.debug('initForce().. dim = ' + dim); | 1048 | $log.debug('initForce().. dim = ' + dim); |
1132 | 1049 | ||
1133 | tms.initModel(mkModelApi(uplink), dim); | 1050 | tms.initModel(mkModelApi(uplink), dim); |
1051 | + tss.initSelect(mkSelectApi(uplink)); | ||
1134 | 1052 | ||
1135 | settings = angular.extend({}, defaultSettings, opts); | 1053 | settings = angular.extend({}, defaultSettings, opts); |
1136 | 1054 | ||
... | @@ -1154,7 +1072,7 @@ | ... | @@ -1154,7 +1072,7 @@ |
1154 | .on('tick', tick); | 1072 | .on('tick', tick); |
1155 | 1073 | ||
1156 | drag = sus.createDragBehavior(force, | 1074 | drag = sus.createDragBehavior(force, |
1157 | - selectObject, atDragEnd, dragEnabled, clickEnabled); | 1075 | + tss.selectObject, atDragEnd, dragEnabled, clickEnabled); |
1158 | } | 1076 | } |
1159 | 1077 | ||
1160 | function newDim(_dim_) { | 1078 | function newDim(_dim_) { | ... | ... |
... | @@ -24,18 +24,21 @@ | ... | @@ -24,18 +24,21 @@ |
24 | 'use strict'; | 24 | 'use strict'; |
25 | 25 | ||
26 | // injected refs | 26 | // injected refs |
27 | - var $log, fs, rnd, api; | 27 | + var $log, fs, rnd; |
28 | + | ||
29 | + // api to topoForce | ||
30 | + var api; | ||
31 | + /* | ||
32 | + projection() | ||
33 | + network {...} | ||
34 | + restyleLinkElement( ldata ) | ||
35 | + removeLinkElement( ldata ) | ||
36 | + */ | ||
28 | 37 | ||
29 | // shorthand | 38 | // shorthand |
30 | var lu, rlk, nodes, links; | 39 | var lu, rlk, nodes, links; |
31 | 40 | ||
32 | - // api: | 41 | + var dim; // dimensions of layout [w,h] |
33 | - // projection: func() | ||
34 | - // network {...} | ||
35 | - // restyleLinkElement: func(ldata) | ||
36 | - // removeLinkElement: func(ldata) | ||
37 | - | ||
38 | - var dim; // dimensions of layout, as [w,h] | ||
39 | 42 | ||
40 | // configuration 'constants' | 43 | // configuration 'constants' |
41 | var defaultLinkType = 'direct', | 44 | var defaultLinkType = 'direct', | ... | ... |
... | @@ -26,7 +26,8 @@ | ... | @@ -26,7 +26,8 @@ |
26 | var $log, ps, gs; | 26 | var $log, ps, gs; |
27 | 27 | ||
28 | // constants | 28 | // constants |
29 | - var idSum = 'topo-p-summary', | 29 | + var pCls = 'topo-p', |
30 | + idSum = 'topo-p-summary', | ||
30 | idDet = 'topo-p-detail', | 31 | idDet = 'topo-p-detail', |
31 | panelOpts = { | 32 | panelOpts = { |
32 | width: 260 | 33 | width: 260 |
... | @@ -36,60 +37,134 @@ | ... | @@ -36,60 +37,134 @@ |
36 | var summaryPanel, | 37 | var summaryPanel, |
37 | detailPanel; | 38 | detailPanel; |
38 | 39 | ||
39 | - // ========================== | ||
40 | - // *** SHOW SUMMARY *** | ||
41 | 40 | ||
42 | - function showSummary(data) { | 41 | + // === ----------------------------------------------------- |
43 | - populateSummary(data); | 42 | + // Utility functions |
44 | - showSummaryPanel(); | 43 | + |
44 | + function addSep(tbody) { | ||
45 | + tbody.append('tr').append('td').attr('colspan', 2).append('hr'); | ||
46 | + } | ||
47 | + | ||
48 | + function addProp(tbody, label, value) { | ||
49 | + var tr = tbody.append('tr'); | ||
50 | + | ||
51 | + function addCell(cls, txt) { | ||
52 | + tr.append('td').attr('class', cls).text(txt); | ||
53 | + } | ||
54 | + addCell('label', label + ' :'); | ||
55 | + addCell('value', value); | ||
56 | + } | ||
57 | + | ||
58 | + function listProps(tbody, data) { | ||
59 | + data.propOrder.forEach(function(p) { | ||
60 | + if (p === '-') { | ||
61 | + addSep(tbody); | ||
62 | + } else { | ||
63 | + addProp(tbody, p, data.props[p]); | ||
64 | + } | ||
65 | + }); | ||
66 | + } | ||
67 | + | ||
68 | + function dpa(x) { | ||
69 | + return detailPanel.append(x); | ||
70 | + } | ||
71 | + | ||
72 | + function spa(x) { | ||
73 | + return summaryPanel.append(x); | ||
45 | } | 74 | } |
46 | 75 | ||
76 | + // === ----------------------------------------------------- | ||
77 | + // Functions for populating the summary panel | ||
78 | + | ||
47 | function populateSummary(data) { | 79 | function populateSummary(data) { |
48 | summaryPanel.empty(); | 80 | summaryPanel.empty(); |
49 | 81 | ||
50 | - var svg = summaryPanel.append('svg'), | 82 | + var svg = spa('svg'), |
51 | - title = summaryPanel.append('h2'), | 83 | + title = spa('h2'), |
52 | - table = summaryPanel.append('table'), | 84 | + table = spa('table'), |
53 | tbody = table.append('tbody'); | 85 | tbody = table.append('tbody'); |
54 | 86 | ||
55 | gs.addGlyph(svg, 'node', 40); | 87 | gs.addGlyph(svg, 'node', 40); |
56 | gs.addGlyph(svg, 'bird', 24, true, [8,12]); | 88 | gs.addGlyph(svg, 'bird', 24, true, [8,12]); |
57 | 89 | ||
58 | title.text(data.id); | 90 | title.text(data.id); |
91 | + listProps(tbody, data); | ||
92 | + } | ||
59 | 93 | ||
60 | - data.propOrder.forEach(function(p) { | 94 | + // === ----------------------------------------------------- |
61 | - if (p === '-') { | 95 | + // Functions for populating the detail panel |
62 | - addSep(tbody); | 96 | + |
63 | - } else { | 97 | + function displaySingle(data) { |
64 | - addProp(tbody, p, data.props[p]); | 98 | + detailPanel.empty(); |
99 | + | ||
100 | + var svg = dpa('svg'), | ||
101 | + title = dpa('h2'), | ||
102 | + table = dpa('table'), | ||
103 | + tbody = table.append('tbody'); | ||
104 | + | ||
105 | + gs.addGlyph(svg, (data.type || 'unknown'), 40); | ||
106 | + title.text(data.id); | ||
107 | + listProps(tbody, data); | ||
108 | + dpa('hr'); | ||
65 | } | 109 | } |
110 | + | ||
111 | + function displayMulti(ids) { | ||
112 | + detailPanel.empty(); | ||
113 | + | ||
114 | + var title = dpa('h3'), | ||
115 | + table = dpa('table'), | ||
116 | + tbody = table.append('tbody'); | ||
117 | + | ||
118 | + title.text('Selected Nodes'); | ||
119 | + ids.forEach(function (d, i) { | ||
120 | + addProp(tbody, i+1, d); | ||
66 | }); | 121 | }); |
122 | + dpa('hr'); | ||
67 | } | 123 | } |
68 | 124 | ||
69 | - function addSep(tbody) { | 125 | + function addAction(text, cb) { |
70 | - tbody.append('tr').append('td').attr('colspan', 2).append('hr'); | 126 | + dpa('div') |
127 | + .classed('actionBtn', true) | ||
128 | + .text(text) | ||
129 | + .on('click', cb); | ||
71 | } | 130 | } |
72 | 131 | ||
73 | - function addProp(tbody, label, value) { | 132 | + // === ----------------------------------------------------- |
74 | - var tr = tbody.append('tr'); | 133 | + // Event Handlers |
75 | 134 | ||
76 | - function addCell(cls, txt) { | 135 | + function showSummary(data) { |
77 | - tr.append('td').attr('class', cls).text(txt); | 136 | + populateSummary(data); |
78 | - } | 137 | + showSummaryPanel(); |
79 | - addCell('label', label + ' :'); | ||
80 | - addCell('value', value); | ||
81 | } | 138 | } |
82 | 139 | ||
140 | + | ||
141 | + // === ----------------------------------------------------- | ||
142 | + // === LOGIC For showing/hiding summary and detail panels... | ||
143 | + | ||
83 | function showSummaryPanel() { | 144 | function showSummaryPanel() { |
84 | summaryPanel.show(); | 145 | summaryPanel.show(); |
85 | // TODO: augment, once we have the details pane also | 146 | // TODO: augment, once we have the details pane also |
86 | } | 147 | } |
87 | 148 | ||
149 | + function showDetailPanel() { | ||
150 | + // TODO: augment with summary-accomodation-logic | ||
151 | + detailPanel.show(); | ||
152 | + } | ||
153 | + | ||
154 | + function hideDetailPanel() { | ||
155 | + detailPanel.hide(); | ||
156 | + } | ||
157 | + | ||
158 | + | ||
159 | + | ||
88 | // ========================== | 160 | // ========================== |
89 | 161 | ||
90 | function initPanels() { | 162 | function initPanels() { |
91 | summaryPanel = ps.createPanel(idSum, panelOpts); | 163 | summaryPanel = ps.createPanel(idSum, panelOpts); |
92 | detailPanel = ps.createPanel(idDet, panelOpts); | 164 | detailPanel = ps.createPanel(idDet, panelOpts); |
165 | + | ||
166 | + summaryPanel.classed(pCls, true); | ||
167 | + detailPanel.classed(pCls, true); | ||
93 | } | 168 | } |
94 | 169 | ||
95 | function destroyPanels() { | 170 | function destroyPanels() { |
... | @@ -112,7 +187,18 @@ | ... | @@ -112,7 +187,18 @@ |
112 | return { | 187 | return { |
113 | initPanels: initPanels, | 188 | initPanels: initPanels, |
114 | destroyPanels: destroyPanels, | 189 | destroyPanels: destroyPanels, |
115 | - showSummary: showSummary | 190 | + |
191 | + showSummary: showSummary, | ||
192 | + | ||
193 | + displaySingle: displaySingle, | ||
194 | + displayMulti: displayMulti, | ||
195 | + addAction: addAction, | ||
196 | + | ||
197 | + showDetailPanel: showDetailPanel, | ||
198 | + hideDetailPanel: hideDetailPanel, | ||
199 | + | ||
200 | + detailVisible: function () { return detailPanel.isVisible(); }, | ||
201 | + summaryVisible: function () { return summaryPanel.isVisible(); } | ||
116 | }; | 202 | }; |
117 | }]); | 203 | }]); |
118 | }()); | 204 | }()); | ... | ... |
1 | +/* | ||
2 | + * Copyright 2015 Open Networking Laboratory | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | + | ||
17 | +/* | ||
18 | + ONOS GUI -- Topology Selection Module. | ||
19 | + Defines behavior when selecting nodes. | ||
20 | + */ | ||
21 | + | ||
22 | +(function () { | ||
23 | + 'use strict'; | ||
24 | + | ||
25 | + // injected refs | ||
26 | + var $log, fs, tps; | ||
27 | + | ||
28 | + // api to topoForce | ||
29 | + var api; | ||
30 | + /* | ||
31 | + node() // get ref to D3 selection of nodes | ||
32 | + zoomingOrPanning( ev ) | ||
33 | + updateDeviceColors( [dev] ) | ||
34 | + sendEvent( type, {payload} ) | ||
35 | + */ | ||
36 | + | ||
37 | + // internal state | ||
38 | + var hovered, // the node over which the mouse is hovering | ||
39 | + selections = {}, // currently selected nodes (by id) | ||
40 | + selectOrder = [], // the order in which we made selections | ||
41 | + haveDetails = false, // do we have details of one or more nodes? | ||
42 | + useDetails = true; // should we show details if we have 'em? | ||
43 | + | ||
44 | + // ========================== | ||
45 | + | ||
46 | + function nSel() { | ||
47 | + return selectOrder.length; | ||
48 | + } | ||
49 | + function getSel(idx) { | ||
50 | + return selections[selectOrder[idx]]; | ||
51 | + } | ||
52 | + function allSelectionsClass(cls) { | ||
53 | + for (var i=0, n=nSel(); i<n; i++) { | ||
54 | + if (getSel(i).obj.class !== cls) { | ||
55 | + return false; | ||
56 | + } | ||
57 | + } | ||
58 | + return true; | ||
59 | + } | ||
60 | + | ||
61 | + // ========================== | ||
62 | + | ||
63 | + function nodeMouseOver(m) { | ||
64 | + if (!m.dragStarted) { | ||
65 | + $log.debug("MouseOver()...", m); | ||
66 | + if (hovered != m) { | ||
67 | + hovered = m; | ||
68 | + requestTrafficForMode(); | ||
69 | + } | ||
70 | + } | ||
71 | + } | ||
72 | + | ||
73 | + function nodeMouseOut(m) { | ||
74 | + if (!m.dragStarted) { | ||
75 | + if (hovered) { | ||
76 | + hovered = null; | ||
77 | + requestTrafficForMode(); | ||
78 | + } | ||
79 | + $log.debug("MouseOut()...", m); | ||
80 | + } | ||
81 | + } | ||
82 | + | ||
83 | + // ========================== | ||
84 | + | ||
85 | + function selectObject(obj) { | ||
86 | + var el = this, | ||
87 | + ev = d3.event.sourceEvent, | ||
88 | + n; | ||
89 | + | ||
90 | + if (api.zoomingOrPanning(ev)) { | ||
91 | + return; | ||
92 | + } | ||
93 | + | ||
94 | + if (el) { | ||
95 | + n = d3.select(el); | ||
96 | + } else { | ||
97 | + api.node().each(function (d) { | ||
98 | + if (d == obj) { | ||
99 | + n = d3.select(el = this); | ||
100 | + } | ||
101 | + }); | ||
102 | + } | ||
103 | + if (!n) return; | ||
104 | + | ||
105 | + if (ev.shiftKey && n.classed('selected')) { | ||
106 | + deselectObject(obj.id); | ||
107 | + updateDetail(); | ||
108 | + return; | ||
109 | + } | ||
110 | + | ||
111 | + if (!ev.shiftKey) { | ||
112 | + deselectAll(); | ||
113 | + } | ||
114 | + | ||
115 | + selections[obj.id] = { obj: obj, el: el }; | ||
116 | + selectOrder.push(obj.id); | ||
117 | + | ||
118 | + n.classed('selected', true); | ||
119 | + api.updateDeviceColors(obj); | ||
120 | + updateDetail(); | ||
121 | + | ||
122 | + debugSel(); | ||
123 | + } | ||
124 | + | ||
125 | + function deselectObject(id) { | ||
126 | + var obj = selections[id]; | ||
127 | + if (obj) { | ||
128 | + d3.select(obj.el).classed('selected', false); | ||
129 | + delete selections[id]; | ||
130 | + fs.removeFromArray(id, selectOrder); | ||
131 | + api.updateDeviceColors(obj.obj); | ||
132 | + } | ||
133 | + | ||
134 | + debugSel(); | ||
135 | + } | ||
136 | + | ||
137 | + function deselectAll() { | ||
138 | + // deselect all nodes in the network... | ||
139 | + api.node().classed('selected', false); | ||
140 | + selections = {}; | ||
141 | + selectOrder = []; | ||
142 | + api.updateDeviceColors(); | ||
143 | + updateDetail(); | ||
144 | + | ||
145 | + debugSel(); | ||
146 | + } | ||
147 | + | ||
148 | + function debugSel() { | ||
149 | + $log.debug(' ..... Selected now >> ', selectOrder); | ||
150 | + } | ||
151 | + | ||
152 | + // === ----------------------------------------------------- | ||
153 | + | ||
154 | + function requestDetails() { | ||
155 | + var data = getSel(0).obj; | ||
156 | + api.sendEvent('requestDetails', { | ||
157 | + id: data.id, | ||
158 | + class: data.class | ||
159 | + }); | ||
160 | + } | ||
161 | + | ||
162 | + // === ----------------------------------------------------- | ||
163 | + | ||
164 | + function updateDetail() { | ||
165 | + var nSel = selectOrder.length; | ||
166 | + if (!nSel) { | ||
167 | + emptySelect(); | ||
168 | + } else if (nSel === 1) { | ||
169 | + singleSelect(); | ||
170 | + } else { | ||
171 | + multiSelect(); | ||
172 | + } | ||
173 | + } | ||
174 | + | ||
175 | + function emptySelect() { | ||
176 | + haveDetails = false; | ||
177 | + tps.hideDetailPanel(); | ||
178 | + cancelTraffic(); | ||
179 | + } | ||
180 | + | ||
181 | + function singleSelect() { | ||
182 | + // NOTE: detail is shown from 'showDetails' event callback | ||
183 | + requestDetails(); | ||
184 | + cancelTraffic(); | ||
185 | + requestTrafficForMode(); | ||
186 | + } | ||
187 | + | ||
188 | + function multiSelect() { | ||
189 | + haveDetails = true; | ||
190 | + | ||
191 | + // display the selected nodes in the detail panel | ||
192 | + tps.displayMulti(selectOrder); | ||
193 | + | ||
194 | + // always add the 'show traffic' action | ||
195 | + tps.addAction('Show Related Traffic', showRelatedIntentsAction); | ||
196 | + | ||
197 | + // add other actions, based on what is selected... | ||
198 | + if (nSel() === 2 && allSelectionsClass('host')) { | ||
199 | + tps.addAction('Create Host-to-Host Flow', addHostIntentAction); | ||
200 | + } else if (nSel() >= 2 && allSelectionsClass('host')) { | ||
201 | + tps.addAction('Create Multi-Source Flow', addMultiSourceIntentAction); | ||
202 | + } | ||
203 | + | ||
204 | + cancelTraffic(); | ||
205 | + requestTrafficForMode(); | ||
206 | + } | ||
207 | + | ||
208 | + | ||
209 | + // === ----------------------------------------------------- | ||
210 | + // Event Handlers | ||
211 | + | ||
212 | + function showDetails(data) { | ||
213 | + haveDetails = true; | ||
214 | + | ||
215 | + // display the data for the single selected node | ||
216 | + tps.displaySingle(data); | ||
217 | + | ||
218 | + // always add the 'show traffic' action | ||
219 | + tps.addAction('Show Related Traffic', showRelatedIntentsAction); | ||
220 | + | ||
221 | + // add other actions, based on what is selected... | ||
222 | + if (data.type === 'switch') { | ||
223 | + tps.addAction('Show Device Flows', showDeviceLinkFlowsAction); | ||
224 | + } | ||
225 | + | ||
226 | + // only show the details panel if the user hasn't "hidden" it | ||
227 | + if (useDetails) { | ||
228 | + tps.showDetailPanel(); | ||
229 | + } | ||
230 | + } | ||
231 | + | ||
232 | + // === ----------------------------------------------------- | ||
233 | + // TODO: migrate these to topoTraffic.js | ||
234 | + | ||
235 | + function cancelTraffic() { | ||
236 | + $log.debug('TODO: cancelTraffic'); | ||
237 | + | ||
238 | + } | ||
239 | + function requestTrafficForMode() { | ||
240 | + $log.debug('TODO: requestTrafficForMode'); | ||
241 | + | ||
242 | + } | ||
243 | + function showRelatedIntentsAction () { | ||
244 | + $log.debug('TODO: showRelatedIntentsAction'); | ||
245 | + | ||
246 | + } | ||
247 | + function addHostIntentAction () { | ||
248 | + $log.debug('TODO: addHostIntentAction'); | ||
249 | + | ||
250 | + } | ||
251 | + function addMultiSourceIntentAction () { | ||
252 | + $log.debug('TODO: addMultiSourceIntentAction'); | ||
253 | + | ||
254 | + } | ||
255 | + function showDeviceLinkFlowsAction () { | ||
256 | + $log.debug('TODO: showDeviceLinkFlowsAction'); | ||
257 | + | ||
258 | + } | ||
259 | + | ||
260 | + | ||
261 | + // === ----------------------------------------------------- | ||
262 | + // === MODULE DEFINITION === | ||
263 | + | ||
264 | + angular.module('ovTopo') | ||
265 | + .factory('TopoSelectService', | ||
266 | + ['$log', 'FnService', 'TopoPanelService', | ||
267 | + | ||
268 | + function (_$log_, _fs_, _tps_) { | ||
269 | + $log = _$log_; | ||
270 | + fs = _fs_; | ||
271 | + tps = _tps_; | ||
272 | + | ||
273 | + function initSelect(_api_) { | ||
274 | + api = _api_; | ||
275 | + } | ||
276 | + | ||
277 | + function destroySelect() { } | ||
278 | + | ||
279 | + return { | ||
280 | + initSelect: initSelect, | ||
281 | + destroySelect: destroySelect, | ||
282 | + | ||
283 | + showDetails: showDetails, | ||
284 | + | ||
285 | + nodeMouseOver: nodeMouseOver, | ||
286 | + nodeMouseOut: nodeMouseOut, | ||
287 | + selectObject: selectObject, | ||
288 | + deselectObject: deselectObject, | ||
289 | + deselectAll: deselectAll, | ||
290 | + hovered: function () { return hovered; } | ||
291 | + }; | ||
292 | + }]); | ||
293 | +}()); |
... | @@ -88,7 +88,7 @@ describe('factory: fw/layer/panel.js', function () { | ... | @@ -88,7 +88,7 @@ describe('factory: fw/layer/panel.js', function () { |
88 | var p = ps.createPanel('foo'); | 88 | var p = ps.createPanel('foo'); |
89 | expect(fs.areFunctions(p, [ | 89 | expect(fs.areFunctions(p, [ |
90 | 'show', 'hide', 'toggle', 'empty', 'append', | 90 | 'show', 'hide', 'toggle', 'empty', 'append', |
91 | - 'width', 'height', 'isVisible', 'el' | 91 | + 'width', 'height', 'isVisible', 'classed', 'el' |
92 | ])).toBeTruthy(); | 92 | ])).toBeTruthy(); |
93 | }); | 93 | }); |
94 | 94 | ... | ... |
... | @@ -34,7 +34,16 @@ describe('factory: view/topo/topoPanel.js', function() { | ... | @@ -34,7 +34,16 @@ describe('factory: view/topo/topoPanel.js', function() { |
34 | 34 | ||
35 | it('should define api functions', function () { | 35 | it('should define api functions', function () { |
36 | expect(fs.areFunctions(tps, [ | 36 | expect(fs.areFunctions(tps, [ |
37 | - 'initPanels', 'destroyPanels', 'showSummary' | 37 | + 'initPanels', |
38 | + 'destroyPanels', | ||
39 | + 'showSummary', | ||
40 | + 'displaySingle', | ||
41 | + 'displayMulti', | ||
42 | + 'addAction', | ||
43 | + 'showDetailPanel', | ||
44 | + 'hideDetailPanel', | ||
45 | + 'detailVisible', | ||
46 | + 'summaryVisible' | ||
38 | ])).toBeTruthy(); | 47 | ])).toBeTruthy(); |
39 | }); | 48 | }); |
40 | 49 | ... | ... |
1 | +/* | ||
2 | + * Copyright 2015 Open Networking Laboratory | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | + | ||
17 | +/* | ||
18 | + ONOS GUI -- Topo View -- Topo Selection Service - Unit Tests | ||
19 | + */ | ||
20 | +describe('factory: view/topo/topoSelect.js', function() { | ||
21 | + var $log, fs, tss; | ||
22 | + | ||
23 | + beforeEach(module('ovTopo', 'onosUtil', 'onosLayer')); | ||
24 | + | ||
25 | + beforeEach(inject(function (_$log_, FnService, TopoSelectService) { | ||
26 | + $log = _$log_; | ||
27 | + fs = FnService; | ||
28 | + tss = TopoSelectService; | ||
29 | + })); | ||
30 | + | ||
31 | + it('should define TopoSelectService', function () { | ||
32 | + expect(tss).toBeDefined(); | ||
33 | + }); | ||
34 | + | ||
35 | + it('should define api functions', function () { | ||
36 | + expect(fs.areFunctions(tss, [ | ||
37 | + 'initSelect', 'destroySelect', 'showDetails', | ||
38 | + 'nodeMouseOver', 'nodeMouseOut', 'selectObject', 'deselectObject', | ||
39 | + 'deselectAll', 'hovered' | ||
40 | + ])).toBeTruthy(); | ||
41 | + }); | ||
42 | + | ||
43 | + // TODO: more tests... | ||
44 | +}); |
-
Please register or login to post a comment