16 var logger = Logger('package.js');
18 var $tree = $('#tic-package-left-col-tree');
27 function _getChecked() {
29 _.forEach(pkgInfo, function(value, key) {
30 if(value.checked === true) {
31 checkedList.push(value);
37 function _find(name, data) {
38 if (_.isEmpty(data)) {
44 function _findTreeNode(name) {
45 var regular = name.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
46 return $tree.treeview('findNodes', [ '^' + regular + '$' ])[0];
49 function _setDefaultPackage(defaultPackages) {
51 var pickDefault = _.pick(pkgInfo, defaultPackages);
52 _.forEach(pickDefault, function(value, key) {
53 // One or more of the same nodes can exist
54 _.forEach(value.view, function(node) {
61 $tree.treeview('checkNode', [nodes, { silent: true }]);
62 // _.forEach(nodes, function(node) {
63 // node.state.checked = true;
67 // Compares two rpm version numbers (e.g. "1.7.1" or "1.2b").
68 // This function is based on https://gist.github.com/em92/d58944f21c68b69433cefb6c49e0defd
69 function versionCompare(v1, v2, options) {
78 var v1parts = v1.split(/[.+-/_]/);
79 var v2parts = v2.split(/[.+-/_]/);
81 function compareParts(v1parts, v2parts, options) {
82 var zeroExtend = options && options.zeroExtend;
85 while (v1parts.length < v2parts.length) v1parts.push("0");
86 while (v2parts.length < v1parts.length) v2parts.push("0");
89 for (var i = 0; i < v1parts.length; ++i) {
90 if (v2parts.length === i) {
94 var v1part = parseInt(v1parts[i]);
95 var v2part = parseInt(v2parts[i]);
96 // (NaN == NaN) -> false
97 var v1part_is_string = !(v1part === v1part);
98 var v2part_is_string = !(v2part === v2part);
99 v1part = v1part_is_string ? v1parts[i] : v1part;
100 v2part = v2part_is_string ? v2parts[i] : v2part;
102 if (v1part_is_string === v2part_is_string) {
103 if (v1part_is_string === false) {
105 if (v1part === v2part) {
107 } else if (v1part > v2part) {
113 // letters and numbers in string
114 // split letters and numbers
115 var v1subparts = v1part.match(/[a-zA-Z]+|[0-9]+/g);
116 var v2subparts = v2part.match(/[a-zA-Z]+|[0-9]+/g);
117 if ( (v1subparts.length === 1) && (v2subparts.length === 1) ) {
118 // only letters in string
119 v1part = v1subparts[0];
120 v2part = v2subparts[0];
121 if (v1part === v2part) {
123 } else if (v1part > v2part) {
129 var result = compareParts(v1subparts, v2subparts);
137 return v2part_is_string ? 1 : -1;
141 if (v1parts.length != v2parts.length) {
147 return compareParts(v1parts, v2parts, options);
152 // analyze install-dependency (requires)
153 function _analyzeInstallDependency(checkNode) {
155 var pkg_count = Object.keys(pkgInfo).length
159 var selected = new Array(pkg_count);
160 var scc_id = new Array(pkg_count);
161 var min_num = new Array(pkg_count);
164 _.forEach(pkgInfo, function(value, key) {
165 // check type: -1(unchecked), 0(checked), 1~N(checking)
166 selected[value.id] = value.checked ? 0 : -1;
167 scc_id[value.id] = 0;
170 function _comapre_ver(ver1, ver2, flag) {
171 var epoch1 = ver1.epoch;
172 var epoch2 = ver2.epoch;
173 // epoch is a number (optional, default=0)
174 epoch1 = epoch1 ? epoch1 : 0;
175 epoch2 = epoch2 ? epoch2 : 0;
177 if (epoch1 === epoch2) {
178 var result = versionCompare(ver1.ver, ver2.ver);
179 // version match (ver1 == ver2)
181 // flag == true means to compare versions between require and provide
182 if (!flag || ver1.rel) {
183 return versionCompare(ver1.rel, ver2.rel);
188 } else if (epoch1 > epoch2) {
195 function _makeSCC(pkgId) {
200 while(!_.isEmpty(stack)) {
201 var pkg = stack.pop();
202 scc_id[pkg.id] = sccNum;
205 if (pkg.id === pkgId) {
210 // circular depependency
211 if (sccList.length > 1) {
213 var groupPkgList = [];
214 _.forEach(sccList, function(pkg) {
216 groupPkgList.push(pkg.name);
218 groups[groupId] = groupPkgList;
219 logger.info('circular dependency: groupId(' + groupId + '), list(' + groupPkgList + ')');
223 function _createReference(node1, node2) {
224 if (!_.isEmpty(node1.forward) && _.includes(node1.forward, node2.name))
227 if (!_.isEmpty(node1.forward)) {
228 node1.forward.push(node2.name);
230 node1.forward = [node2.name];
233 if (!_.isEmpty(node2.backward)) {
234 node2.backward.push(node1.name);
236 node2.backward = [node1.name];
240 function _select_rpm(capability, require) {
241 if (capability.length === 1) {
242 return pkgInfo[capability[0].name];
245 var provideList = [];
246 // 1. Choose the rpm included in version from provides
248 _.forEach(capability, function(provide) {
249 var cmpResult = _comapre_ver(require, provide.data, true);
250 if (cmpResult === 0 && _.includes(['EQ', 'GE', 'LE'], require.flags)) {
251 provideList.push(provide);
252 } else if (cmpResult === 1 && _.includes(['LT', 'LE'], require.flags)) {
253 provideList.push(provide);
254 } else if (cmpResult === -1 && _.includes(['GT', 'GE'], require.flags)) {
255 provideList.push(provide);
259 provideList = capability;
262 // error case (the rpm does not exist)
263 if (_.isEmpty(provideList)) {
267 if (provideList.length === 1) {
268 return pkgInfo[provideList[0].name];
271 // 2 Select one of the rpms by priority
272 // 2-1. Choose the default rpm or the selected rpm
273 // TODO: default profile rpm should be selected
274 _.forEach(provideList, function(provide) {
275 var tmpInfo = pkgInfo[provide.name];
276 if (tmpInfo.checked || selected[tmpInfo.id] >= 1) {
281 var maxVersion = null;
282 // # 2-2. Select the latest version of rpm
283 _.forEach(provideList, function(provide) {
285 var ret = _comapre_ver(maxVersion.data, provide.data)
287 maxVersion = provide;
290 maxVersion = provide;
294 return pkgInfo[maxVersion.name];
297 function _analyzeDep(node) {
300 selected[pkgId] = number;
301 min_num[pkgId] = number;
305 dependents[node.name] = node;
307 // installation dependency analysis of package
309 _.forEach(['requires'], function(depTag) {
310 if (_.has(node, depTag)) {
311 _.forEach(node.requires, function(require) {
314 if (_.has(provides, require.name)) {
315 var capList = provides[require.name];
316 choose = _select_rpm(capList, require);
317 } else if (_.has(files, require.name)) {
318 choose = pkgInfo[files[require.name][0]];
322 // add forward/backward reference
323 _createReference(node, choose);
325 if (selected[choose.id] === -1) {
326 var result = _analyzeDep(choose);
327 _.forEach(result, function(value, key) {
328 if (!_.has(dependents, key)) {
329 dependents[key] = value;
332 min_num[pkgId] = Math.min(min_num[pkgId], min_num[choose.id])
333 } else if (selected[choose.id] >= 1 && scc_id[choose.id] === 0) {
334 // cross edge that can not be ignored
335 min_num[pkgId] = Math.min(min_num[pkgId], min_num[choose.id])
338 logger.info('the capability('+require.name+')does not exist. should be checked for error');
344 if (min_num[pkgId] === selected[pkgId]) {
351 return _analyzeDep(checkNode);
354 // analyze uncheck dependency
355 function _analyzeUncheckDependency(uncheckNode) {
357 function _checkCircularDependency(node) {
358 var groupId = node.group
359 var groupPkgList = groups[groupId];
362 // Set object for group
363 _.forEach(groupPkgList, function(pkgName) {
364 groupObj[pkgName] = null;
367 var isUncheckable = true;
368 _.forEach(groupPkgList, function(pkgName) {
369 var pkg = _find(pkgName);
371 // the node is selfChecked or uncheckable
372 if (pkg.selfChecked || !isUncheckable) {
373 isUncheckable = false;
377 _.forEach(pkg.backward, function(backRef) {
378 // If node is Referenced by another node (Not a node in the group),
379 // unable to uncheck group nodes
380 if (!_.has(groupObj, backRef)) {
381 isUncheckable = false;
389 groupVisited[groupId] = {};
391 // Delete backward/forward reference of group node
392 _.forEach(groupPkgList, function(pkgName) {
393 var pkg = _find(pkgName);
397 groupVisited[groupId][pkg.name] = -1;
399 logger.info('Group(' + groupId + ') is uncheckable : ' + groupPkgList);
402 logger.info('Group(' + groupId + ') is Not uncheckable: ' + groupPkgList);
407 function _analyzeUncheck(parent, node) {
408 if (!_.isEmpty(parent)) {
410 if (!_.isEmpty(node.backward)) {
411 var bIndex = node.backward.indexOf(parent.name);
414 // remove backward reference (parent)
415 node.backward.splice(bIndex, 1);
418 // selfCheck node is not unchecked
419 if (node.selfChecked === true) {
425 var uncheckPkgs = null;
426 // Check that the selected node is uncheckable
427 if (!_.isEmpty(node.backward)) {
428 // check circular dependency
429 if (!node.group || !_checkCircularDependency(node)) {
434 // the selected node is uncheckable
436 uncheckPkgs[node.name] = node;
437 // uncheckable pkg of group
438 if (node.group && !_.isEmpty(groupVisited[node.group])) {
439 groupVisited[node.group][node.name] = 1;
442 // if selected node has forward references
443 if (!_.isEmpty(node.forward)) {
444 _.forEach(node.forward, function(fname) {
445 var forwardNode = _find(fname);
447 // If pkg has a circular dependency and is unchekcable,
448 // circular dep. pkgs can only be visited once
449 var fvisit = groupVisited[forwardNode.group];
450 if (!_.isEmpty(fvisit) && fvisit[fname] === 1) {
454 var result = _analyzeUncheck(node, forwardNode);
456 // updates pkgs for uncheck
457 if (!_.isEmpty(result)) {
458 _.forEach(result, function(value, key) {
459 if (!_.has(uncheckPkgs, key)) {
460 uncheckPkgs[key] = value;
465 // forward reference reset
473 // { 'group_id' : {pkgName: -1, ... }, ... }
474 var groupVisited = {};
475 var uncheckPkgs = _analyzeUncheck(null, uncheckNode);
477 // delete groupId from groups
478 _.forEach(groupVisited, function(groupList, groupId) {
479 if (!_.isEmpty(groups[groupId])) {
480 delete groups[groupId];
490 function _updateSummary() {
491 var pacakgeImageSize = $('#tic-package-image-size').empty();
492 var packageImageInstalledSize = $('#tic-package-image-installed-size').empty();
493 var packageListBadge = $('#tic-package-list-badge').empty();
494 var packageList = $('#tic-package-list').empty();
496 var list = _getChecked();
497 var count = _.size(list);
499 var imageSize = _.sumBy(list, function getImageSize(item) {
500 return _.toNumber(item.size || 0);
502 var imageInstalledSize = _.sumBy(list, function getImageInstalled(item) {
503 return _.toNumber(item.installed || 0);
506 if (_.isNumber(imageSize)) {
507 pacakgeImageSize.html(Util.bytesToSize(imageSize));
509 if (_.isNumber(imageInstalledSize)) {
510 packageImageInstalledSize.html(Util.bytesToSize(imageInstalledSize));
512 if (_.isNumber(count)) {
513 packageListBadge.html(count);
515 if (!_.isEmpty(list)) {
516 packageList.html(_.orderBy(_.map(list, 'name')).join('<br>'));
519 $('#tic-package-create').toggleClass('disabled', count === 0);
521 require('js/page/image').updateSummary();
525 * Treeview: A node is selected.
527 function _nodeSelected(event, node) {
528 var text = $('#tic-package-info-text').empty();
529 var version = $('#tic-package-info-version').empty();
530 var arch = $('#tic-package-info-arch').empty();
531 var size = $('#tic-package-info-size').empty();
532 var installedSize = $('#tic-package-info-installed-size').empty();
533 var summary = $('#tic-package-info-summary').empty();
534 var description = $('#tic-package-info-description').empty();
535 var dependency = $('#tic-package-info-dependency').empty();
536 var dependencyBadge = $('#tic-package-info-dependency-badge').empty();
538 var pkg = pkgInfo[node.text];
540 if (!_.isEmpty(pkg.name)) {
543 if (!_.isEmpty(pkg.version)) {
544 if (!_.isEmpty(pkg.version.rel)) {
545 version.html(pkg.version.ver + '-' + pkg.version.rel);
547 version.html(pkg.version.ver);
550 if (!_.isEmpty(pkg.arch)) {
553 if (!_.isEmpty(pkg.size)) {
554 size.html(Util.bytesToSize(pkg.size));
556 if (!_.isEmpty(pkg.installed)) {
557 installedSize.html(Util.bytesToSize(pkg.installed));
559 if (!_.isEmpty(pkg.summary)) {
560 summary.html(pkg.summary);
562 if (!_.isEmpty(pkg.description)) {
563 description.html(pkg.description);
565 // if (!_.isEmpty(info.dependency)) {
566 // dependencyBadge.html(info.dependency.length)
567 // dependency.html(_.orderBy(info.dependency).join('<br>'));
572 * Treeview: A node is checked.
574 function _onNodeChecked(event, node) {
575 var startTS = performance.now();
576 var localNode = _find(node.text);
578 if (_.isEmpty(localNode)){
579 // MISC is virtual pacakge.
582 if (localNode.checked === true) {
583 $tree.treeview('uncheckNode', [node, { silent: false }]);
587 logger.info('checked: ' + localNode.name);
589 _nodeSelected(event, node);
590 localNode.selfChecked = true;
592 // analyze install-dependency (requires)
593 var depPkg = _analyzeInstallDependency(localNode)
595 var analyzeTS = performance.now();
597 // TODO: temporary code
599 _.forEach(depPkg, function(value, key) { tempcode.push(value.name); });
600 logger.info(localNode.name + ' install-dependency(' + tempcode.length +'): ' + tempcode);
603 if (!_.isEmpty(depPkg)) {
604 _.forEach(depPkg, function(value, key) {
606 value.checked = true;
607 // update treeview data
608 _.forEach(value.view, function(node) {
609 if (node.state.checked === false) {
610 toggleNode.push(node);
615 $tree.treeview('checkNode', [toggleNode, { silent: true }]);
618 var endTS = performance.now();
619 logger.info('[Check] Total time: ' + (endTS - startTS) + 'ms');
620 logger.info('[Check] Analyze dep. time: ' + (analyzeTS - startTS) + 'ms');
621 logger.info('[Check] Update view time: ' + (endTS - analyzeTS) + 'ms');
626 * Treeview: A node is unchecked.
628 function _onNodeUnchecked(event, node) {
629 var startTS = performance.now();
630 var localNode = _find(node.text);
632 if (_.isEmpty(localNode)){
633 // MISC is virtual pacakge.
636 if (localNode.checked === false) {
637 $tree.treeview('checkNode', [node, { silent: false }]);
641 logger.info('unchecked: ' + node.text);
643 var uncheckPkgs = _analyzeUncheckDependency(localNode);
645 var analyzeTS = performance.now();
647 // TODO: temporary code
649 _.forEach(uncheckPkgs, function(value, key) { tempcode.push(value.name); });
650 logger.info(localNode.name + ' uncheck-dependency(' + tempcode.length +'): ' + tempcode);
653 if (!_.isEmpty(uncheckPkgs)) {
654 _.forEach(uncheckPkgs, function(value, key) {
656 value.checked = false;
657 // update treeview data
658 _.forEach(value.view, function(node) {
659 if (node.state.checked === true) {
660 toggleNode.push(node);
665 $tree.treeview('uncheckNode', [toggleNode, { silent: true }]);
666 localNode.selfChecked = false;
669 Util.showAlertDialog('Could not uncheck the \'' +localNode.name + '\'' + '<br> because the \'' + localNode.backward + '\' packages depends on it');
671 // selected node change to check state
672 $tree.treeview('checkNode', [[node], { silent: true }]);
675 var endTS = performance.now();
676 logger.info('[Uncheck] Total time: ' + (endTS - startTS) + 'ms');
677 logger.info('[Uncheck] Analyze dep. time: ' + (analyzeTS - startTS) + 'ms');
678 logger.info('[Uncheck] Update view time: ' + (endTS - analyzeTS) + 'ms');
682 * Treeview: Initialize tree data.
684 function updatePackageTree(rawData) {
685 repos = rawData.repos;
686 pkgInfo = rawData.data.packages
687 provides = rawData.data.provides;
688 files = rawData.data.files;
689 groups = rawData.data.groups;
691 _.forEach(groups, function(value, key) {
695 return new Promise(function (resolve, reject) {
697 function _onRendered(event, nodes) {
698 packages = _.values(nodes);
699 _.forEach(nodes, function(node, key) {
700 // add a reference variable for treeview
701 var pkg = pkgInfo[node.text]
703 if (_.isEmpty(pkg.view)) {
711 _setDefaultPackage(rawData.defaultpackages);
715 function _onNodeUnselected(event, node) {
716 $('#tic-package-info-text').empty();
717 $('#tic-package-info-version').empty();
718 $('#tic-package-info-arch').empty();
719 $('#tic-package-info-size').empty();
720 $('#tic-package-info-installed-size').empty();
721 $('#tic-package-info-summary').empty();
722 $('#tic-package-info-description').empty();
723 $('#tic-package-info-dependency').empty();
724 $('#tic-package-info-dependency-badge').empty();
731 onRendered: _onRendered,
732 onNodeSelected: _nodeSelected,
733 onNodeUnselected: _onNodeUnselected,
734 onNodeChecked: _onNodeChecked,
735 onNodeUnchecked: _onNodeUnchecked
744 var filterText = $('#tic-package-toolbar-input').val();
745 var matchNodes = $tree.treeview('search', [ filterText, {
746 ignoreCase: true, // case insensitive
747 exactMatch: false, // like or equals
748 revealResults: true, // reveal matching nodes
751 _.forEach(packages, function (node) {
754 if (!_.isEmpty(matchNodes)) {
755 _.forEach(matchNodes, function (node) {
759 if (_.isEmpty(filterText)) {
760 _.forEach(packages, function (node) {
765 $('#tic-package-toolbar-input-clear').toggleClass('hidden', _.isEmpty(filterText));
767 $('#tic-package-toolbar-input').on('input change', _filter);
769 function _inputClearBtnHandler() {
770 $('#tic-package-toolbar-input').val('').trigger('change').focus();
771 $(this).toggleClass('hidden', true);
773 $('#tic-package-toolbar-input-clear').on('click', _inputClearBtnHandler);
775 function _checkAllBtnHandler() {
776 $tree.treeview('checkAll', { silent: true });
777 _.forEach(pkgInfo, function(value, key){
778 value.checked = true;
779 value.selfChecked = false;
783 $('#tic-package-toolbar-checkall').on('click', _checkAllBtnHandler);
785 function _uncheckAllBtnHandler() {
786 // FIXME: bug for state.checked = false in treeview objects
787 $tree.treeview('checkAll', { silent: true });
788 $tree.treeview('uncheckAll', { silent: true });
789 _.forEach(pkgInfo, function(value, key){
790 value.checked = false;
791 value.selfChecked = false;
792 value.forward = null;
793 value.backward = null;
799 $('#tic-package-toolbar-uncheckall').on('click', _uncheckAllBtnHandler);
801 function _collapseAll() {
802 $tree.treeview('collapseAll');
804 $('#tic-package-left-col-tree-toolbar-collapse-all').on('click', _collapseAll);
806 function _expandAll() {
807 $tree.treeview('expandAll');
809 $('#tic-package-left-col-tree-toolbar-expand-all').on('click', _expandAll);
811 // Patch: Bootstrap dropdown menu not working (not dropping down when clicked)
812 $('.dropdown-toggle').dropdown();
819 * Initialize for treeview
820 * @method updatePackageTree
821 * @param {array} array of objects
824 updatePackageTree: updatePackageTree,
827 * Get checked package nodes
828 * @method getCheckedPackages
829 * @return {array} array of objects
831 getCheckedPackages: _getChecked