Jian Li
Committed by Thomas Vachuska

[ONOS-3635] Implement detail panel view for extended app properties

This commit implements detail panel view of application.
Current implementation adds id, state, category, version, origin,
role and url properties to detail panel view.
List of features and required applications will be added in a
separated commit.

Change-Id: Id5cd7ed128b3d69225153aca990c91b462e5a677
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
15 */ 15 */
16 package org.onosproject.ui.impl; 16 package org.onosproject.ui.impl;
17 17
18 +import com.fasterxml.jackson.databind.node.ArrayNode;
18 import com.fasterxml.jackson.databind.node.ObjectNode; 19 import com.fasterxml.jackson.databind.node.ObjectNode;
19 import com.google.common.collect.ImmutableSet; 20 import com.google.common.collect.ImmutableSet;
20 import org.onosproject.app.ApplicationAdminService; 21 import org.onosproject.app.ApplicationAdminService;
...@@ -42,6 +43,10 @@ public class ApplicationViewMessageHandler extends UiMessageHandler { ...@@ -42,6 +43,10 @@ public class ApplicationViewMessageHandler extends UiMessageHandler {
42 43
43 private static final String APP_MGMT_REQ = "appManagementRequest"; 44 private static final String APP_MGMT_REQ = "appManagementRequest";
44 45
46 + private static final String APP_DETAILS_REQ = "appDetailsRequest";
47 + private static final String APP_DETAILS_RESP = "appDetailsResponse";
48 + private static final String DETAILS = "details";
49 +
45 private static final String STATE = "state"; 50 private static final String STATE = "state";
46 private static final String STATE_IID = "_iconid_state"; 51 private static final String STATE_IID = "_iconid_state";
47 private static final String ID = "id"; 52 private static final String ID = "id";
...@@ -51,19 +56,25 @@ public class ApplicationViewMessageHandler extends UiMessageHandler { ...@@ -51,19 +56,25 @@ public class ApplicationViewMessageHandler extends UiMessageHandler {
51 private static final String ORIGIN = "origin"; 56 private static final String ORIGIN = "origin";
52 private static final String DESC = "desc"; 57 private static final String DESC = "desc";
53 private static final String URL = "url"; 58 private static final String URL = "url";
59 + private static final String README = "readme";
60 + private static final String ROLE = "role";
61 + private static final String REQUIRED_APPS = "_required_apps";
62 + private static final String FEATURES = "features";
54 63
55 private static final String ICON_ID_ACTIVE = "active"; 64 private static final String ICON_ID_ACTIVE = "active";
56 private static final String ICON_ID_INACTIVE = "appInactive"; 65 private static final String ICON_ID_INACTIVE = "appInactive";
57 66
58 private static final String[] COL_IDS = { 67 private static final String[] COL_IDS = {
59 - STATE, STATE_IID, ID, ICON, VERSION, CATEGORY, ORIGIN, DESC, URL 68 + STATE, STATE_IID, ID, ICON, VERSION, CATEGORY, ORIGIN, DESC,
69 + URL, README, ROLE, REQUIRED_APPS, FEATURES
60 }; 70 };
61 71
62 @Override 72 @Override
63 protected Collection<RequestHandler> createRequestHandlers() { 73 protected Collection<RequestHandler> createRequestHandlers() {
64 return ImmutableSet.of( 74 return ImmutableSet.of(
65 new AppDataRequest(), 75 new AppDataRequest(),
66 - new AppMgmtRequest() 76 + new AppMgmtRequest(),
77 + new DetailRequestHandler()
67 ); 78 );
68 } 79 }
69 80
...@@ -135,4 +146,46 @@ public class ApplicationViewMessageHandler extends UiMessageHandler { ...@@ -135,4 +146,46 @@ public class ApplicationViewMessageHandler extends UiMessageHandler {
135 } 146 }
136 } 147 }
137 } 148 }
149 +
150 + // handler for selected application detail requests
151 + private final class DetailRequestHandler extends RequestHandler {
152 + private DetailRequestHandler() {
153 + super(APP_DETAILS_REQ);
154 + }
155 +
156 + @Override
157 + public void process(long sid, ObjectNode payload) {
158 + String id = string(payload, ID);
159 + ApplicationService as = get(ApplicationService.class);
160 + ApplicationId appId = as.getId(id);
161 + ApplicationState state = as.getState(appId);
162 + Application app = as.getApplication(appId);
163 + ObjectNode data = objectNode();
164 +
165 + data.put(STATE, state.toString());
166 + data.put(ID, appId.name());
167 + data.put(VERSION, app.version().toString());
168 + data.put(ROLE, app.role().toString());
169 + data.put(CATEGORY, app.category());
170 + data.put(ORIGIN, app.origin());
171 + data.put(README, app.readme());
172 + data.put(URL, app.url());
173 +
174 + // process required applications
175 + ArrayNode requiredApps = arrayNode();
176 + app.requiredApps().forEach(s -> requiredApps.add(s));
177 +
178 + data.set(REQUIRED_APPS, requiredApps);
179 +
180 + // process features
181 + ArrayNode features = arrayNode();
182 + app.features().forEach(f -> features.add(f));
183 +
184 + data.set(FEATURES, features);
185 +
186 + ObjectNode rootNode = objectNode();
187 + rootNode.set(DETAILS, data);
188 + sendMessage(APP_DETAILS_RESP, 0, rootNode);
189 + }
190 + }
138 } 191 }
......
1 /* 1 /*
2 - * Copyright 2015 Open Networking Laboratory 2 + * Copyright 2015-2016 Open Networking Laboratory
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
...@@ -60,3 +60,118 @@ ...@@ -60,3 +60,118 @@
60 .dark #app-dialog.floatpanel.dialog { 60 .dark #app-dialog.floatpanel.dialog {
61 background-color: #444; 61 background-color: #444;
62 } 62 }
63 +
64 +#application-details-panel.floatpanel {
65 + -moz-border-radius: 0;
66 + border-radius: 0;
67 + z-index: 0;
68 +}
69 +
70 +.light #application-details-panel.floatpanel {
71 + background-color: rgb(229, 234, 237);
72 +}
73 +.dark #application-details-panel.floatpanel {
74 + background-color: #3A4042;
75 +}
76 +
77 +#application-details-panel .container {
78 + padding: 0 12px;
79 +}
80 +
81 +#application-details-panel .close-btn {
82 + position: absolute;
83 + right: 10px;
84 + top: 0;
85 + cursor: pointer;
86 +}
87 +.light .close-btn svg.embeddedIcon .icon.plus .glyph {
88 + fill: #aaa;
89 +}
90 +.dark .close-btn svg.embeddedIcon .icon.plus .glyph {
91 + fill: #ccc;
92 +}
93 +
94 +#application-details-panel .dev-icon {
95 + display: inline-block;
96 + padding: 0 6px 0 0;
97 + vertical-align: middle;
98 +}
99 +.light .dev-icon svg.embeddedIcon .glyph {
100 + fill: rgb(0, 172, 229);
101 +}
102 +.dark .dev-icon svg.embeddedIcon .glyph {
103 + fill: #486D91;
104 +}
105 +
106 +#application-details-panel h2 {
107 + display: inline-block;
108 + margin: 8px 0;
109 +}
110 +
111 +#application-details-panel .top div.left {
112 + float: left;
113 + padding: 0 18px 0 0;
114 +}
115 +#application-details-panel .top div.right {
116 + display: inline-block;
117 +}
118 +
119 +#application-details-panel td.label {
120 + font-style: italic;
121 + padding-right: 12px;
122 + /* works for both light and dark themes ... */
123 + color: #777;
124 +}
125 +
126 +#application-details-panel .actionBtns div {
127 + padding: 12px 6px;
128 +}
129 +
130 +#application-details-panel .top hr {
131 + width: 95%;
132 + margin: 0 auto;
133 +}
134 +
135 +.light #application-details-panel hr {
136 + opacity: .5;
137 + border-color: #FFF;
138 +}
139 +.dark #application-details-panel hr {
140 + border-color: #666;
141 +}
142 +
143 +#application-details-panel .bottom table {
144 + border-spacing: 0;
145 +}
146 +
147 +#application-details-panel .bottom th {
148 + letter-spacing: 0.02em;
149 +}
150 +
151 +.light #application-details-panel .bottom th {
152 + background-color: #CCC;
153 + /* default text color */
154 +}
155 +.dark #application-details-panel .bottom th {
156 + background-color: #131313;
157 + color: #ccc;
158 +}
159 +
160 +#application-details-panel .bottom th,
161 +#application-details-panel .bottom td {
162 + padding: 6px 12px;
163 + text-align: center;
164 +}
165 +
166 +.light #application-details-panel .bottom tr:nth-child(odd) {
167 + background-color: #f9f9f9;
168 +}
169 +.light #application-details-panel .bottom tr:nth-child(even) {
170 + background-color: #EBEDF2;
171 +}
172 +.dark #application-details-panel .bottom tr:nth-child(odd) {
173 + background-color: #333;
174 +}
175 +.dark #application-details-panel .bottom tr:nth-child(even) {
176 + background-color: #555;
177 +}
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -67,7 +67,8 @@ ...@@ -67,7 +67,8 @@
67 <td class="table-icon"> 67 <td class="table-icon">
68 <div icon icon-id="{{app._iconid_state}}"></div> 68 <div icon icon-id="{{app._iconid_state}}"></div>
69 </td> 69 </td>
70 - <td><img data-ng-src="./rs/applications/{{app.icon}}/icon" height="28px" width="28px" /></td> 70 + <td><img data-ng-src="./rs/applications/{{app.icon}}/icon"
71 + height="28px" width="28px" /></td>
71 <td>{{app.id}}</td> 72 <td>{{app.id}}</td>
72 <td>{{app.version}}</td> 73 <td>{{app.version}}</td>
73 <td>{{app.category}}</td> 74 <td>{{app.category}}</td>
...@@ -80,4 +81,6 @@ ...@@ -80,4 +81,6 @@
80 81
81 </div> 82 </div>
82 83
84 + <application-details-panel></application-details-panel>
85 +
83 </div> 86 </div>
......
1 /* 1 /*
2 - * Copyright 2015 Open Networking Laboratory 2 + * Copyright 2015-2016 Open Networking Laboratory
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the 'License'); 4 * Licensed under the Apache License, Version 2.0 (the 'License');
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
...@@ -21,11 +21,32 @@ ...@@ -21,11 +21,32 @@
21 (function () { 21 (function () {
22 'use strict'; 22 'use strict';
23 23
24 + // injected refs
25 + var $log, $scope, $loc, fs, ps, wss, is, ns, ks, is;
26 +
27 + // internal state
28 + var detailsPanel,
29 + pStartY,
30 + pHeight,
31 + top,
32 + bottom,
33 + iconDiv,
34 + wSize = false;
35 +
24 // constants 36 // constants
25 var INSTALLED = 'INSTALLED', 37 var INSTALLED = 'INSTALLED',
26 ACTIVE = 'ACTIVE', 38 ACTIVE = 'ACTIVE',
27 appMgmtReq = 'appManagementRequest', 39 appMgmtReq = 'appManagementRequest',
40 + topPdg = 50,
41 + ctnrPdg = 24,
42 + winWidth = 500,
43 + scrollSize = 17,
44 + pName = 'application-details-panel',
45 + detailsReq = 'appDetailsRequest',
46 + detailsResp = 'appDetailsResponse',
28 fileUploadUrl = 'applications/upload', 47 fileUploadUrl = 'applications/upload',
48 + iconUrlPrefix = 'rs/applications/',
49 + iconUrlSuffix = '/icon',
29 dialogId = 'app-dialog', 50 dialogId = 'app-dialog',
30 dialogOpts = { 51 dialogOpts = {
31 edge: 'right' 52 edge: 'right'
...@@ -34,26 +55,199 @@ ...@@ -34,26 +55,199 @@
34 'org.onosproject.drivers': true 55 'org.onosproject.drivers': true
35 }, 56 },
36 discouragement = 'Deactivating or uninstalling this component can' + 57 discouragement = 'Deactivating or uninstalling this component can' +
37 - ' have serious negative consequences! Do so at your own risk!!'; 58 + ' have serious negative consequences! Do so at your own risk!!',
59 + propOrder = ['id', 'state', 'category', 'version', 'origin', 'role', 'url'],
60 + friendlyProps = ['App ID', 'State', 'Category', 'Version', 'Origin', 'Role', 'URL'];
61 +
62 + function createDetailsPane() {
63 + detailsPanel = ps.createPanel(pName, {
64 + width: wSize.width,
65 + margin: 0,
66 + hideMargin: 0
67 + });
68 + detailsPanel.el().style({
69 + position: 'absolute',
70 + top: pStartY + 'px'
71 + });
72 + $scope.hidePanel = function () { detailsPanel.hide(); };
73 + detailsPanel.hide();
74 + }
75 +
76 + function closePanel() {
77 + if (detailsPanel.isVisible()) {
78 + $scope.selId = null;
79 + detailsPanel.hide();
80 + return true;
81 + }
82 + return false;
83 + }
84 +
85 + function handleEscape() {
86 + return editNameCancel() || closePanel();
87 + }
88 +
89 + function addCloseBtn(div) {
90 + is.loadEmbeddedIcon(div, 'plus', 30);
91 + div.select('g').attr('transform', 'translate(25, 0) rotate(45)');
92 + div.on('click', closePanel);
93 + }
94 +
95 + function setUpPanel() {
96 + var container, closeBtn, tblDiv;
97 + detailsPanel.empty();
98 +
99 + container = detailsPanel.append('div').classed('container', true);
100 +
101 + top = container.append('div').classed('top', true);
102 + closeBtn = top.append('div').classed('close-btn', true);
103 + addCloseBtn(closeBtn);
104 + iconDiv = top.append('div').classed('dev-icon', true);
105 + top.append('h2');
106 +
107 + tblDiv = top.append('div').classed('top-tables', true);
108 + tblDiv.append('div').classed('left', true).append('table');
109 + tblDiv.append('div').classed('right', true).append('table');
110 + tblDiv.append('div').classed('readme', true).append('table');
111 +
112 + top.append('hr');
113 +
114 + // TODO: need add required applications and features
115 + bottom = container.append('div').classed('bottom', true);
116 + bottom.append('h2');
117 + bottom.append('table');
118 + }
119 +
120 + function addProp(tbody, index, value) {
121 + var tr = tbody.append('tr');
122 +
123 + function addCell(cls, txt) {
124 + tr.append('td').attr('class', cls).html(txt);
125 + }
126 + addCell('label', friendlyProps[index] + ' :');
127 + addCell('value', value);
128 + }
129 +
130 + function addIcon(tbody, value) {
131 + var tr = tbody.append('tr');
132 + var td = tr.append('td');
133 + td.append('img').attr('src', iconUrlPrefix + value + iconUrlSuffix);
134 + }
135 +
136 + function addReadme(tbody, value) {
137 + var tr = tbody.append('tr');
138 + tr.append('td').html(value);
139 + }
140 +
141 + function populateTop(tblDiv, details) {
142 + var leftTbl = tblDiv.select('.left')
143 + .select('table')
144 + .append('tbody'),
145 + rightTbl = tblDiv.select('.right')
146 + .select('table')
147 + .append('tbody'),
148 + readmeTbl = tblDiv.select('.readme')
149 + .select('table')
150 + .append('tbody');
151 +
152 + top.select('h2').html(details.name);
153 +
154 + // place application icon to the left table
155 + addIcon(leftTbl, details.id);
156 +
157 + // place rest of the fields to the right table
158 + propOrder.forEach(function (prop, i) {
159 + addProp(rightTbl, i, details[prop]);
160 + });
161 +
162 + // place readme field to the readme table
163 + addReadme(readmeTbl, details.readme);
164 + }
165 +
166 + function populateName(div, name) {
167 + var lab = div.select('.label'),
168 + val = div.select('.value');
169 + lab.html('Friendly Name:');
170 + val.html(name);
171 + }
172 +
173 + function populateDetails(details) {
174 + var nameDiv, topTbs, btmTbl, ports;
175 + setUpPanel();
176 +
177 + nameDiv = top.select('.name-div');
178 + topTbs = top.select('.top-tables');
179 + btmTbl = bottom.select('table');
180 +
181 + populateName(nameDiv, details.name);
182 + populateTop(topTbs, details);
183 + populateBottom(btmTbl);
184 +
185 + detailsPanel.height(pHeight);
186 + }
187 +
188 + function populateBottom(table) {
189 + var theader = table.append('thead').append('tr'),
190 + tbody = table.append('tbody'),
191 + tbWidth, tbHeight;
192 +
193 + tbWidth = fs.noPxStyle(tbody, 'width') + scrollSize;
194 + tbHeight = pHeight
195 + - (fs.noPxStyle(detailsPanel.el()
196 + .select('.top'), 'height'));
197 + table.style({
198 + height: tbHeight + 'px',
199 + width: tbWidth + 'px',
200 + overflow: 'auto',
201 + display: 'block'
202 + });
203 +
204 + detailsPanel.width(winWidth + ctnrPdg);
205 + }
206 +
207 + function respDetailsCb(data) {
208 + $scope.panelData = data.details;
209 + $scope.$apply();
210 + }
38 211
39 angular.module('ovApp', []) 212 angular.module('ovApp', [])
40 .controller('OvAppCtrl', 213 .controller('OvAppCtrl',
41 ['$log', '$scope', '$http', 214 ['$log', '$scope', '$http',
42 - 'FnService', 'TableBuilderService', 'WebSocketService', 'UrlFnService', 215 + 'FnService', 'TableBuilderService', 'PanelService', 'WebSocketService',
43 - 'KeyService', 'DialogService', 216 + 'IconService', 'UrlFnService', 'KeyService', 'DialogService',
44 217
45 - function ($log, $scope, $http, fs, tbs, wss, ufs, ks, ds) { 218 + function (_$log_, _$scope_, $http, _fs_, tbs, _ps_, _wss_, _is_, ufs, _ks_, ds) {
219 + $log = _$log_;
220 + $scope = _$scope_;
221 + wss = _wss_;
222 + ks = _ks_;
223 + fs = _fs_;
224 + ps = _ps_;
225 + is = _is_;
226 + $scope.panelData = {};
46 $scope.ctrlBtnState = {}; 227 $scope.ctrlBtnState = {};
47 $scope.uploadTip = 'Upload an application (.oar file)'; 228 $scope.uploadTip = 'Upload an application (.oar file)';
48 $scope.activateTip = 'Activate selected application'; 229 $scope.activateTip = 'Activate selected application';
49 $scope.deactivateTip = 'Deactivate selected application'; 230 $scope.deactivateTip = 'Deactivate selected application';
50 $scope.uninstallTip = 'Uninstall selected application'; 231 $scope.uninstallTip = 'Uninstall selected application';
51 232
233 + var handlers = {};
234 +
235 + // details panel handlers
236 + handlers[detailsResp] = respDetailsCb;
237 + wss.bindHandlers(handlers);
238 +
52 function selCb($event, row) { 239 function selCb($event, row) {
53 // $scope.selId is set by code in tableBuilder 240 // $scope.selId is set by code in tableBuilder
54 $scope.ctrlBtnState.selection = !!$scope.selId; 241 $scope.ctrlBtnState.selection = !!$scope.selId;
55 refreshCtrls(); 242 refreshCtrls();
56 ds.closeDialog(); // don't want dialog from previous selection 243 ds.closeDialog(); // don't want dialog from previous selection
244 +
245 + if ($scope.selId) {
246 + wss.sendEvent(detailsReq, { id: row.id });
247 + } else {
248 + $scope.hidePanel();
249 + }
250 + $log.debug('Got a click on:', row);
57 } 251 }
58 252
59 function refreshCtrls() { 253 function refreshCtrls() {
...@@ -94,7 +288,6 @@ ...@@ -94,7 +288,6 @@
94 ['scroll down', 'See more apps'] 288 ['scroll down', 'See more apps']
95 ]); 289 ]);
96 290
97 -
98 function createConfirmationText(action, itemId) { 291 function createConfirmationText(action, itemId) {
99 var content = ds.createDiv(); 292 var content = ds.createDiv();
100 content.append('p').text(action + ' ' + itemId); 293 content.append('p').text(action + ' ' + itemId);
...@@ -154,6 +347,7 @@ ...@@ -154,6 +347,7 @@
154 347
155 $scope.$on('$destroy', function () { 348 $scope.$on('$destroy', function () {
156 ks.unbindKeys(); 349 ks.unbindKeys();
350 + wss.unbindHandlers(handlers);
157 }); 351 });
158 352
159 $log.log('OvAppCtrl has been created'); 353 $log.log('OvAppCtrl has been created');
...@@ -190,5 +384,72 @@ ...@@ -190,5 +384,72 @@
190 }); 384 });
191 } 385 }
192 }; 386 };
387 + }])
388 +
389 + .directive('applicationDetailsPanel',
390 + ['$rootScope', '$window', '$timeout', 'KeyService',
391 + function ($rootScope, $window, $timeout, ks) {
392 + return function (scope) {
393 + var unbindWatch;
394 +
395 + function heightCalc() {
396 + pStartY = fs.noPxStyle(d3.select('.tabular-header'), 'height')
397 + + topPdg;
398 + wSize = fs.windowSize(pStartY);
399 + pHeight = wSize.height;
400 + }
401 +
402 + function initPanel() {
403 + heightCalc();
404 + createDetailsPane();
405 + $log.debug('start to initialize panel!');
406 + }
407 +
408 + // Safari has a bug where it renders the fixed-layout table wrong
409 + // if you ask for the window's size too early
410 + if (scope.onos.browser === 'safari') {
411 + $timeout(initPanel);
412 + } else {
413 + initPanel();
414 + }
415 + // create key bindings to handle panel
416 + ks.keyBindings({
417 + esc: [handleEscape, 'Close the details panel'],
418 + _helpFormat: ['esc']
419 + });
420 + ks.gestureNotes([
421 + ['click', 'Select a row to show application details'],
422 + ['scroll down', 'See more application']
423 + ]);
424 +
425 + // if the panelData changes
426 + scope.$watch('panelData', function () {
427 + if (!fs.isEmptyObject(scope.panelData)) {
428 + populateDetails(scope.panelData);
429 + detailsPanel.show();
430 + }
431 + });
432 +
433 + // if the window size changes
434 + unbindWatch = $rootScope.$watchCollection(
435 + function () {
436 + return {
437 + h: $window.innerHeight,
438 + w: $window.innerWidth
439 + };
440 + }, function () {
441 + if (!fs.isEmptyObject(scope.panelData)) {
442 + heightCalc();
443 + populateDetails(scope.panelData);
444 + }
445 + }
446 + );
447 +
448 + scope.$on('$destroy', function () {
449 + unbindWatch();
450 + ks.unbindKeys();
451 + ps.destroyPanel(pName);
452 + });
453 + };
193 }]); 454 }]);
194 }()); 455 }());
......