blob: 5e8d600827d01ec637744113555e52ce366fb1b2 [file] [log] [blame]
Thomas Vachuska0fa583c2015-03-30 23:07:41 -07001/*
Jian Lia54de5a2016-01-20 23:10:39 -08002 * Copyright 2015-2016 Open Networking Laboratory
Thomas Vachuska0fa583c2015-03-30 23:07:41 -07003 *
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -07004 * Licensed under the Apache License, Version 2.0 (the 'License');
Thomas Vachuska0fa583c2015-03-30 23:07:41 -07005 * 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
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -070011 * distributed under the License is distributed on an 'AS IS' BASIS,
Thomas Vachuska0fa583c2015-03-30 23:07:41 -070012 * 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 -- App View Module
19 */
20
21(function () {
22 'use strict';
23
Jian Lia54de5a2016-01-20 23:10:39 -080024 // 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
Bri Prebilic Cole522e7562015-06-22 15:56:25 -070036 // constants
37 var INSTALLED = 'INSTALLED',
38 ACTIVE = 'ACTIVE',
Simon Hunt8d28a552016-01-11 14:01:02 -080039 appMgmtReq = 'appManagementRequest',
Jian Lia54de5a2016-01-20 23:10:39 -080040 topPdg = 50,
41 ctnrPdg = 24,
42 winWidth = 500,
43 scrollSize = 17,
44 pName = 'application-details-panel',
45 detailsReq = 'appDetailsRequest',
46 detailsResp = 'appDetailsResponse',
Simon Hunt8d28a552016-01-11 14:01:02 -080047 fileUploadUrl = 'applications/upload',
Jian Lia54de5a2016-01-20 23:10:39 -080048 iconUrlPrefix = 'rs/applications/',
49 iconUrlSuffix = '/icon',
Simon Hunt8d28a552016-01-11 14:01:02 -080050 dialogId = 'app-dialog',
51 dialogOpts = {
52 edge: 'right'
Simon Hunt3f92c432016-01-12 17:34:23 -080053 },
54 strongWarning = {
55 'org.onosproject.drivers': true
56 },
57 discouragement = 'Deactivating or uninstalling this component can' +
Jian Lia54de5a2016-01-20 23:10:39 -080058 ' 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 }
Bri Prebilic Coleb699a162015-04-13 12:01:39 -0700211
Thomas Vachuska0fa583c2015-03-30 23:07:41 -0700212 angular.module('ovApp', [])
213 .controller('OvAppCtrl',
Bri Prebilic Cole522e7562015-06-22 15:56:25 -0700214 ['$log', '$scope', '$http',
Jian Lia54de5a2016-01-20 23:10:39 -0800215 'FnService', 'TableBuilderService', 'PanelService', 'WebSocketService',
216 'IconService', 'UrlFnService', 'KeyService', 'DialogService',
Thomas Vachuska0fa583c2015-03-30 23:07:41 -0700217
Jian Lia54de5a2016-01-20 23:10:39 -0800218 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 = {};
Bri Prebilic Cole6b95a3f2015-06-04 09:15:00 -0700227 $scope.ctrlBtnState = {};
Bri Prebilic Cole6c82ade2015-08-05 11:12:30 -0700228 $scope.uploadTip = 'Upload an application (.oar file)';
Bri Prebilic Coleeef67ae2015-07-01 16:26:59 -0700229 $scope.activateTip = 'Activate selected application';
230 $scope.deactivateTip = 'Deactivate selected application';
231 $scope.uninstallTip = 'Uninstall selected application';
Bri Prebilic Cole6b95a3f2015-06-04 09:15:00 -0700232
Jian Lia54de5a2016-01-20 23:10:39 -0800233 var handlers = {};
234
235 // details panel handlers
236 handlers[detailsResp] = respDetailsCb;
237 wss.bindHandlers(handlers);
238
Bri Prebilic Coleb699a162015-04-13 12:01:39 -0700239 function selCb($event, row) {
Simon Hunt3f92c432016-01-12 17:34:23 -0800240 // $scope.selId is set by code in tableBuilder
Bri Prebilic Cole6b95a3f2015-06-04 09:15:00 -0700241 $scope.ctrlBtnState.selection = !!$scope.selId;
Bri Prebilic Cole522e7562015-06-22 15:56:25 -0700242 refreshCtrls();
Simon Hunt3f92c432016-01-12 17:34:23 -0800243 ds.closeDialog(); // don't want dialog from previous selection
Jian Lia54de5a2016-01-20 23:10:39 -0800244
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);
Bri Prebilic Cole522e7562015-06-22 15:56:25 -0700251 }
252
Bri Prebilic Colea7f81e52015-06-23 10:11:08 -0700253 function refreshCtrls() {
254 var row, rowIdx;
255 if ($scope.ctrlBtnState.selection) {
256 rowIdx = fs.find($scope.selId, $scope.tableData);
257 row = rowIdx >= 0 ? $scope.tableData[rowIdx] : null;
258
259 $scope.ctrlBtnState.installed = row && row.state === INSTALLED;
260 $scope.ctrlBtnState.active = row && row.state === ACTIVE;
261 } else {
262 $scope.ctrlBtnState.installed = false;
263 $scope.ctrlBtnState.active = false;
264 }
Thomas Vachuska619c5382015-04-02 13:41:47 -0700265 }
Thomas Vachuska0fa583c2015-03-30 23:07:41 -0700266
Bri Prebilic Cole6b95a3f2015-06-04 09:15:00 -0700267 tbs.buildTable({
268 scope: $scope,
269 tag: 'app',
Bri Prebilic Cole522e7562015-06-22 15:56:25 -0700270 selCb: selCb,
Simon Hunta678b842016-01-11 17:14:18 -0800271 respCb: refreshCtrls,
272 // pre-populate sort so active apps are at the top of the list
273 sortParams: {
Simon Hunt051e9fa2016-01-19 15:54:22 -0800274 firstCol: 'state',
275 firstDir: 'desc',
276 secondCol: 'id',
277 secondDir: 'asc'
Simon Hunta678b842016-01-11 17:14:18 -0800278 }
Bri Prebilic Cole6b95a3f2015-06-04 09:15:00 -0700279 });
280
Bri Prebilic Cole9dcaea52015-07-21 14:39:48 -0700281 // TODO: reexamine where keybindings should be - directive or controller?
282 ks.keyBindings({
283 esc: [$scope.selectCallback, 'Deselect app'],
284 _helpFormat: ['esc']
285 });
286 ks.gestureNotes([
287 ['click row', 'Select / deselect app'],
288 ['scroll down', 'See more apps']
289 ]);
290
Simon Hunt3f92c432016-01-12 17:34:23 -0800291 function createConfirmationText(action, itemId) {
Simon Hunt8d28a552016-01-11 14:01:02 -0800292 var content = ds.createDiv();
Simon Hunt3f92c432016-01-12 17:34:23 -0800293 content.append('p').text(action + ' ' + itemId);
294 if (strongWarning[itemId]) {
295 content.append('p').text(discouragement).classed('strong', true);
296 }
Simon Hunt8d28a552016-01-11 14:01:02 -0800297 return content;
298 }
299
300 function confirmAction(action) {
Simon Hunt3f92c432016-01-12 17:34:23 -0800301 var itemId = $scope.selId,
Simon Hunt8d28a552016-01-11 14:01:02 -0800302 spar = $scope.sortParams;
303
304 function dOk() {
Simon Hunt3f92c432016-01-12 17:34:23 -0800305 $log.debug('Initiating', action, 'of', itemId);
Simon Hunt8d28a552016-01-11 14:01:02 -0800306 wss.sendEvent(appMgmtReq, {
307 action: action,
Simon Hunt3f92c432016-01-12 17:34:23 -0800308 name: itemId,
Simon Hunt8d28a552016-01-11 14:01:02 -0800309 sortCol: spar.sortCol,
310 sortDir: spar.sortDir
311 });
312 }
313
314 function dCancel() {
Simon Hunt3f92c432016-01-12 17:34:23 -0800315 $log.debug('Canceling', action, 'of', itemId);
Simon Hunt8d28a552016-01-11 14:01:02 -0800316 }
317
318 ds.openDialog(dialogId, dialogOpts)
319 .setTitle('Confirm Action')
Simon Hunt3f92c432016-01-12 17:34:23 -0800320 .addContent(createConfirmationText(action, itemId))
Simon Hunt8d28a552016-01-11 14:01:02 -0800321 .addButton('OK', dOk)
322 .addButton('Cancel', dCancel);
323 }
324
Bri Prebilic Cole522e7562015-06-22 15:56:25 -0700325 $scope.appAction = function (action) {
326 if ($scope.ctrlBtnState.selection) {
Simon Hunt8d28a552016-01-11 14:01:02 -0800327 confirmAction(action);
Bri Prebilic Cole522e7562015-06-22 15:56:25 -0700328 }
Bri Prebilic Colebd0bc772015-05-13 13:02:26 -0700329 };
Thomas Vachuska530e52a2015-05-06 19:51:32 -0700330
Bri Prebilic Cole522e7562015-06-22 15:56:25 -0700331 $scope.$on('FileChanged', function () {
332 var formData = new FormData();
333 if ($scope.appFile) {
334 formData.append('file', $scope.appFile);
Simon Hunt8d28a552016-01-11 14:01:02 -0800335 $http.post(ufs.rsUrl(fileUploadUrl), formData, {
Bri Prebilic Cole522e7562015-06-22 15:56:25 -0700336 transformRequest: angular.identity,
337 headers: {
338 'Content-Type': undefined
339 }
340 })
Bri Prebilic Colea7f81e52015-06-23 10:11:08 -0700341 .finally(function () {
Bri Prebilic Cole522e7562015-06-22 15:56:25 -0700342 $scope.sortCallback($scope.sortParams);
343 document.getElementById('inputFileForm').reset();
Bri Prebilic Cole522e7562015-06-22 15:56:25 -0700344 });
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -0700345 }
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -0700346 });
347
Bri Prebilic Cole9dcaea52015-07-21 14:39:48 -0700348 $scope.$on('$destroy', function () {
349 ks.unbindKeys();
Jian Lia54de5a2016-01-20 23:10:39 -0800350 wss.unbindHandlers(handlers);
Bri Prebilic Cole9dcaea52015-07-21 14:39:48 -0700351 });
352
Thomas Vachuska619c5382015-04-02 13:41:47 -0700353 $log.log('OvAppCtrl has been created');
Bri Prebilic Cole522e7562015-06-22 15:56:25 -0700354 }])
355
356 // triggers the input form to appear when button is clicked
357 .directive('triggerForm', function () {
358 return {
359 restrict: 'A',
360 link: function (scope, elem) {
361 elem.bind('click', function () {
362 document.getElementById('uploadFile')
Bri Prebilic Cole6c82ade2015-08-05 11:12:30 -0700363 .dispatchEvent(new MouseEvent('click'));
Bri Prebilic Cole522e7562015-06-22 15:56:25 -0700364 });
365 }
366 };
367 })
368
369 // binds the model file to the scope in scope.appFile
370 // sends upload request to the server
371 .directive('fileModel', ['$parse',
372 function ($parse) {
373 return {
374 restrict: 'A',
375 link: function (scope, elem, attrs) {
376 var model = $parse(attrs.fileModel),
377 modelSetter = model.assign;
378
379 elem.bind('change', function () {
380 scope.$apply(function () {
381 modelSetter(scope, elem[0].files[0]);
382 });
383 scope.$emit('FileChanged');
384 });
385 }
386 };
Jian Lia54de5a2016-01-20 23:10:39 -0800387 }])
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 };
454 }]);
Thomas Vachuska0fa583c2015-03-30 23:07:41 -0700455}());