summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bitbake/lib/toaster/toastergui/static/js/projectapp.js234
-rw-r--r--bitbake/lib/toaster/toastergui/templates/project.html50
-rwxr-xr-xbitbake/lib/toaster/toastergui/views.py97
3 files changed, 246 insertions, 135 deletions
diff --git a/bitbake/lib/toaster/toastergui/static/js/projectapp.js b/bitbake/lib/toaster/toastergui/static/js/projectapp.js
index 767ea13a7e..0bdc55a733 100644
--- a/bitbake/lib/toaster/toastergui/static/js/projectapp.js
+++ b/bitbake/lib/toaster/toastergui/static/js/projectapp.js
@@ -100,6 +100,16 @@ function _diffArrays(existingArray, newArray, compareElements, onAdded, onDelete
100 100
101} 101}
102 102
103// add Array findIndex if not there
104
105if (Array.prototype.findIndex === undefined) {
106 Array.prototype.findIndex = function (callback) {
107 var i = 0;
108 for ( i = 0; i < this.length; i++ )
109 if (callback(this[i], i, this)) return i;
110 return -1;
111 }
112}
103 113
104var projectApp = angular.module('project', ['ngCookies', 'ngAnimate', 'ui.bootstrap', 'ngRoute', 'ngSanitize'], angular_formpost); 114var projectApp = angular.module('project', ['ngCookies', 'ngAnimate', 'ui.bootstrap', 'ngRoute', 'ngSanitize'], angular_formpost);
105 115
@@ -126,11 +136,17 @@ projectApp.filter('timediff', function() {
126 } 136 }
127}); 137});
128 138
139/**
140 * main controller for the project page
141 */
129 142
130// main controller for the project page
131projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $location, $cookies, $cookieStore, $q, $sce, $anchorScroll, $animate, $sanitize) { 143projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $location, $cookies, $cookieStore, $q, $sce, $anchorScroll, $animate, $sanitize) {
132 144
133 $scope.getSuggestions = function(type, currentValue) { 145 /**
146 * Retrieves text suggestions for text-edit drop down autocomplete boxes
147 */
148
149 $scope.getAutocompleteSuggestions = function(type, currentValue) {
134 var deffered = $q.defer(); 150 var deffered = $q.defer();
135 151
136 $http({method:"GET", url: $scope.urls.xhr_datatypeahead, params : { type: type, value: currentValue}}) 152 $http({method:"GET", url: $scope.urls.xhr_datatypeahead, params : { type: type, value: currentValue}})
@@ -147,17 +163,19 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
147 163
148 var inXHRcall = false; 164 var inXHRcall = false;
149 165
150 // default handling of XHR calls that handles errors and updates commonly-used pages 166 /**
167 * XHR call wrapper that automatically handles errors and auto-updates the page content to reflect project state on server side.
168 */
151 $scope._makeXHRCall = function(callparams) { 169 $scope._makeXHRCall = function(callparams) {
152 if (inXHRcall) { 170 if (inXHRcall) {
153 if (callparams.data === undefined) { 171 if (callparams.data === undefined) {
154 // we simply skip the data refresh calls 172 // we simply skip the data refresh calls
155 console.warn("race on XHR, aborted"); 173 console.warn("TRC1: race on XHR, aborted");
156 return; 174 return;
157 } else { 175 } else {
158 // we return a promise that we'll solve by reissuing the command later 176 // we return a promise that we'll solve by reissuing the command later
159 var delayed = $q.defer(); 177 var delayed = $q.defer();
160 console.warn("race on XHR, delayed"); 178 console.warn("TRC2: race on XHR, delayed");
161 $interval(function () {$scope._makeXHRCall(callparams).then(function (d) { delayed.resolve(d); });}, 100, 1); 179 $interval(function () {$scope._makeXHRCall(callparams).then(function (d) { delayed.resolve(d); });}, 100, 1);
162 180
163 return delayed.promise; 181 return delayed.promise;
@@ -178,33 +196,6 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
178 deffered.reject(_data.error); 196 deffered.reject(_data.error);
179 } 197 }
180 else { 198 else {
181
182 if (_data.builds !== undefined) {
183 var toDelete = [];
184 // step 1 - delete entries not found
185 $scope.builds.forEach(function (elem) {
186 if (-1 == _data.builds.findIndex(function (elemX) { return elemX.id == elem.id && elemX.status == elem.status; })) {
187 toDelete.push(elem);
188 }
189 });
190 toDelete.forEach(function (elem) {
191 $scope.builds.splice($scope.builds.indexOf(elem),1);
192 });
193 // step 2 - merge new entries
194 _data.builds.forEach(function (elem) {
195 var found = false;
196 var i = 0;
197 for (i = 0 ; i < $scope.builds.length; i ++) {
198 if ($scope.builds[i].id > elem.id) continue;
199 if ($scope.builds[i].id == elem.id) { found=true; break;}
200 if ($scope.builds[i].id < elem.id) break;
201 }
202 if (!found) {
203 $scope.builds.splice(i, 0, elem);
204 }
205 });
206
207 }
208 if (_data.layers !== undefined) { 199 if (_data.layers !== undefined) {
209 200
210 var addedLayers = []; 201 var addedLayers = [];
@@ -239,12 +230,59 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
239 // step 3 - display alerts. 230 // step 3 - display alerts.
240 if (addedLayers.length > 0) { 231 if (addedLayers.length > 0) {
241 $scope.displayAlert($scope.zone2alerts, "You have added <b>"+addedLayers.length+"</b> layer" + ((addedLayers.length>1)?"s: ":": ") + addedLayers.map(function (e) { return "<a href=\""+e.layerdetailurl+"\">"+e.name+"</a>" }).join(", "), "alert-info"); 232 $scope.displayAlert($scope.zone2alerts, "You have added <b>"+addedLayers.length+"</b> layer" + ((addedLayers.length>1)?"s: ":": ") + addedLayers.map(function (e) { return "<a href=\""+e.layerdetailurl+"\">"+e.name+"</a>" }).join(", "), "alert-info");
233 // invalidate error layer data based on current layers
234 $scope.layersForTargets = {};
242 } 235 }
243 if (deletedLayers.length > 0) { 236 if (deletedLayers.length > 0) {
244 $scope.displayAlert($scope.zone2alerts, "You have deleted <b>"+deletedLayers.length+"</b> layer" + ((deletedLayers.length>1)?"s: ":": ") + deletedLayers.map(function (e) { return "<a href=\""+e.layerdetailurl+"\">"+e.name+"</a>" }).join(", "), "alert-info"); 237 $scope.displayAlert($scope.zone2alerts, "You have deleted <b>"+deletedLayers.length+"</b> layer" + ((deletedLayers.length>1)?"s: ":": ") + deletedLayers.map(function (e) { return "<a href=\""+e.layerdetailurl+"\">"+e.name+"</a>" }).join(", "), "alert-info");
238 // invalidate error layer data based on current layers
239 $scope.layersForTargets = {};
245 } 240 }
246 241
247 } 242 }
243
244
245 if (_data.builds !== undefined) {
246 var toDelete = [];
247 // step 1 - delete entries not found
248 $scope.builds.forEach(function (elem) {
249 if (-1 == _data.builds.findIndex(function (elemX) { return elemX.id == elem.id && elemX.status == elem.status; })) {
250 toDelete.push(elem);
251 }
252 });
253 toDelete.forEach(function (elem) {
254 $scope.builds.splice($scope.builds.indexOf(elem),1);
255 });
256 // step 2 - merge new entries
257 _data.builds.forEach(function (elem) {
258 var found = false;
259 var i = 0;
260 for (i = 0 ; i < $scope.builds.length; i ++) {
261 if ($scope.builds[i].id > elem.id) continue;
262 if ($scope.builds[i].id == elem.id) { found=true; break;}
263 if ($scope.builds[i].id < elem.id) break;
264 }
265 if (!found) {
266 $scope.builds.splice(i, 0, elem);
267 }
268 });
269 // step 3 - merge "Canceled" builds
270 $scope.canceledBuilds.forEach(function (elem) {
271 // mock the build object
272 var found = false;
273 var i = 0;
274 for (i = 0; i < $scope.builds.length; i ++) {
275 if ($scope.builds[i].id > elem.id) continue;
276 if ($scope.builds[i].id == elem.id) { found=true; break;}
277 if ($scope.builds[i].id < elem.id) break;
278 }
279 if (!found) {
280 $scope.builds.splice(i, 0, elem);
281 }
282 });
283
284 $scope.fetchLayersForTargets();
285 }
248 if (_data.targets !== undefined) { 286 if (_data.targets !== undefined) {
249 $scope.targets = _data.targets; 287 $scope.targets = _data.targets;
250 } 288 }
@@ -267,17 +305,26 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
267 deffered.resolve(_data); 305 deffered.resolve(_data);
268 } 306 }
269 }).error(function(_data, _status, _headers, _config) { 307 }).error(function(_data, _status, _headers, _config) {
270 console.warn("Failed HTTP XHR request (" + _status + ")" + _data); 308 if (_status == 0) {
309 // the server has gone away
310 alert("The server is not responding. The application will terminate now")
311 $interval.cancel($scope.pollHandle);
312 }
313 else {
271 console.error("Failed HTTP XHR request: ", _data, _status, _headers, _config); 314 console.error("Failed HTTP XHR request: ", _data, _status, _headers, _config);
272 inXHRcall = false; 315 inXHRcall = false;
273 deffered.reject(_data.error); 316 deffered.reject(_data.error);
317 }
274 }); 318 });
275 319
276 return deffered.promise; 320 return deffered.promise;
277 } 321 }
278 322
279 $scope.layeralert = undefined; 323 $scope.layeralert = undefined;
280 // shows user alerts on invalid project data 324 /**
325 * Verifies and shows user alerts on invalid project data
326 */
327
281 $scope.validateData = function () { 328 $scope.validateData = function () {
282 if ($scope.layers.length == 0) { 329 if ($scope.layers.length == 0) {
283 $scope.layeralert = $scope.displayAlert($scope.zone1alerts, "You need to add some layers to this project. <a href=\""+$scope.urls.layers+"\">View all layers available in Toaster</a> or <a href=\""+$scope.urls.importlayer+"\">import a layer</a>"); 330 $scope.layeralert = $scope.displayAlert($scope.zone1alerts, "You need to add some layers to this project. <a href=\""+$scope.urls.layers+"\">View all layers available in Toaster</a> or <a href=\""+$scope.urls.importlayer+"\">import a layer</a>");
@@ -289,14 +336,14 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
289 } 336 }
290 } 337 }
291 338
292 $scope.targetExistingBuild = function(targets) { 339 $scope.buildExistingTarget = function(targets) {
293 var oldTargetName = $scope.targetName; 340 var oldTargetName = $scope.targetName;
294 $scope.targetName = targets.map(function(v,i,a){return v.target}).join(' '); 341 $scope.targetName = targets.map(function(v,i,a){return v.target}).join(' ');
295 $scope.targetNamedBuild(); 342 $scope.targetNamedBuild();
296 $scope.targetName = oldTargetName; 343 $scope.targetName = oldTargetName;
297 } 344 }
298 345
299 $scope.targetNamedBuild = function(target) { 346 $scope.buildNamedTarget = function(target) {
300 if ($scope.targetName === undefined && $scope.targetName1 === undefined){ 347 if ($scope.targetName === undefined && $scope.targetName1 === undefined){
301 console.warn("No target defined, please type in a target name"); 348 console.warn("No target defined, please type in a target name");
302 return; 349 return;
@@ -310,7 +357,7 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
310 targets: $scope.safeTargetName, 357 targets: $scope.safeTargetName,
311 } 358 }
312 }).then(function (data) { 359 }).then(function (data) {
313 console.warn("received ", data); 360 console.warn("TRC3: received ", data);
314 $scope.targetName = undefined; 361 $scope.targetName = undefined;
315 $scope.targetName1 = undefined; 362 $scope.targetName1 = undefined;
316 $location.hash('buildslist'); 363 $location.hash('buildslist');
@@ -329,22 +376,20 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
329 $scope.safeTargetName = $scope.safeTargetName.replace(/\[.*\]/, '').trim(); 376 $scope.safeTargetName = $scope.safeTargetName.replace(/\[.*\]/, '').trim();
330 } 377 }
331 378
332 $scope.buildCancel = function(id) { 379 $scope.buildCancel = function(build) {
333 $scope._makeXHRCall({ 380 $scope._makeXHRCall({
334 method: "POST", url: $scope.urls.xhr_build, 381 method: "POST", url: $scope.urls.xhr_build,
335 data: { 382 data: {
336 buildCancel: id, 383 buildCancel: build.id,
337 } 384 }
385 }).then( function () {
386 build['status'] = "deleted";
387 $scope.canceledBuilds.push(build);
338 }); 388 });
339 } 389 }
340 390
341 $scope.buildDelete = function(id) { 391 $scope.buildDelete = function(build) {
342 $scope._makeXHRCall({ 392 $scope.canceledBuilds.splice($scope.canceledBuilds.indexOf(build), 1);
343 method: "POST", url: $scope.urls.xhr_build,
344 data: {
345 buildDelete: id,
346 }
347 });
348 } 393 }
349 394
350 395
@@ -352,6 +397,12 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
352 $scope.layerAddId = item.id; 397 $scope.layerAddId = item.id;
353 } 398 }
354 399
400
401 $scope.layerAddById = function (id) {
402 $scope.layerAddId = id;
403 $scope.layerAdd();
404 }
405
355 $scope.layerAdd = function() { 406 $scope.layerAdd = function() {
356 407
357 $http({method:"GET", url: $scope.urls.xhr_datatypeahead, params : { type: "layerdeps", value: $scope.layerAddId }}) 408 $http({method:"GET", url: $scope.urls.xhr_datatypeahead, params : { type: "layerdeps", value: $scope.layerAddId }})
@@ -369,7 +420,7 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
369 $scope.selectedItems = (function () { s = {}; for (var i = 0; i < items.length; i++) { s[items[i].id] = true; };return s; })(); 420 $scope.selectedItems = (function () { s = {}; for (var i = 0; i < items.length; i++) { s[items[i].id] = true; };return s; })();
370 421
371 $scope.ok = function() { 422 $scope.ok = function() {
372 console.warn("scope selected is ", $scope.selectedItems); 423 console.warn("TRC4: scope selected is ", $scope.selectedItems);
373 $modalInstance.close(Object.keys($scope.selectedItems).filter(function (e) { return $scope.selectedItems[e];})); 424 $modalInstance.close(Object.keys($scope.selectedItems).filter(function (e) { return $scope.selectedItems[e];}));
374 }; 425 };
375 426
@@ -378,7 +429,7 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
378 }; 429 };
379 430
380 $scope.update = function() { 431 $scope.update = function() {
381 console.warn("updated ", $scope.selectedItems); 432 console.warn("TRC5: updated ", $scope.selectedItems);
382 }; 433 };
383 }, 434 },
384 resolve: { 435 resolve: {
@@ -393,7 +444,7 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
393 444
394 modalInstance.result.then(function (selectedArray) { 445 modalInstance.result.then(function (selectedArray) {
395 selectedArray.push($scope.layerAddId); 446 selectedArray.push($scope.layerAddId);
396 console.warn("selected", selectedArray); 447 console.warn("TRC6: selected", selectedArray);
397 448
398 $scope._makeXHRCall({ 449 $scope._makeXHRCall({
399 method: "POST", url: $scope.urls.xhr_edit, 450 method: "POST", url: $scope.urls.xhr_edit,
@@ -429,7 +480,16 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
429 } 480 }
430 481
431 482
432 $scope.test = function(elementid) { 483 /**
484 * Verifies if a project settings change would trigger layer updates. If user confirmation is needed,
485 * a modal dialog will prompt the user to ack the changes. If not, the editProjectSettings() function is called directly.
486 *
487 * Only "versionlayers" change for is supported (and hardcoded) for now.
488 */
489
490 $scope.testProjectSettingsChange = function(elementid) {
491 if (elementid != '#change-project-version') throw "Not implemented";
492
433 $http({method:"GET", url: $scope.urls.xhr_datatypeahead, params : { type: "versionlayers", value: $scope.projectVersion }}). 493 $http({method:"GET", url: $scope.urls.xhr_datatypeahead, params : { type: "versionlayers", value: $scope.projectVersion }}).
434 success(function (_data) { 494 success(function (_data) {
435 if (_data.error != "ok") { 495 if (_data.error != "ok") {
@@ -463,17 +523,21 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
463 } 523 }
464 }); 524 });
465 525
466 modalInstance.result.then(function () { $scope.edit(elementid)}); 526 modalInstance.result.then(function () { $scope.editProjectSettings(elementid)});
467 } else { 527 } else {
468 $scope.edit(elementid); 528 $scope.editProjectSettings(elementid);
469 } 529 }
470 } 530 }
471 }); 531 });
472 } 532 }
473 533
474 $scope.edit = function(elementid) { 534 /**
535 * Performs changes to project settings, and updates the user interface accordingly.
536 */
537
538 $scope.editProjectSettings = function(elementid) {
475 var data = {}; 539 var data = {};
476 console.warn("edit with ", elementid); 540 console.warn("TRC7: editProjectSettings with ", elementid);
477 var alertText = undefined; 541 var alertText = undefined;
478 var alertZone = undefined; 542 var alertZone = undefined;
479 var oldLayers = []; 543 var oldLayers = [];
@@ -508,10 +572,14 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
508 alertText += "<strong>" + $scope.project.release.desc + "</strong>. "; 572 alertText += "<strong>" + $scope.project.release.desc + "</strong>. ";
509 } 573 }
510 if (elementid == '#change-project-version') { 574 if (elementid == '#change-project-version') {
575 $scope.layersForTargets = {}; // invalidate error layers for the targets, since layers changed
576
511 // requirement https://bugzilla.yoctoproject.org/attachment.cgi?id=2229, notification for changed version to include layers 577 // requirement https://bugzilla.yoctoproject.org/attachment.cgi?id=2229, notification for changed version to include layers
512 $scope.zone2alerts.forEach(function (e) { e.close() }); 578 $scope.zone2alerts.forEach(function (e) { e.close() });
513 alertText += "This has caused the following changes in your project layers:<ul>" 579 alertText += "This has caused the following changes in your project layers:<ul>"
514 580
581
582 // warnings - this is executed AFTER the generic XHRCall handling is done; at this point,
515 if (_data.layers !== undefined) { 583 if (_data.layers !== undefined) {
516 // show added/deleted layer notifications; scope.layers is already updated by this point. 584 // show added/deleted layer notifications; scope.layers is already updated by this point.
517 var addedLayers = []; 585 var addedLayers = [];
@@ -547,6 +615,10 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
547 } 615 }
548 616
549 617
618 /**
619 * Extracts a command passed through the local path in location, and executes/updates UI based on the command
620 */
621
550 $scope.updateDisplayWithCommands = function() { 622 $scope.updateDisplayWithCommands = function() {
551 cmd = $location.path(); 623 cmd = $location.path();
552 624
@@ -630,6 +702,10 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
630 }); 702 });
631 } 703 }
632 704
705 /**
706 * Utility function to display an alert to the user
707 */
708
633 $scope.displayAlert = function(zone, text, type) { 709 $scope.displayAlert = function(zone, text, type) {
634 if (zone.maxid === undefined) { zone.maxid = 0; } 710 if (zone.maxid === undefined) { zone.maxid = 0; }
635 var crtid = zone.maxid ++; 711 var crtid = zone.maxid ++;
@@ -644,6 +720,10 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
644 return o; 720 return o;
645 } 721 }
646 722
723 /**
724 * Toggles display items between label and input box (the edit pencil icon) on selected settings in project page
725 */
726
647 $scope.toggle = function(id) { 727 $scope.toggle = function(id) {
648 $scope.projectName = $scope.project.name; 728 $scope.projectName = $scope.project.name;
649 $scope.projectVersion = $scope.project.release.id; 729 $scope.projectVersion = $scope.project.release.id;
@@ -657,34 +737,52 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
657 keys = Object.keys($scope.mostBuiltTargets); 737 keys = Object.keys($scope.mostBuiltTargets);
658 keys = keys.filter(function (e) { if ($scope.mostBuiltTargets[e]) return e }); 738 keys = keys.filter(function (e) { if ($scope.mostBuiltTargets[e]) return e });
659 return keys.length == 0; 739 return keys.length == 0;
740 }
741
742 /**
743 * Helper function to deal with error string recognition and manipulation
744 */
660 745
746 $scope.getTargetNameFromErrorMsg = function (msg) {
747 targets = msg.split(" ").splice(2).map(function (v) { return v.replace(/'/g, '')})
748 return targets;
661 } 749 }
662 750
663 // init code 751 $scope.fetchLayersForTargets = function () {
664 // 752 $scope.builds.forEach(function (buildrequest) {
665 $scope.init = function() { 753 buildrequest.errors.forEach(function (error) {
666 $scope.pollHandle = $interval(function () { $scope._makeXHRCall({method: "GET", url: $scope.urls.xhr_edit, data: undefined});}, 2000, 0); 754 if (error.msg.indexOf("Nothin") == 0) {
755 $scope.getTargetNameFromErrorMsg(error.msg).forEach(function (target) {
756 if ($scope.layersForTargets[target] === undefined)
757 $scope.getAutocompleteSuggestions("layers4target", target).then( function (list) {
758 $scope.layersForTargets[target] = list;
759 })
760 })
761 }
762 })
763 })
667 } 764 }
668 765
669 $scope.init();
670});
671 766
767 /**
768 * Page init code - just init variables and set the automated refresh
769 */
672 770
673/** 771 $scope.init = function() {
674 TESTING CODE 772 $scope.canceledBuilds = [];
675*/ 773 $scope.layersForTargets = {};
774 $scope.fetchLayersForTargets();
775 $scope.pollHandle = $interval(function () { $scope._makeXHRCall({method: "GET", url: $scope.urls.xhr_edit, data: undefined});}, 2000, 0);
776 }
676 777
677function test_diff_arrays() { 778});
678 _diffArrays([1,2,3], [2,3,4], function(e,f) { return e==f; }, function(e) {console.warn("added", e)}, function(e) {console.warn("deleted", e);})
679}
680 779
681// test_diff_arrays();
682 780
683var s = undefined; 781var s = undefined;
684 782
685function test_set_alert(text) { 783function test_set_alert(text) {
686 s = angular.element("div#main").scope(); 784 s = angular.element("div#main").scope();
687 s.displayAlert(s.zone3alerts, text); 785 s.displayAlert(s.zone3alerts, text);
688 console.warn(s.zone3alerts); 786 console.warn("TRC8: zone3alerts", s.zone3alerts);
689 s.$digest(); 787 s.$digest();
690} 788}
diff --git a/bitbake/lib/toaster/toastergui/templates/project.html b/bitbake/lib/toaster/toastergui/templates/project.html
index 2979db74ed..67b267256b 100644
--- a/bitbake/lib/toaster/toastergui/templates/project.html
+++ b/bitbake/lib/toaster/toastergui/templates/project.html
@@ -89,9 +89,9 @@ vim: expandtab tabstop=2
89 89
90 <!-- build form --> 90 <!-- build form -->
91 <div class="well"> 91 <div class="well">
92 <form class="build-form" ng-submit="targetNamedBuild()"> 92 <form class="build-form" ng-submit="buildNamedTarget()">
93 <div class="input-append controls"> 93 <div class="input-append controls">
94 <input type="text" class="huge span7" placeholder="Type the target(s) you want to build" autocomplete="off" ng-model="targetName" typeahead="e.name for e in getSuggestions('targets', $viewValue)|filter:$viewValue" typeahead-template-url="suggestion_details" ng-disabled="!layers.length"/> 94 <input type="text" class="huge span7" placeholder="Type the target(s) you want to build" autocomplete="off" ng-model="targetName" typeahead="e.name for e in getAutocompleteSuggestions('targets', $viewValue)|filter:$viewValue" typeahead-template-url="suggestion_details" ng-disabled="!layers.length"/>
95 <button type="submit" id="build-button" class="btn btn-large btn-primary" ng-disabled="!targetName.length"> 95 <button type="submit" id="build-button" class="btn btn-large btn-primary" ng-disabled="!targetName.length">
96 Build 96 Build
97 </button> 97 </button>
@@ -108,6 +108,9 @@ vim: expandtab tabstop=2
108 </form> 108 </form>
109 </div> 109 </div>
110 110
111
112 <!-- latest builds list -->
113
111 <a id="buildslist"></a> 114 <a id="buildslist"></a>
112 <h2 class="air" ng-if="builds.length">Latest builds</h2> 115 <h2 class="air" ng-if="builds.length">Latest builds</h2>
113 <div class="animate-repeat alert" ng-repeat="b in builds track by b.id" ng-class="{'queued':'alert-info', 'deleted':'alert-info', 'in progress': 'alert-info', 'failed':'alert-error', 'completed':{'In Progress':'alert-info', 'Succeeded':'alert-success', 'Failed':'alert-error'}[b.build[0].status]}[b.status]"> 116 <div class="animate-repeat alert" ng-repeat="b in builds track by b.id" ng-class="{'queued':'alert-info', 'deleted':'alert-info', 'in progress': 'alert-info', 'failed':'alert-error', 'completed':{'In Progress':'alert-info', 'Succeeded':'alert-success', 'Failed':'alert-error'}[b.build[0].status]}[b.status]">
@@ -116,9 +119,27 @@ vim: expandtab tabstop=2
116 119
117 <case ng-switch-when="failed"> 120 <case ng-switch-when="failed">
118 <div class="lead span3"> <span ng-repeat="t in b.targets" ng-include src="'target_display'"></span></div> 121 <div class="lead span3"> <span ng-repeat="t in b.targets" ng-include src="'target_display'"></span></div>
122 <div >
123 <button class="btn pull-right btn-danger" ng-click="buildExistingTarget(b.targets)">Run again</button>
124 </div>
119 <div class="row-fluid"> 125 <div class="row-fluid">
120 <div class="air well" ng-repeat="e in b.errors"> 126 <div class="air well" ng-repeat="e in b.errors">
121 Error type {[e.type]}: <pre>{[e.msg]}</pre> 127 <pre>{[e.msg]}</pre>
128 <ngif ng-if="e.msg.indexOf('Nothin') == 0">
129 <div ng-repeat="t in getTargetNameFromErrorMsg(e.msg)">
130 <p class="lead">The target <strong>{[t]}</strong> is not provided by any of your project layers.</p>
131 <p> Your build has failed because the target <strong>{[t]}</strong> is not provided by any of your project layers.</p>
132 <ngif ng-if="layersForTargets[t].length > 0">
133 <p>The following layers provide this target. You could add one of them to your project.</p>
134 <button class="btn btn-danger add-layer-with-dependencies" ng-repeat="l in layersForTargets[t]" ng-click="layerAddById(l.id)">Add {[l.name]}</button>
135 </ngif>
136 </div>
137 </ngif>
138 <ngif ng-if="e.msg.indexOf('Nothin') != 0">
139 <p>
140 Please contact your system administrator to help troubleshoot this error.
141 </p>
142 </ngif>
122 </div> 143 </div>
123 </div> 144 </div>
124 </case> 145 </case>
@@ -128,7 +149,7 @@ vim: expandtab tabstop=2
128 <div class="span4 lead" >Build queued 149 <div class="span4 lead" >Build queued
129 <i title="This build will start as soon as a build server is available" class="icon-question-sign get-help get-help-blue heading-help" data-toggle="tooltip"></i> 150 <i title="This build will start as soon as a build server is available" class="icon-question-sign get-help get-help-blue heading-help" data-toggle="tooltip"></i>
130 </div> 151 </div>
131 <button class="btn pull-right btn-info" ng-click="buildCancel(b.id)">Cancel</button> 152 <button class="btn pull-right btn-info" ng-click="buildCancel(b)">Cancel</button>
132 </case> 153 </case>
133 154
134 <case ng-switch-when="created"> 155 <case ng-switch-when="created">
@@ -136,7 +157,7 @@ vim: expandtab tabstop=2
136 <div class="span6" > 157 <div class="span6" >
137 <span class="lead">Creating build</span> 158 <span class="lead">Creating build</span>
138 </div> 159 </div>
139 <button class="btn pull-right btn-info" ng-click="buildCancel(b.id)">Cancel</button> 160 <button class="btn pull-right btn-info" ng-click="buildCancel(b)">Cancel</button>
140 </case> 161 </case>
141 162
142 <case ng-switch-when="deleted"> 163 <case ng-switch-when="deleted">
@@ -144,7 +165,7 @@ vim: expandtab tabstop=2
144 <div class="span6" id="{[b.id]}-deleted" > 165 <div class="span6" id="{[b.id]}-deleted" >
145 <span class="lead">Build deleted</span> 166 <span class="lead">Build deleted</span>
146 </div> 167 </div>
147 <button class="btn pull-right btn-info" ng-click="buildDelete(b.id)">Close</button> 168 <button class="btn pull-right btn-info" ng-click="buildDelete(b)">Close</button>
148 </case> 169 </case>
149 170
150 171
@@ -198,7 +219,7 @@ vim: expandtab tabstop=2
198 </div> 219 </div>
199 <div> <span class="lead">Build time: <a href="{[b.build[0].build_time_page_url]}">{[b.build[0].build_time|timediff]}</a></span> 220 <div> <span class="lead">Build time: <a href="{[b.build[0].build_time_page_url]}">{[b.build[0].build_time|timediff]}</a></span>
200 <button class="btn pull-right" ng-class="{'Succeeded': 'btn-success', 'Failed': 'btn-danger'}[b.build[0].status]" 221 <button class="btn pull-right" ng-class="{'Succeeded': 'btn-success', 'Failed': 'btn-danger'}[b.build[0].status]"
201 ng-click="targetExistingBuild(b.targets)">Run again</button> 222 ng-click="buildExistingTarget(b.targets)">Run again</button>
202 223
203 </div> 224 </div>
204 </case> 225 </case>
@@ -244,7 +265,7 @@ vim: expandtab tabstop=2
244 </div> 265 </div>
245 <form ng-submit="layerAdd()"> 266 <form ng-submit="layerAdd()">
246 <div class="input-append"> 267 <div class="input-append">
247 <input type="text" class="input-xlarge" id="layer" autocomplete="off" placeholder="Type a layer name" data-minLength="1" ng-model="layerAddName" typeahead="e.name for e in getSuggestions('layers', $viewValue)|filter:$viewValue" typeahead-template-url="suggestion_details" typeahead-on-select="onLayerSelect($item, $model, $label)" typeahead-editable="false" ng-class="{ 'has-error': layerAddName.$invalid }" /> 268 <input type="text" class="input-xlarge" id="layer" autocomplete="off" placeholder="Type a layer name" data-minLength="1" ng-model="layerAddName" typeahead="e.name for e in getAutocompleteSuggestions('layers', $viewValue)|filter:$viewValue" typeahead-template-url="suggestion_details" typeahead-on-select="onLayerSelect($item, $model, $label)" typeahead-editable="false" ng-class="{ 'has-error': layerAddName.$invalid }" />
248 <input type="submit" id="add-layer" class="btn" value="Add" ng-disabled="!layerAddName.length"/> 269 <input type="submit" id="add-layer" class="btn" value="Add" ng-disabled="!layerAddName.length"/>
249 </div> 270 </div>
250 {% csrf_token %} 271 {% csrf_token %}
@@ -265,9 +286,9 @@ vim: expandtab tabstop=2
265 Targets 286 Targets
266 <i class="icon-question-sign get-help heading-help" title="What you build, often a recipe producing a root file system file (an image). Something like <code>core-image-minimal</code> or <code>core-image-sato</code>"></i> 287 <i class="icon-question-sign get-help heading-help" title="What you build, often a recipe producing a root file system file (an image). Something like <code>core-image-minimal</code> or <code>core-image-sato</code>"></i>
267 </h3> 288 </h3>
268 <form ng-submit="targetNamedBuild()"> 289 <form ng-submit="buildNamedTarget()">
269 <div class="input-append"> 290 <div class="input-append">
270 <input type="text" class="input-xlarge" placeholder="Type the target(s) you want to build" autocomplete="off" data-minLength="1" ng-model="targetName1" typeahead="e.name for e in getSuggestions('targets', $viewValue)|filter:$viewValue" typeahead-template-url="suggestion_details" ng-disabled="!layers.length"> 291 <input type="text" class="input-xlarge" placeholder="Type the target(s) you want to build" autocomplete="off" data-minLength="1" ng-model="targetName1" typeahead="e.name for e in getAutocompleteSuggestions('targets', $viewValue)|filter:$viewValue" typeahead-template-url="suggestion_details" ng-disabled="!layers.length">
271 <button type="submit" id="build-button" class="btn btn-primary" ng-disabled="!targetName1.length"> 292 <button type="submit" id="build-button" class="btn btn-primary" ng-disabled="!targetName1.length">
272 Build </button> 293 Build </button>
273 </div> 294 </div>
@@ -304,8 +325,8 @@ vim: expandtab tabstop=2
304 <strong>Machine changes have a big impact on build outcome.</strong> 325 <strong>Machine changes have a big impact on build outcome.</strong>
305 You cannot really compare the builds for the new machine with the previous ones. 326 You cannot really compare the builds for the new machine with the previous ones.
306 </div> 327 </div>
307 <form ng-submit="edit('#select-machine')" class="input-append"> 328 <form ng-submit="editProjectSettings('#select-machine')" class="input-append">
308 <input type="text" id="machine" autocomplete="off" ng-model="machineName" typeahead="m.name for m in getSuggestions('machines', $viewValue)"/> 329 <input type="text" id="machine" autocomplete="off" ng-model="machineName" typeahead="m.name for m in getAutocompleteSuggestions('machines', $viewValue)"/>
309 <input type="submit" id="apply-change-machine" class="btn" type="button" ng-disabled="machineName == machine.name || machineName.length == 0" value="Save"></input> 330 <input type="submit" id="apply-change-machine" class="btn" type="button" ng-disabled="machineName == machine.name || machineName.length == 0" value="Save"></input>
310 <input type="reset" id="cancel-machine" class="btn btn-link" ng-click="toggle('#select-machine')" value="Cancel"></input> 331 <input type="reset" id="cancel-machine" class="btn btn-link" ng-click="toggle('#select-machine')" value="Cancel"></input>
311 {% csrf_token %} 332 {% csrf_token %}
@@ -337,7 +358,7 @@ vim: expandtab tabstop=2
337 <i class="icon-pencil" ng-click="toggle('#change-project-name')" ></i> 358 <i class="icon-pencil" ng-click="toggle('#change-project-name')" ></i>
338 </p> 359 </p>
339 <div id="change-project-name" style="display:none;"> 360 <div id="change-project-name" style="display:none;">
340 <form ng-submit="edit('#change-project-name')" class="input-append"> 361 <form ng-submit="editProjectSettings('#change-project-name')" class="input-append">
341 <input type="text" class="input-xlarge" id="type-project-name" ng-model="projectName"> 362 <input type="text" class="input-xlarge" id="type-project-name" ng-model="projectName">
342 <input type="submit" class="btn" value="Save" ng-disabled="project.name == projectName"/> 363 <input type="submit" class="btn" value="Save" ng-disabled="project.name == projectName"/>
343 <input type="reset" class="btn btn-link" value="Cancel" ng-click="toggle('#change-project-name')"> 364 <input type="reset" class="btn btn-link" value="Cancel" ng-click="toggle('#change-project-name')">
@@ -354,7 +375,7 @@ vim: expandtab tabstop=2
354 <i id="change-version" class="icon-pencil" ng-click="toggle('#change-project-version')" ></i> 375 <i id="change-version" class="icon-pencil" ng-click="toggle('#change-project-version')" ></i>
355 </p> 376 </p>
356 <div class="div-inline" id="change-project-version" style="display:none;"> 377 <div class="div-inline" id="change-project-version" style="display:none;">
357 <form ng-submit="test('#change-project-version')" class="input-append"> 378 <form ng-submit="testProjectSettingsChange('#change-project-version')" class="input-append">
358 <select id="select-version" ng-model="projectVersion"> 379 <select id="select-version" ng-model="projectVersion">
359 <option ng-repeat="r in releases" value="{[r.id]}" ng-selected="r.id == project.release.id">{[r.description]}</option> 380 <option ng-repeat="r in releases" value="{[r.id]}" ng-selected="r.id == project.release.id">{[r.description]}</option>
360 </select> 381 </select>
@@ -404,6 +425,7 @@ angular.element(document).ready(function() {
404 scope.updateDisplayWithCommands(); 425 scope.updateDisplayWithCommands();
405 scope.validateData(); 426 scope.validateData();
406 427
428 scope.init();
407 scope.$digest(); 429 scope.$digest();
408 430
409 }); 431 });
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
index 420b37cb73..8c21ca48e0 100755
--- a/bitbake/lib/toaster/toastergui/views.py
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -40,7 +40,6 @@ from django.utils import formats
40from toastergui.templatetags.projecttags import json as jsonfilter 40from toastergui.templatetags.projecttags import json as jsonfilter
41import json 41import json
42 42
43
44# all new sessions should come through the landing page; 43# all new sessions should come through the landing page;
45# determine in which mode we are running in, and redirect appropriately 44# determine in which mode we are running in, and redirect appropriately
46def landing(request): 45def landing(request):
@@ -52,37 +51,25 @@ def landing(request):
52 51
53 return render(request, 'landing.html') 52 return render(request, 'landing.html')
54 53
54# returns a list for most recent builds; for use in the Project page, xhr_ updates, and other places, as needed
55def _project_recent_build_list(prj): 55def _project_recent_build_list(prj):
56 # build requests not yet started 56 return map(lambda x: {
57 return (map(lambda x: { 57 "id": x.pk,
58 "id": x.pk, 58 "targets" : map(lambda y: {"target": y.target, "task": y.task }, x.brtarget_set.all()),
59 "targets" : map(lambda y: {"target": y.target }, x.brtarget_set.all()), 59 "status": x.get_state_display(),
60 "status": x.get_state_display(), 60 "errors": map(lambda y: {"type": y.errtype, "msg": y.errmsg, "tb": y.traceback}, x.brerror_set.exclude(errmsg__contains="Command Failed")),
61 }, prj.buildrequest_set.filter(state__lt = BuildRequest.REQ_INPROGRESS).order_by("-pk")) + 61 "build" : map( lambda y: {"id": y.pk,
62 # build requests started, but with no build yet 62 "status": y.get_outcome_display(),
63 map(lambda x: { 63 "completed_on" : y.completed_on.strftime('%s')+"000",
64 "id": x.pk, 64 "build_time" : (y.completed_on - y.started_on).total_seconds(),
65 "targets" : map(lambda y: {"target": y.target }, x.brtarget_set.all()), 65 "build_page_url" : reverse('builddashboard', args=(y.pk,)),
66 "status": x.get_state_display(), 66 'build_time_page_url': reverse('buildtime', args=(y.pk,)),
67 }, prj.buildrequest_set.filter(state = BuildRequest.REQ_INPROGRESS, build = None).order_by("-pk")) + 67 "errors": y.errors_no,
68 # build requests that failed 68 "warnings": y.warnings_no,
69 map(lambda x: { 69 "completeper": y.completeper(),
70 "id": x.pk, 70 "eta": y.eta().strftime('%s')+"000"}, Build.objects.filter(buildrequest = x)),
71 "targets" : map(lambda y: {"target": y.target }, x.brtarget_set.all()), 71 }, list(prj.buildrequest_set.filter(Q(state__lt=BuildRequest.REQ_COMPLETED) or Q(state=BuildRequest.REQ_DELETED)).order_by("-pk")) +
72 "status": x.get_state_display(), 72 list(prj.buildrequest_set.filter(state__in=[BuildRequest.REQ_COMPLETED, BuildRequest.REQ_FAILED]).order_by("-pk")[:3]))
73 "errors": map(lambda y: {"type": y.errtype, "msg": y.errmsg, "tb": y.traceback}, x.brerror_set.all()),
74 }, prj.buildrequest_set.filter(state = BuildRequest.REQ_FAILED).order_by("-pk")) +
75 # and already made builds
76 map(lambda x: {
77 "id": x.pk,
78 "targets": map(lambda y: {"target": y.target }, x.target_set.all()),
79 "status": x.get_outcome_display(),
80 "completed_on" : x.completed_on.strftime('%s')+"000",
81 "build_time" : (x.completed_on - x.started_on).total_seconds(),
82 "build_page_url" : reverse('builddashboard', args=(x.pk,)),
83 "completeper": x.completeper(),
84 "eta": x.eta().ctime(),
85 }, prj.build_set.all()))
86 73
87 74
88def _build_page_range(paginator, index = 1): 75def _build_page_range(paginator, index = 1):
@@ -1959,25 +1946,6 @@ if toastermain.settings.MANAGED:
1959 1946
1960 raise Exception("Invalid HTTP method for this page") 1947 raise Exception("Invalid HTTP method for this page")
1961 1948
1962 # returns a list for most recent builds; for use in the Project page, xhr_ updates, and other places, as needed
1963 def _project_recent_build_list(prj):
1964 return map(lambda x: {
1965 "id": x.pk,
1966 "targets" : map(lambda y: {"target": y.target, "task": y.task }, x.brtarget_set.all()),
1967 "status": x.get_state_display(),
1968 "errors": map(lambda y: {"type": y.errtype, "msg": y.errmsg, "tb": y.traceback}, x.brerror_set.all()),
1969 "build" : map( lambda y: {"id": y.pk,
1970 "status": y.get_outcome_display(),
1971 "completed_on" : y.completed_on.strftime('%s')+"000",
1972 "build_time" : (y.completed_on - y.started_on).total_seconds(),
1973 "build_page_url" : reverse('builddashboard', args=(y.pk,)),
1974 'build_time_page_url': reverse('buildtime', args=(y.pk,)),
1975 "errors": y.errors_no,
1976 "warnings": y.warnings_no,
1977 "completeper": y.completeper(),
1978 "eta": y.eta().strftime('%s')+"000"}, Build.objects.filter(buildrequest = x)),
1979 }, list(prj.buildrequest_set.filter(Q(state__lt=BuildRequest.REQ_COMPLETED) or Q(state=BuildRequest.REQ_DELETED)).order_by("-pk")) +
1980 list(prj.buildrequest_set.filter(state__in=[BuildRequest.REQ_COMPLETED, BuildRequest.REQ_FAILED]).order_by("-pk")[:3]))
1981 1949
1982 1950
1983 # Shows the edit project page 1951 # Shows the edit project page
@@ -2177,7 +2145,7 @@ if toastermain.settings.MANAGED:
2177 # all layers for the current project 2145 # all layers for the current project
2178 queryset_all = prj.compatible_layerversions().filter(layer__name__icontains=request.GET.get('value','')) 2146 queryset_all = prj.compatible_layerversions().filter(layer__name__icontains=request.GET.get('value',''))
2179 2147
2180 # but not layers with equivalent layers already in project 2148 # but not layers with equivalent layers already in project
2181 if not request.GET.has_key('include_added'): 2149 if not request.GET.has_key('include_added'):
2182 queryset_all = queryset_all.exclude(pk__in = [x.id for x in prj.projectlayer_equivalent_set()])[:8] 2150 queryset_all = queryset_all.exclude(pk__in = [x.id for x in prj.projectlayer_equivalent_set()])[:8]
2183 2151
@@ -2192,7 +2160,9 @@ if toastermain.settings.MANAGED:
2192 queryset = prj.compatible_layerversions().exclude(pk__in = [x.id for x in prj.projectlayer_equivalent_set()]).filter( 2160 queryset = prj.compatible_layerversions().exclude(pk__in = [x.id for x in prj.projectlayer_equivalent_set()]).filter(
2193 layer__name__in = [ x.depends_on.layer.name for x in LayerVersionDependency.objects.filter(layer_version_id = request.GET['value'])]) 2161 layer__name__in = [ x.depends_on.layer.name for x in LayerVersionDependency.objects.filter(layer_version_id = request.GET['value'])])
2194 2162
2195 return HttpResponse(jsonfilter( { "error":"ok", "list" : map( _lv_to_dict, queryset) }), content_type = "application/json") 2163 final_list = set([x.get_equivalents_wpriority(prj)[0] for x in queryset])
2164
2165 return HttpResponse(jsonfilter( { "error":"ok", "list" : map( _lv_to_dict, final_list) }), content_type = "application/json")
2196 2166
2197 2167
2198 2168
@@ -2213,16 +2183,36 @@ if toastermain.settings.MANAGED:
2213 }), content_type = "application/json") 2183 }), content_type = "application/json")
2214 2184
2215 2185
2186 # returns layer versions that provide the named targets
2187 if request.GET['type'] == "layers4target":
2188 # we returnd ata only if the recipe can't be provided by the current project layer set
2189 if reduce(lambda x, y: x + y, [x.recipe_layer_version.filter(name="anki").count() for x in prj.projectlayer_equivalent_set()], 0):
2190 final_list = []
2191 else:
2192 queryset_all = prj.compatible_layerversions().filter(recipe_layer_version__name = request.GET.get('value', '__none__'))
2193
2194 # exclude layers in the project
2195 queryset_all = queryset_all.exclude(pk__in = [x.id for x in prj.projectlayer_equivalent_set()])
2196
2197 # and show only the selected layers for this project
2198 final_list = set([x.get_equivalents_wpriority(prj)[0] for x in queryset_all])
2199
2200 return HttpResponse(jsonfilter( { "error":"ok", "list" : map( _lv_to_dict, final_list) }), content_type = "application/json")
2201
2216 # returns targets provided by current project layers 2202 # returns targets provided by current project layers
2217 if request.GET['type'] == "targets": 2203 if request.GET['type'] == "targets":
2218 queryset_all = Recipe.objects.all() 2204 queryset_all = Recipe.objects.all()
2219 queryset_all = queryset_all.filter(layer_version__in = prj.projectlayer_equivalent_set()) 2205 layer_equivalent_set = []
2206 for i in prj.projectlayer_set.all():
2207 layer_equivalent_set += i.layercommit.get_equivalents_wpriority(prj)
2208 queryset_all = queryset_all.filter(layer_version__in = layer_equivalent_set)
2220 return HttpResponse(jsonfilter({ "error":"ok", 2209 return HttpResponse(jsonfilter({ "error":"ok",
2221 "list" : map ( lambda x: {"id": x.pk, "name": x.name, "detail":"[" + x.layer_version.layer.name+ (" | " + x.layer_version.up_branch.name + "]" if x.layer_version.up_branch is not None else "]")}, 2210 "list" : map ( lambda x: {"id": x.pk, "name": x.name, "detail":"[" + x.layer_version.layer.name+ (" | " + x.layer_version.up_branch.name + "]" if x.layer_version.up_branch is not None else "]")},
2222 queryset_all.filter(name__icontains=request.GET.get('value',''))[:8]), 2211 queryset_all.filter(name__icontains=request.GET.get('value',''))[:8]),
2223 2212
2224 }), content_type = "application/json") 2213 }), content_type = "application/json")
2225 2214
2215 # returns machines provided by the current project layers
2226 if request.GET['type'] == "machines": 2216 if request.GET['type'] == "machines":
2227 queryset_all = Machine.objects.all() 2217 queryset_all = Machine.objects.all()
2228 if 'project_id' in request.session: 2218 if 'project_id' in request.session:
@@ -2234,6 +2224,7 @@ if toastermain.settings.MANAGED:
2234 2224
2235 }), content_type = "application/json") 2225 }), content_type = "application/json")
2236 2226
2227 # returns all projects
2237 if request.GET['type'] == "projects": 2228 if request.GET['type'] == "projects":
2238 queryset_all = Project.objects.all() 2229 queryset_all = Project.objects.all()
2239 ret = { "error": "ok", 2230 ret = { "error": "ok",