// vim: set tabstop=4 expandtab ai: // BitBake Toaster Implementation // // Copyright (C) 2013 Intel Corporation // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. angular_formpost = function($httpProvider) { // Use x-www-form-urlencoded Content-Type // By Ezekiel Victor, http://victorblog.com/2012/12/20/make-angularjs-http-service-behave-like-jquery-ajax/, no license, with attribution $httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8'; /** * The workhorse; converts an object to x-www-form-urlencoded serialization. * @param {Object} obj * @return {String} */ var param = function(obj) { var query = '', name, value, fullSubName, subName, subValue, innerObj, i; for(name in obj) { value = obj[name]; if(value instanceof Array) { for(i=0; i elem.orderid) break; } if (!found) { $scope.layers.splice(i, 0, elem); addedLayers.push(elem); } }); // step 3 - display alerts. if (addedLayers.length > 0) { $scope.displayAlert($scope.zone2alerts, "You have added "+addedLayers.length+" layer" + ((addedLayers.length>1)?"s: ":": ") + addedLayers.map(function (e) { return ""+e.name+""; }).join(", "), "alert-info"); // invalidate error layer data based on current layers $scope.layersForTargets = {}; } if (deletedLayers.length > 0) { $scope.displayAlert($scope.zone2alerts, "You have deleted "+deletedLayers.length+" layer" + ((deletedLayers.length>1)?"s: ":": ") + deletedLayers.map(function (e) { return ""+e.name+""; }).join(", "), "alert-info"); // invalidate error layer data based on current layers $scope.layersForTargets = {}; } } if (_data.builds !== undefined) { var toDelete = []; // step 1 - delete entries not found $scope.builds.forEach(function (elem) { if (-1 == _data.builds.findIndex(function (elemX) { return elemX.id == elem.id && elemX.status == elem.status; })) { toDelete.push(elem); } }); toDelete.forEach(function (elem) { $scope.builds.splice($scope.builds.indexOf(elem),1); }); // step 2 - merge new entries _data.builds.forEach(function (elem) { var found = false; var i = 0; for (i = 0 ; i < $scope.builds.length; i ++) { if ($scope.builds[i].id > elem.id) continue; if ($scope.builds[i].id == elem.id) { found=true; // do deep data copy for (var attr in elem) { $scope.builds[i][attr] = elem[attr]; } break; } if ($scope.builds[i].id < elem.id) break; } if (!found) { $scope.builds.splice(i, 0, elem); } }); // step 3 - merge "Canceled" builds $scope.canceledBuilds.forEach(function (elem) { // mock the build object var found = false; var i = 0; for (i = 0; i < $scope.builds.length; i ++) { if ($scope.builds[i].id > elem.id) continue; if ($scope.builds[i].id == elem.id) { found=true; break; } if ($scope.builds[i].id < elem.id) break; } if (!found) { $scope.builds.splice(i, 0, elem); } }); $scope.fetchLayersForTargets(); } if (_data.targets !== undefined) { $scope.targets = _data.targets; } if (_data.machine !== undefined) { $scope.machine = _data.machine; } if (_data.user !== undefined) { $scope.user = _data.user; } if (_data.prj !== undefined) { $scope.project = _data.prj; // update breadcrumb, outside the controller $('#project_name').text($scope.project.name); } $scope.validateData(); inXHRcall = false; deffered.resolve(_data); } }).error(function(_data, _status, _headers, _config) { if (_status === 0) { // the server has gone away // alert("The server is not responding. The application will terminate now"); $interval.cancel($scope.pollHandle); } else { console.error("Failed HTTP XHR request: ", _data, _status, _headers, _config); inXHRcall = false; deffered.reject(_data.error); } }); return deffered.promise; }; $scope.layeralert = undefined; /** * Verifies and shows user alerts on invalid project data */ $scope.validateData = function () { if ($scope.layers.length === 0) { $scope.layeralert = $scope.displayAlert($scope.zone1alerts, "You need to add some layers to this project. View all layers available in Toaster or import a layer"); } else { if ($scope.layeralert !== undefined) { $scope.layeralert.close(); $scope.layeralert = undefined; } } }; $scope.buildExistingTarget = function(targets) { $scope.buildTargetList(targets.map(function(v){return v.target;})); }; $scope.buildTargetList = function(targetlist) { var oldTargetName = $scope.targetName; $scope.targetName = targetlist.join(' '); $scope.buildNamedTarget(); $scope.targetName = oldTargetName; }; $scope.buildNamedTarget = function() { if ($scope.targetName === undefined && $scope.targetName1 === undefined){ console.warn("No target defined, please type in a target name"); return; } // this writes the $scope.safeTargetName variable $scope.sanitizeTargetName(); $scope._makeXHRCall({ method: "POST", url: $scope.urls.xhr_build, data : { targets: $scope.safeTargetName, } }).then(function (data) { // make sure nobody re-uses the current $safeTargetName delete $scope.safeTargetName; console.warn("TRC3: received ", data); $scope.targetName = undefined; $scope.targetName1 = undefined; $location.hash('buildslist'); // call $anchorScroll() $anchorScroll(); }); }; $scope.sanitizeTargetName = function() { $scope.safeTargetName = undefined; if (undefined === $scope.targetName) $scope.safeTargetName = $scope.targetName1; if (undefined === $scope.targetName1) $scope.safeTargetName = $scope.targetName; if (undefined === $scope.safeTargetName) return; $scope.safeTargetName = $scope.safeTargetName.replace(/\[.*\]/, '').trim(); }; $scope.buildCancel = function(build) { $scope._makeXHRCall({ method: "POST", url: $scope.urls.xhr_build, data: { buildCancel: build.id, } }).then( function () { build.status = "deleted"; $scope.canceledBuilds.push(build); }); }; $scope.buildDelete = function(build) { $scope.canceledBuilds.splice($scope.canceledBuilds.indexOf(build), 1); }; $scope.onLayerSelect = function (item) { $scope.layerAddId = item.id; }; $scope.layerAddById = function (id) { $scope.layerAddId = id; $scope.layerAdd(); }; $scope.layerAdd = function() { $http({method:"GET", url: $scope.urls.xhr_datatypeahead, params : { type: "layerdeps", value: $scope.layerAddId }}) .success(function (_data) { if (_data.error != "ok") { console.warn(_data.error); } else { if (_data.list.length > 0) { // activate modal var modalInstance = $modal.open({ templateUrl: 'dependencies_modal', controller: function ($scope, $modalInstance, items, layerAddName) { $scope.items = items; $scope.layerAddName = layerAddName; $scope.selectedItems = (function () { s = {}; for (var i = 0; i < items.length; i++) { s[items[i].id] = true; } return s; })(); $scope.ok = function() { console.warn("TRC4: scope selected is ", $scope.selectedItems); $modalInstance.close(Object.keys($scope.selectedItems).filter(function (e) { return $scope.selectedItems[e];})); }; $scope.cancel = function() { $modalInstance.dismiss('cancel'); }; $scope.update = function() { console.warn("TRC5: updated ", $scope.selectedItems); }; }, resolve: { items: function () { return _data.list; }, layerAddName: function () { return $scope.layerAddName; }, } }); modalInstance.result.then(function (selectedArray) { selectedArray.push($scope.layerAddId); console.warn("TRC6: selected", selectedArray); $scope._makeXHRCall({ method: "POST", url: $scope.urls.xhr_edit, data: { layerAdd: selectedArray.join(","), } }).then(function () { $scope.layerAddName = undefined; }); }); } else { $scope._makeXHRCall({ method: "POST", url: $scope.urls.xhr_edit, data: { layerAdd: $scope.layerAddId, } }).then(function () { $scope.layerAddName = undefined; }); } } }); }; $scope.layerDel = function(id) { $scope._makeXHRCall({ method: "POST", url: $scope.urls.xhr_edit, data: { layerDel: id, } }); }; /** * Verifies if a project settings change would trigger layer updates. If user confirmation is needed, * a modal dialog will prompt the user to ack the changes. If not, the editProjectSettings() function is called directly. * * Only "versionlayers" change for is supported (and hardcoded) for now. */ $scope.testProjectSettingsChange = function(elementid) { if (elementid != '#change-project-version') throw "Not implemented"; $http({method:"GET", url: $scope.urls.xhr_datatypeahead, params : { type: "versionlayers", value: $scope.projectVersion }}). success(function (_data) { if (_data.error != "ok") { alert (_data.error); } else { if (_data.list.length > 0) { // activate modal var modalInstance = $modal.open({ templateUrl: 'change_version_modal', controller: function ($scope, $modalInstance, items, releaseName) { $scope.items = items; $scope.releaseName = releaseName; $scope.ok = function() { $modalInstance.close(); }; $scope.cancel = function() { $modalInstance.dismiss('cancel'); }; }, resolve: { items: function () { return _data.list; }, releaseName: function () { return $scope.releases.filter(function (e) { if (e.id == $scope.projectVersion) return e;})[0].name; }, } }); modalInstance.result.then(function () { $scope.editProjectSettings(elementid); }); } else { $scope.editProjectSettings(elementid); } } }); }; /** * Performs changes to project settings, and updates the user interface accordingly. */ $scope.editProjectSettings = function(elementid) { var data = {}; console.warn("TRC7: editProjectSettings with ", elementid); var alertText; var alertZone; var oldLayers = []; switch(elementid) { case '#select-machine': alertText = "You have changed the machine to: " + $scope.machineName + ""; alertZone = $scope.zone2alerts; data.machineName = $scope.machineName; break; case '#change-project-name': data.projectName = $scope.projectName; alertText = "You have changed the project name to: " + $scope.projectName + ""; alertZone = $scope.zone3alerts; break; case '#change-project-version': data.projectVersion = $scope.projectVersion; alertText = "You have changed the release to: "; alertZone = $scope.zone3alerts; // save old layers oldLayers = $scope.layers.slice(0); break; default: throw "FIXME: implement conversion for element " + elementid; } $scope._makeXHRCall({ method: "POST", url: $scope.urls.xhr_edit, data: data, }).then( function (_data) { $scope.toggle(elementid); if (data.projectVersion !== undefined) { alertText += "" + $scope.project.release.desc + ". "; } if (elementid == '#change-project-version') { $scope.layersForTargets = {}; // invalidate error layers for the targets, since layers changed // requirement https://bugzilla.yoctoproject.org/attachment.cgi?id=2229, notification for changed version to include layers $scope.zone2alerts.forEach(function (e) { e.close(); }); // warnings - this is executed AFTER the generic XHRCall handling is done; at this point, if (_data.layers !== undefined) { // show added/deleted layer notifications; scope.layers is already updated by this point. var addedLayers = []; var deletedLayers = []; _diffArrays( oldLayers, $scope.layers, function (e, f) { return e.id == f.id; }, function (e) {addedLayers.push(e); }, function (e) {deletedLayers.push(e); }); var hasDifferentLayers = (addedLayers.length || deletedLayers.length) if (hasDifferentLayers) { alertText += "This has caused the following changes in your project layers:"; } } $scope.displayAlert(alertZone, alertText, "alert-info"); }); }; /** * Extracts a command passed through the local path in location, and executes/updates UI based on the command */ $scope.updateDisplayWithCommands = function() { function _cmdExecuteWithParam(param, f) { var cmd = $location.path(); if (cmd.indexOf(param) === 0) { if (cmd.indexOf("=") > -1) { var parameter = cmd.split("=", 2)[1]; if (parameter !== undefined && parameter.length > 0) { f(parameter); } } else { f(); } } } _cmdExecuteWithParam("/newproject", function () { $scope.displayAlert($scope.zone1alerts, "Your project " + $scope.project.name + " has been created. You can now add layers and select targets you want to build.", "alert-success"); }); _cmdExecuteWithParam("/machineselected", function () { $scope.displayAlert($scope.zone2alerts, "You have changed the machine to: " + $scope.machine.name + "", "alert-info"); var machineDistro = angular.element("#machine-distro"); angular.element("html, body").animate({ scrollTop: machineDistro.position().top }, 700).promise().done(function() { $animate.addClass(machineDistro, "machines-highlight"); }); }); _cmdExecuteWithParam("/layerimported", function () { var imported = $cookieStore.get("layer-imported-alert"); var text; if (!imported) return; if (imported.deps_added.length === 0) { text = "You have imported "+imported.imported_layer.name+ " and added it to your project."; } else { var links = ""+imported.imported_layer.name+ ", "; imported.deps_added.map (function(item, index){ links +=""+item.name+ ""; /*If we're at the last element we don't want the trailing comma */ if (imported.deps_added[index+1] !== undefined) links += ", "; }); /* Length + 1 here to do deps + the imported layer */ text = "You have imported "+imported.imported_layer.name+ " and added "+(imported.deps_added.length+1)+ " layers to your project: "+links+""; } $scope.displayAlert($scope.zone2alerts, text, "alert-info"); // This doesn't work $cookieStore.remove("layer-imported-alert"); //use jquery plugin instead $.removeCookie("layer-imported-alert", { path: "/"}); }); _cmdExecuteWithParam("/targetbuild=", function (targets) { var oldTargetName = $scope.targetName; $scope.targetName = targets.split(",").join(" "); $scope.buildNamedTarget(); $scope.targetName = oldTargetName; $location.path(''); }); _cmdExecuteWithParam("/machineselect=", function (machine) { $scope.machineName = machine; $scope.toggle('#select-machine'); }); _cmdExecuteWithParam("/layeradd=", function (layer) { angular.forEach(layer.split(","), function (l) { $scope.layerAddId = l; $scope.layerAdd(); }); }); }; /** * Utility function to display an alert to the user */ $scope.displayAlert = function(zone, text, type) { if (zone.maxid === undefined) { zone.maxid = 0; } var crtid = zone.maxid ++; angular.forEach(zone, function (o) { o.close(); }); o = { id: crtid, text: text, type: type, close: function() { zone.splice((function(id) { for (var i = 0; i < zone.length; i++) if (id == zone[i].id) { return i; } return undefined; }) (crtid), 1); }, }; zone.push(o); return o; }; /** * Toggles display items between label and input box (the edit pencil icon) on selected settings in project page */ $scope.toggle = function(id) { $scope.projectName = $scope.project.name; $scope.projectVersion = $scope.project.release.id; $scope.machineName = $scope.machine.name; angular.element(id).toggle(); angular.element(id+"-opposite").toggle(); }; /** * Functionality related to "Most build targets" */ $scope.enableBuildSelectedTargets = function () { var keys = Object.keys($scope.mostBuiltTargets); keys = keys.filter(function (e) { if ($scope.mostBuiltTargets[e]) return e; }); return keys.length === 0; }; $scope.buildSelectedTargets = function () { var keys = Object.keys($scope.mostBuiltTargets); keys = keys.filter(function (e) { if ($scope.mostBuiltTargets[e]) return e; }); $scope.buildTargetList(keys); for (var i = 0; i < keys.length; i++) { $scope.mostBuiltTargets[keys[i]] = 0; } }; /** * Helper function to deal with error string recognition and manipulation */ $scope.getTargetNameFromErrorMsg = function (msg) { return msg.split(" ").splice(2).map(function (v) { return v.replace(/'/g, ''); }); }; /** * Utility function to retrieve which layers can be added to the project if the target was not * provided by any of the existing project layers */ $scope.fetchLayersForTargets = function () { $scope.builds.forEach(function (buildrequest) { buildrequest.errors.forEach(function (error) { if (error.msg.indexOf("Nothin") === 0) { $scope.getTargetNameFromErrorMsg(error.msg).forEach(function (target) { if ($scope.layersForTargets[target] === undefined) $scope.getAutocompleteSuggestions("layers4target", target).then( function (list) { $scope.layersForTargets[target] = list; }); }); } }); }); }; /** * Page init code - just init variables and set the automated refresh */ $scope.init = function() { $scope.canceledBuilds = []; $scope.layersForTargets = {}; $scope.fetchLayersForTargets(); $scope.pollHandle = $interval(function () { $scope._makeXHRCall({method: "GET", url: $scope.urls.xhr_edit, data: undefined});}, 2000, 0); }; }); var _testing_scope; function test_set_alert(text) { _testing_scope = angular.element("div#main").scope(); _testing_scope.displayAlert(_testing_scope.zone3alerts, text); console.warn("TRC8: zone3alerts", _testing_scope.zone3alerts); _testing_scope.$digest(); }