delete builds, projects, recipes, layers
cover delete builds, projects, recipes, layers
Work to implement deleting of builds projects recipes and layers. Includes a number of clean ups in affected areas.
The following changes since commit 725e66e1d08ae000d8f68455ddca0e192080dc1f:
meta-environment: ensure corret TOOLCHAIN_CONFIGSITE_NOCACHE value (2016-09-26 10:06:02 +0100)
are available in the git repository at:
git://git.yoctoproject.org/poky-contrib ed/submit/michaelw/toaster/delete_buttons
http://git.yoctoproject.org/cgit.cgi/poky-contrib/log/?h=ed/submit/michaelw/toaster/delete_buttons
Michael Wood (10):
toaster: Clean up and convert to rest api project edit and get calls
toaster: move MostRecentBuildsView to its own widget
toaster: libtoaster Add a global notification set/show mechanism
toaster: project page Implement front end feature to delete project
toaster: alerts and modals Avoid modals and alerts overlaying each
other
toaster: Add backend API for deleting a build
toaster: Add front end controls for deleting a build
toaster: importlayer Convert success import to new notification system
toaster: customrecipe Add frontend feature to delete custom image
recipe
toaster: layerdetails Update implementation of delete imported layer
lib/toaster/toastergui/api.py | 312 +++++++++++++-------- lib/toaster/toastergui/static/js/customrecipe.js | 32 +++ lib/toaster/toastergui/static/js/importlayer.js | 59 +++- lib/toaster/toastergui/static/js/layerDepsModal.js | 12 +- lib/toaster/toastergui/static/js/layerdetails.js | 7 +- lib/toaster/toastergui/static/js/libtoaster.js | 42 ++- lib/toaster/toastergui/static/js/projectpage.js | 237 +++------------- lib/toaster/toastergui/static/js/projecttopbar.js | 6 +- lib/toaster/toastergui/static/js/tests/test.js | 7 +- lib/toaster/toastergui/templates/base.html | 1 + .../toastergui/templates/basebuildpage.html | 138 ++++++--- .../toastergui/templates/baseprojectpage.html | 6 + .../toastergui/templates/builddashboard.html | 2 +- lib/toaster/toastergui/templates/customrecipe.html | 35 ++- lib/toaster/toastergui/templates/layerdetails.html | 9 +- lib/toaster/toastergui/templates/project.html | 35 ++- .../toastergui/templates/projecttopbar.html | 2 +- lib/toaster/toastergui/urls.py | 11 +- lib/toaster/toastergui/views.py | 131 +-------- lib/toaster/toastergui/widgets.py | 141 ++++++++-- 20 files changed, 679 insertions(+), 546 deletions(-)
Signature · 1 line
2.6.6
PATCH 1/10 toaster: Clean up and convert to rest api project edit and get calls
From: Michael Wood <[email protected]>
Convert the project xhr calls into proper rest api and port the client side calls to use the new API. Fix all the pyflakes identified issues and clean up unused fields.
Also remove the api and client side code for changing release on the fly as this is no longer supported.
[YOCTO #9519]
--- lib/toaster/toastergui/api.py | 161 ++++++++++++++++++++++++ lib/toaster/toastergui/static/js/libtoaster.js | 3 +- lib/toaster/toastergui/static/js/projectpage.js | 147 +--------------------- lib/toaster/toastergui/static/js/tests/test.js | 7 +- lib/toaster/toastergui/templates/base.html | 1 + lib/toaster/toastergui/urls.py | 4 + lib/toaster/toastergui/views.py | 131 +------------------ 7 files changed, 179 insertions(+), 275 deletions(-)
lib/toaster/toastergui/api.py +161 -0
@@ -20,11 +20,13 @@ import re import logging +from collections import Counter from orm.models import Project, ProjectTarget, Build, Layer_Version from orm.models import LayerVersionDependency, LayerSource, ProjectLayer from orm.models import Recipe, CustomImageRecipe, CustomImagePackage from orm.models import Layer, Target, Package, Package_Dependency +from orm.models import ProjectVariable from bldcontrol.models import BuildRequest from bldcontrol import bbcontroller
@@ -772,3 +774,162 @@ class XhrCustomRecipePackages(View): except CustomImageRecipe.DoesNotExist: return error_response("Tried to remove package that wasn't" " present") + + +class XhrProject(View): + """ Create, delete or edit a project + + Entry point: /xhr_project/<project_id> + """ + def post(self, request, *args, **kwargs): + """ + Edit project control + + Args: + layerAdd = layer_version_id layer_version_id ... + layerDel = layer_version_id layer_version_id ... + projectName = new_project_name + machineName = new_machine_name + + Returns: + {"error": "ok"} + or + {"error": <error message>} + """ + try: + prj = Project.objects.get(pk=kwargs['project_id']) + except Project.DoesNotExist: + return error_response("No such project") + + # Add layers + if 'layerAdd' in request.POST and len(request.POST['layerAdd']) > 0: + for layer_version_id in request.POST['layerAdd'].split(','): + try: + lv = Layer_Version.objects.get(pk=int(layer_version_id)) + ProjectLayer.objects.get_or_create(project=prj, + layercommit=lv) + except Layer_Version.DoesNotExist: + return error_response("Layer version %s asked to add " + "doesn't exist" % layer_version_id) + + # Remove layers + if 'layerDel' in request.POST and len(request.POST['layerDel']) > 0: + layer_version_ids = request.POST['layerDel'].split(',') + ProjectLayer.objects.filter( + project=prj, + layercommit_id__in=layer_version_ids).delete() + + # Project name change + if 'projectName' in request.POST: + prj.name = request.POST['projectName'] + prj.save() + + # Machine name change + if 'machineName' in request.POST: + machinevar = prj.projectvariable_set.get(name="MACHINE") + machinevar.value = request.POST['machineName'] + machinevar.save() + + return JsonResponse({"error": "ok"}) + + def get(self, request, *args, **kwargs): + """ + Returns: + json object representing the current project + or: + {"error": <error message>} + """ + + try: + project = Project.objects.get(pk=kwargs['project_id']) + except Project.DoesNotExist: + return error_response("Project %s does not exist" % + kwargs['project_id']) + + # Create the frequently built targets list + + freqtargets = Counter(Target.objects.filter( + Q(build__project=project), + ~Q(build__outcome=Build.IN_PROGRESS) + ).order_by("target").values_list("target", flat=True)) + + freqtargets = freqtargets.most_common(5) + + # We now have the targets in order of frequency but if there are two + # with the same frequency then we need to make sure those are in + # alphabetical order without losing the frequency ordering + + tmp = [] + switch = None + for i, freqtartget in enumerate(freqtargets): + target, count = freqtartget + try: + target_next, count_next = freqtargets[i+1] + if count == count_next and target > target_next: + switch = target + continue + except IndexError: + pass + + tmp.append(target) + + if switch: + tmp.append(switch) + switch = None + + freqtargets = tmp + + layers = [] + for layer in project.projectlayer_set.all(): + layers.append({ + "id": layer.layercommit.pk, + "name": layer.layercommit.layer.name, + "vcs_url": layer.layercommit.layer.vcs_url, + "local_source_dir": layer.layercommit.layer.local_source_dir, + "vcs_reference": layer.layercommit.get_vcs_reference(), + "url": layer.layercommit.layer.layer_index_url, + "layerdetailurl": layer.layercommit.get_detailspage_url( + project.pk), + "layersource": layer.layercommit.layer_source + }) + + data = { + "name": project.name, + "layers": layers, + "freqtargets": freqtargets, + } + + if project.release is not None: + data['release'] = { + "id": project.release.pk, + "name": project.release.name, + "description": project.release.description + } + + try: + data["machine"] = {"name": + project.projectvariable_set.get( + name="MACHINE").value} + except ProjectVariable.DoesNotExist: + data["machine"] = None + try: + data["distro"] = project.projectvariable_set.get( + name="DISTRO").value + except ProjectVariable.DoesNotExist: + data["distro"] = "-- not set yet" + + data['error'] = "ok" + + return JsonResponse(data) + + def put(self, request, *args, **kwargs): + # TODO create new project api + return HttpResponse() + + def delete(self, request, *args, **kwargs): + try: + Project.objects.get(kwargs['project_id']).delete() + except Project.DoesNotExist: + return error_response("Project %s does not exist" % + kwargs['project_id']) + return JsonResponse({"error": "ok"})
lib/toaster/toastergui/static/js/libtoaster.js +1 -2
@@ -167,7 +167,6 @@ var libtoaster = (function () { function _getProjectInfo(url, onsuccess, onfail){ $.ajax({ type: "GET", - data : { format: "json" }, url: url, headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, success: function (_data) {
@@ -194,7 +193,7 @@ var libtoaster = (function () { function _editCurrentProject(data, onSuccess, onFail){ $.ajax({ type: "POST", - url: libtoaster.ctx.projectPageUrl + "?format=json", + url: libtoaster.ctx.xhrProjectUrl, data: data, headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, success: function (data) {
lib/toaster/toastergui/static/js/projectpage.js +6 -141
170 lines not on this page · raw message
lib/toaster/toastergui/static/js/tests/test.js +3 -4
@@ -42,9 +42,8 @@ QUnit.test("Layer alert notification", function(assert) { QUnit.test("Project info", function(assert){ var done = assert.async(); - libtoaster.getProjectInfo(libtoaster.ctx.projectPageUrl, function(prjInfo){ + libtoaster.getProjectInfo(libtoaster.ctx.xhrProjectUrl, function(prjInfo){ assert.ok(prjInfo.machine.name); - assert.ok(prjInfo.releases.length > 0); assert.ok(prjInfo.layers.length > 0); assert.ok(prjInfo.freqtargets); assert.ok(prjInfo.release);
@@ -82,11 +81,11 @@ QUnit.test("Add layer", function(assert){ }, 200); /* Compare the number of layers before and after the add in the project */ - libtoaster.getProjectInfo(libtoaster.ctx.projectPageUrl, function(prjInfo){ + libtoaster.getProjectInfo(libtoaster.ctx.xhrProjectUrl, function(prjInfo){ var origNumLayers = prjInfo.layers.length; libtoaster.addRmLayer(layer, true, function(deps){ - libtoaster.getProjectInfo(libtoaster.ctx.projectPageUrl, + libtoaster.getProjectInfo(libtoaster.ctx.xhrProjectUrl, function(prjInfo){ assert.ok(prjInfo.layers.length > origNumLayers, "Layer not added to project");
lib/toaster/toastergui/templates/base.html +1 -0
@@ -44,6 +44,7 @@ {% if project.id %} projectId : {{project.id}}, projectPageUrl : {% url 'project' project.id as purl %}{{purl|json}}, + xhrProjectUrl : {% url 'xhr_project' project.id as pxurl %}{{pxurl|json}}, projectName : {{project.name|json}}, recipesTypeAheadUrl: {% url 'xhr_recipestypeahead' project.id as paturl%}{{paturl|json}}, layersTypeAheadUrl: {% url 'xhr_layerstypeahead' project.id as paturl%}{{paturl|json}},
lib/toaster/toastergui/urls.py +4 -0
@@ -220,6 +220,10 @@ urlpatterns = patterns('toastergui.views', api.XhrBuildRequest.as_view(), name='xhr_buildrequest'), + url(r'xhr_project/(?P<project_id>\d+)$', + api.XhrProject.as_view(), + name='xhr_project'), + url(r'^mostrecentbuilds$', api.MostRecentBuildsView.as_view(), name='most_recent_builds'),
lib/toaster/toastergui/views.py +3 -128
139 lines not on this page · raw message
Signature · 1 line
2.6.6
PATCH 2/10 toaster: move MostRecentBuildsView to its own widget
In reply to Ed BartoshFrom: Michael Wood <[email protected]>
This view is specific to the builds dashboard rather than gernic api so like ToasterTable and ToasterTypeAhead we class it as a widget as it has a single purpose. Also clean up some flake8 identified issues.
Original author of the code moved in this commit is Elliot Smith.
--- lib/toaster/toastergui/api.py | 108 ----------------------------- lib/toaster/toastergui/urls.py | 3 +- lib/toaster/toastergui/widgets.py | 141 +++++++++++++++++++++++++++++++++----- 3 files changed, 127 insertions(+), 125 deletions(-)
lib/toaster/toastergui/api.py +0 -108
@@ -33,10 +33,8 @@ from bldcontrol import bbcontroller from django.http import HttpResponse, JsonResponse from django.views.generic import View from django.core.urlresolvers import reverse -from django.utils import timezone from django.db.models import Q, F from django.db import Error -from toastergui.templatetags.projecttags import json, sectohms, get_tasks from toastergui.templatetags.projecttags import filtered_filesizeformat logger = logging.getLogger("toaster")
@@ -227,112 +225,6 @@ class XhrLayer(View): }) -class MostRecentBuildsView(View): - def _was_yesterday_or_earlier(self, completed_on): - now = timezone.now() - delta = now - completed_on - - if delta.days >= 1: - return True - - return False - - def get(self, request, *args, **kwargs): - """ - Returns a list of builds in JSON format. - """ - project = None - - project_id = request.GET.get('project_id', None) - if project_id: - try: - project = Project.objects.get(pk=project_id) - except: - # if project lookup fails, assume no project - pass - - recent_build_objs = Build.get_recent(project) - recent_builds = [] - - for build_obj in recent_build_objs: - dashboard_url = reverse('builddashboard', args=(build_obj.pk,)) - buildtime_url = reverse('buildtime', args=(build_obj.pk,)) - rebuild_url = \ - reverse('xhr_buildrequest', args=(build_obj.project.pk,)) - cancel_url = \ - reverse('xhr_buildrequest', args=(build_obj.project.pk,)) - - build = {} - build['id'] = build_obj.pk - build['dashboard_url'] = dashboard_url - - buildrequest_id = None - if hasattr(build_obj, 'buildrequest'): - buildrequest_id = build_obj.buildrequest.pk - build['buildrequest_id'] = buildrequest_id - - build['recipes_parsed_percentage'] = \ - int((build_obj.recipes_parsed / - build_obj.recipes_to_parse) * 100) - - tasks_complete_percentage = 0 - if build_obj.outcome in (Build.SUCCEEDED, Build.FAILED): - tasks_complete_percentage = 100 - elif build_obj.outcome == Build.IN_PROGRESS: - tasks_complete_percentage = build_obj.completeper() - build['tasks_complete_percentage'] = tasks_complete_percentage - - build['state'] = build_obj.get_state() - - build['errors'] = build_obj.errors.count() - build['dashboard_errors_url'] = dashboard_url + '#errors' - - build['warnings'] = build_obj.warnings.count() - build['dashboard_warnings_url'] = dashboard_url + '#warnings' - - build['buildtime'] = sectohms(build_obj.timespent_seconds) - build['buildtime_url'] = buildtime_url - - build['rebuild_url'] = rebuild_url - build['cancel_url'] = cancel_url - - build['is_default_project_build'] = build_obj.project.is_default - - build['build_targets_json'] = \ - json(get_tasks(build_obj.target_set.all())) - - # convert completed_on time to user's timezone - completed_on = timezone.localtime(build_obj.completed_on) - - completed_on_template = '%H:%M' - if self._was_yesterday_or_earlier(completed_on): - completed_on_template = '%d/%m/%Y ' + completed_on_template - build['completed_on'] = completed_on.strftime( - completed_on_template) - - targets = [] - target_objs = build_obj.get_sorted_target_list() - for target_obj in target_objs: - if target_obj.task: - targets.append(target_obj.target + ':' + target_obj.task) - else: - targets.append(target_obj.target) - build['targets'] = ' '.join(targets) - - # abbreviated form of the full target list - abbreviated_targets = '' - num_targets = len(targets) - if num_targets > 0: - abbreviated_targets = targets[0] - if num_targets > 1: - abbreviated_targets += (' +%s' % (num_targets - 1)) - build['targets_abbreviated'] = abbreviated_targets - - recent_builds.append(build) - - return JsonResponse(recent_builds, safe=False) - - class XhrCustomRecipe(View): """ Create a custom image recipe """
lib/toaster/toastergui/urls.py +2 -1
@@ -24,6 +24,7 @@ from toastergui import tables from toastergui import buildtables from toastergui import typeaheads from toastergui import api +from toastergui import widgets urlpatterns = patterns('toastergui.views', # landing page
@@ -224,7 +225,7 @@ urlpatterns = patterns('toastergui.views', api.XhrProject.as_view(), name='xhr_project'), - url(r'^mostrecentbuilds$', api.MostRecentBuildsView.as_view(), + url(r'^mostrecentbuilds$', widgets.MostRecentBuildsView.as_view(), name='most_recent_builds'), # default redirection
lib/toaster/toastergui/widgets.py +125 -16
202 lines not on this page · raw message
Signature · 1 line
2.6.6
PATCH 3/10 toaster: libtoaster Add a global notification set/show mechanism
In reply to Ed BartoshFrom: Michael Wood <[email protected]>
We now have a number of places where we show change notifications based on an event in a previous page (imported a layer, deleted a build, deleted a project etc) and we show these notifications on various pages so we add a simple notification utility to libtoaster.
--- lib/toaster/toastergui/static/js/libtoaster.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+)
lib/toaster/toastergui/static/js/libtoaster.js +26 -0
@@ -449,6 +449,16 @@ var libtoaster = (function () { ajaxLoadingTimerEnabled = false; } + /* Utility function to set a notification for the next page load */ + function _setNotification(name, message){ + var data = { + name: name, + message: message + }; + + $.cookie('toaster-notification', JSON.stringify(data), { path: '/'}); + } + return { enableAjaxLoadingTimer: _enableAjaxLoadingTimer, disableAjaxLoadingTimer: _disableAjaxLoadingTimer,
@@ -468,6 +478,7 @@ var libtoaster = (function () { showChangeNotification : _showChangeNotification, createCustomRecipe: _createCustomRecipe, makeProjectNameValidation: _makeProjectNameValidation, + setNotification: _setNotification, }; })();
@@ -502,6 +513,21 @@ function reload_params(params) { /* Things that happen for all pages */ $(document).ready(function() { + (function showNotificationRequest(){ + var cookie = $.cookie('toaster-notification'); + + if (!cookie) + return; + + var notificationData = JSON.parse(cookie); + + libtoaster.showChangeNotification(notificationData.message); + + $.removeCookie('toaster-notification', { path: "/"}); + })(); + + + var ajaxLoadingTimer; /* If we don't have a console object which might be the case in some
Signature · 1 line
2.6.6
PATCH 4/10 toaster: project page Implement front end feature to delete project
In reply to Ed BartoshFrom: Michael Wood <[email protected]>
Add confirm modal and api calls to delete a project from the project dashboard.
[YOCTO #6238]
--- lib/toaster/toastergui/api.py | 15 +++++++--- lib/toaster/toastergui/static/js/projectpage.js | 30 ++++++++++++++++++- lib/toaster/toastergui/static/js/projecttopbar.js | 6 ++-- .../toastergui/templates/baseprojectpage.html | 6 ++++ lib/toaster/toastergui/templates/project.html | 35 +++++++++++++--------- .../toastergui/templates/projecttopbar.html | 2 +- 6 files changed, 71 insertions(+), 23 deletions(-)
lib/toaster/toastergui/api.py +11 -4
@@ -393,7 +393,7 @@ class XhrCustomRecipeId(View): """ Get Custom Image recipe or return an error response""" try: custom_recipe = \ - CustomImageRecipe.objects.get(pk=recipe_id) + CustomImageRecipe.objects.get(pk=recipe_id) return custom_recipe, None except CustomImageRecipe.DoesNotExist:
@@ -418,8 +418,12 @@ class XhrCustomRecipeId(View): if error: return error + project = custom_recipe.project + custom_recipe.delete() - return JsonResponse({"error": "ok"}) + return JsonResponse({"error": "ok", + "gotoUrl": reverse("projectcustomimages", + args=(project.pk,))}) class XhrCustomRecipePackages(View):
@@ -820,8 +824,11 @@ class XhrProject(View): def delete(self, request, *args, **kwargs): try: - Project.objects.get(kwargs['project_id']).delete() + Project.objects.get(pk=kwargs['project_id']).delete() except Project.DoesNotExist: return error_response("Project %s does not exist" % kwargs['project_id']) - return JsonResponse({"error": "ok"}) + return JsonResponse({ + "error": "ok", + "gotoUrl": reverse("all-projects", args=[]) + })
lib/toaster/toastergui/static/js/projectpage.js +29 -1
@@ -45,6 +45,9 @@ function projectPageInit(ctx) { /* Now we're really ready show the page */ $("#project-page").show(); + + /* Set the project name in the delete modal */ + $("#delete-project-modal .project-name").text(prjInfo.name); }); (function notificationRequest(){
@@ -328,7 +331,32 @@ function projectPageInit(ctx) { $("#delete-project-confirmed").click(function(e){ e.preventDefault(); - + libtoaster.disableAjaxLoadingTimer(); + $(this).find('[data-role="submit-state"]').hide(); + $(this).find('[data-role="loading-state"]').show(); + $(this).attr("disabled", "disabled"); + $('#delete-project-modal [data-dismiss="modal"]').hide(); + + $.ajax({ + type: 'DELETE', + url: libtoaster.ctx.xhrProjectUrl, + headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, + success: function (data) { + if (data.error !== "ok") { + console.warn(data.error); + } else { + var msg = $('<span>You have deleted <strong>1</strong> project: <strong id="project-deleted"></strong></span>'); + + msg.find("#project-deleted").text(libtoaster.ctx.projectName); + libtoaster.setNotification("project-deleted", msg.html()); + + window.location.replace(data.gotoUrl); + } + }, + error: function (data) { + console.warn(data); + } + }); }); }
lib/toaster/toastergui/static/js/projecttopbar.js +3 -3
@@ -4,7 +4,7 @@ function projectTopBarInit(ctx) { var projectNameForm = $("#project-name-change-form"); var projectNameContainer = $("#project-name-container"); - var projectName = $("#project-name"); + var projectName = $(".project-name"); var projectNameFormToggle = $("#project-change-form-toggle"); var projectNameChangeCancel = $("#project-name-change-cancel");
@@ -25,14 +25,14 @@ function projectTopBarInit(ctx) { e.preventDefault(); projectNameForm.hide(); projectNameContainer.fadeIn(); - $("#project-name-change-input").val(projectName.text()); + $("#project-name-change-input").val(projectName.first().text()); }); $("#project-name-change-btn").click(function(){ var newProjectName = $("#project-name-change-input").val(); libtoaster.editCurrentProject({ projectName: newProjectName }, function (){ - projectName.html(newProjectName); + projectName.text(newProjectName); libtoaster.ctx.projectName = newProjectName; projectNameChangeCancel.click(); });
lib/toaster/toastergui/templates/baseprojectpage.html +6 -0
@@ -34,6 +34,12 @@ $(document).ready(function(){ <li><a href="{% url 'projectlayers' project.id %}">Layers</a></li> <li class="nav-header">Extra configuration</li> <li><a href="{% url 'projectconf' project.id %}">BitBake variables</a></li> + + <li class="nav-header">Actions</li> + <li> + <a href="#delete-project-modal" role="button" class="text-danger" data-toggle="modal" data-target="#delete-project-modal"> + <i class="icon-trash text-danger"></i> Delete project</a> + </li> </ul> </div> <div class="col-md-10">
lib/toaster/toastergui/templates/project.html +21 -14
@@ -24,30 +24,37 @@ }); </script> -{% comment %} -<!-- Comment out the ability to change the project release, until we decide what to do this functionality --> -<div id="change-release-modal" class="modal hide fade in" tabindex="-1" role="dialog" aria-labelledby="change-release-modal" aria-hidden="false"> +<div id="delete-project-modal" class="modal fade" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="false"> <div class="modal-dialog"> <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button> - <h3>Changing Yocto Project release to <span class="proposed-release-change-name"></span></h3> + <h4>Are you sure you want to delete this project?</h4> </div> <div class="modal-body"> - <p>The following added layers do not exist for <span class="proposed-release-change-name"></span>: </p> - <ul id="layers-to-remove-list"> - </ul> - <p>If you change the Yocto Project release to <span class="proposed-release-change-name"></span>, the above layers will be deleted from your added layers.</p> + <p>Deleting the <strong class="project-name"></strong> project will remove forever:</p> + <ul> + <li>Its configuration information</li> + <li>Its imported layers</li> + <li>Its custom images</li> + <li>All its build information</li> + </ul> </div> <div class="modal-footer"> - <button id="change-release-and-rm-layers" data-dismiss="modal" type="submit" class="btn btn-primary">Change release and delete layers</button> - <button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button> + <button type="button" class="btn btn-primary" id="delete-project-confirmed"> + <span data-role="submit-state">Delete project</span> + <span data-role="loading-state" style="display:none"> + <span class="fa-pulse"> + <i class="fa-pulse icon-spinner"></i> + </span> + Deleting project... + </span> + </button> + <button type="button" class="btn btn-link" data-dismiss="modal">Cancel</button> </div> </div><!-- /.modal-content --> </div><!-- /.modal-dialog --> -</div><!-- /.modal --> -{% endcomment %} +</div> + <div class="row" id="project-page" style="display:none"> <div class="col-md-6">
lib/toaster/toastergui/templates/projecttopbar.html +1 -1
@@ -24,7 +24,7 @@ <!-- project name --> <div class="page-header"> <h1 id="project-name-container"> - <span id="project-name">{{project.name}}</span> + <span class="project-name">{{project.name}}</span> <span class="glyphicon glyphicon-edit" id="project-change-form-toggle"></i>
Signature · 1 line
2.6.6
PATCH 5/10 toaster: alerts and modals Avoid modals and alerts overlaying each other
In reply to Ed BartoshFrom: Michael Wood <[email protected]>
Make sure that when we spawn a modal we clear any notifications and also make sure that old notifications are cleared before showing a new one.
--- lib/toaster/toastergui/static/js/layerDepsModal.js | 12 ++++++++++-- lib/toaster/toastergui/static/js/libtoaster.js | 13 ++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-)
lib/toaster/toastergui/static/js/layerDepsModal.js +10 -2
@@ -6,7 +6,12 @@ * addToProject: Whether to add layers to project on accept * successAdd: function to run on success */ -function showLayerDepsModal(layer, dependencies, title, body, addToProject, successAdd) { +function showLayerDepsModal(layer, + dependencies, + title, + body, + addToProject, + successAdd) { if ($("#dependencies-modal").length === 0) { $.get(libtoaster.ctx.htmlUrl + "/layer_deps_modal.html", function(html){
@@ -43,7 +48,10 @@ function showLayerDepsModal(layer, dependencies, title, body, addToProject, succ $("#dependencies-modal").data("deps", dependencies); - $('#dependencies-modal').modal('show'); + /* Clear any alert notifications before showing the modal */ + $(".alert").fadeOut(function(){ + $('#dependencies-modal').modal('show'); + }); /* Discard the old submission function */ $("#dependencies-modal-form").unbind('submit');
lib/toaster/toastergui/static/js/libtoaster.js +10 -3
@@ -342,10 +342,12 @@ var libtoaster = (function () { } function _showChangeNotification(message){ - var alertMsg = $("#change-notification-msg"); + $(".alert").fadeOut().promise().done(function(){ + var alertMsg = $("#change-notification-msg"); - alertMsg.html(message); - $("#change-notification, #change-notification *").fadeIn(); + alertMsg.html(message); + $("#change-notification, #change-notification *").fadeIn(); + }); } function _createCustomRecipe(name, baseRecipeId, doneCb){
@@ -716,6 +718,11 @@ $(document).ready(function() { }); } + /* Make sure we don't have a notification overlay a modal */ + $(".modal").on('show.bs.modal', function(){ + $(".alert-dismissible").fadeOut(); + }); + if (libtoaster.debug) { check_for_duplicate_ids(); } else {
Signature · 1 line
2.6.6
PATCH 6/10 toaster: Add backend API for deleting a build
In reply to Ed BartoshFrom: Michael Wood <[email protected]>
--- lib/toaster/toastergui/api.py | 30 ++++++++++++++++++++++++++++++ lib/toaster/toastergui/urls.py | 4 ++++ 2 files changed, 34 insertions(+)
lib/toaster/toastergui/api.py +30 -0
@@ -832,3 +832,33 @@ class XhrProject(View): "error": "ok", "gotoUrl": reverse("all-projects", args=[]) }) + + +class XhrBuild(View): + """ Delete a build object + + Entry point: /xhr_build/<build_id> + """ + def delete(self, request, *args, **kwargs): + """ + Delete build data + + Args: + build_id = build_id + + Returns: + {"error": "ok"} + or + {"error": <error message>} + """ + try: + build = Build.objects.get(pk=kwargs['build_id']) + project = build.project + build.delete() + except Build.DoesNotExist: + return error_response("Build %s does not exist" % + kwargs['build_id']) + return JsonResponse({ + "error": "ok", + "gotoUrl": reverse("projectbuilds", args=(project.pk,)) + })
lib/toaster/toastergui/urls.py +4 -0
@@ -225,6 +225,10 @@ urlpatterns = patterns('toastergui.views', api.XhrProject.as_view(), name='xhr_project'), + url(r'xhr_build/(?P<build_id>\d+)$', + api.XhrBuild.as_view(), + name='xhr_build'), + url(r'^mostrecentbuilds$', widgets.MostRecentBuildsView.as_view(), name='most_recent_builds'),
Signature · 1 line
2.6.6
PATCH 7/10 toaster: Add front end controls for deleting a build
In reply to Ed BartoshFrom: Michael Wood <[email protected]>
Add front end modal and controls for deleting a build from the build dashboard.
Also convert the Actions list to links instead of buttons as per the design.
[YOCTO #6238]
--- .../toastergui/templates/basebuildpage.html | 138 ++++++++++++++++----- .../toastergui/templates/builddashboard.html | 2 +- 2 files changed, 105 insertions(+), 35 deletions(-)
lib/toaster/toastergui/templates/basebuildpage.html +104 -34
@@ -3,8 +3,88 @@ {% load project_url_tag %} {% load objects_to_dictionaries_filter %} {% load humanize %} +{% load field_values_filter %} {% block pagecontent %} + <script> + var configVarUrl = "{% url 'configvars' build.id %}"; + + $(document).ready(function(){ + + $("#delete-build-confirm").click(function(){ + libtoaster.disableAjaxLoadingTimer(); + $(this).find('[data-role="submit-state"]').hide(); + $(this).find('[data-role="loading-state"]').show(); + $(this).attr("disabled", "disabled"); + + /* Make the modal non cancelable while delete is in progress */ + $('#delete-build-modal button[data-dismiss="modal"]').hide(); + + $.ajax({ + type: 'DELETE', + url: "{% url 'xhr_build' build.id %}", + headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, + success: function (data) { + if (data.error !== "ok") { + console.warn(data.error); + } else { + window.location.replace(data.gotoUrl); + } + }, + error: function (data) { + console.warn(data); + } + }); + }); + + + $('#breadcrumb > li').append('<span class="divider">→</span>'); + $('#breadcrumb > li:last').addClass("active"); + $('#breadcrumb > li:last > span').remove(); + + $("#build-menu li a").each(function(){ + /* Set the page active state in the Build menu */ + var currentUrl = window.location.href.split('?')[0]; + if (currentUrl === $(this).prop("href")){ + $(this).parent().addClass("active"); + } else { + /* Special case the configvar as this is part of configuration + * page but is a separate url + */ + if (window.location.pathname === configVarUrl){ + $("#menu-configuration").addClass("active"); + } else { + $(this).parent().removeClass("active"); + } + } + }); + }); + </script> + + + +<div class="modal fade" tabindex="-1" role="dialog" id="delete-build-modal" style="display: none;" data-backdrop="static" data-keyboard="false"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-body"> + <p>Are you sure you want to delete the build <strong>{{build.get_sorted_target_list|field_values:"target"|join:", "}} {{build.machine}}</strong> completed on <strong>{{build.completed_on|date:"d/m/y H:i"}}</strong>?</p> + </div> + <div class="modal-footer"> + <button id="delete-build-confirm" class="btn btn-primary btn-large"> + <span data-role="submit-state">Delete build</span> + <span data-role="loading-state" style="display:none"> + <span class="fa-pulse"> + <i class="icon-spinner"></i> + </span> + Deleting build... + </span> + </button> + <button type="button" class="btn btn-link" data-dismiss="modal">Cancel</button> + </div> + </div><!-- /.modal-content --> + </div><!-- /.modal-dialog --> +</div> <!-- / modal --> + <div class="row"> <!-- breadcrumbs --> <div class="col-md-12">
@@ -22,36 +102,6 @@ </li> {% block localbreadcrumb %}{% endblock %} </ul> - <script> - - var configVarUrl = "{% url 'configvars' build.id %}"; - - $(document).ready(function(){ - $('#breadcrumb > li').append('<span class="divider">→</span>'); - $('#breadcrumb > li:last').addClass("active"); - $('#breadcrumb > li:last > span').remove(); - - $("#build-menu li a").each(function(){ - /* Set the page active state in the Build menu */ - var currentUrl = window.location.href.split('?')[0]; - if (currentUrl === $(this).prop("href")){ - $(this).parent().addClass("active"); - } else { - /* Special case the configvar as this is part of configuration - * page but is a separate url, and the direct links to errors - * and warnings, which are part of the build dashboard - */ - if (window.location.pathname === configVarUrl){ - $("#menu-configuration").addClass("active"); - } else if (currentUrl.indexOf('error') > 1 || currentUrl.indexOf('warning') > 1){ - $("#menu-dashboard").addClass("active"); - } else { - $(this).parent().removeClass("active"); - } - } - }); - }); - </script> </div> </div>
@@ -86,13 +136,22 @@ <li><a href="{% url 'cputime' build.pk %}">CPU usage</a></li> <li><a href="{% url 'diskio' build.pk %}">Disk I/O</a></li> - <li class="nav-header">Actions</li> - <a class="btn btn-default btn-block navbar-btn" href="{% url 'build_artifact' build.id 'cookerlog' build.id %}">Download build log</a> + <li class="nav-header">Actions</li> + <li> + <a href="{% url 'build_artifact' build.id 'cookerlog' build.id %}"> + <span class="glyphicon glyphicon-download-alt"></span> + Download build log + </a> + </li> {% with build.get_custom_image_recipes as custom_image_recipes %} {% if custom_image_recipes.count > 0 %} <!-- edit custom image built during this build --> - <button class="btn btn-default btn-block navbar-btn" data-role="edit-custom-image-trigger">Edit custom image</button> + <li> + <a href="#" data-role="edit-custom-image-trigger"> + <span class="glyphicon glyphicon-edit"></span> + Edit custom image + </a> {% include 'editcustomimage_modal.html' %} <script> var editableCustomImageRecipes = {{ custom_image_recipes | objects_to_dictionaries:"id,name" | json }};
@@ -117,12 +176,17 @@ }); }); </script> + </li> {% endif %} {% endwith %} <!-- new custom image from image recipe in this build --> {% if build.has_image_recipes %} - <button class="btn btn-default btn-block navbar-btn" data-role="new-custom-image-trigger">New custom image</button> + <li> + <a href="#" data-role="new-custom-image-trigger"> + <span class="glyphicon glyphicon-plus"></span> + New custom image + </a> {% include 'newcustomimage_modal.html' %} <script> // imageRecipes includes both custom image recipes and built-in
@@ -147,6 +211,12 @@ }); </script> {% endif %} + + <li> + <a href="#delete-build-modal" id="delete-build" data-toggle="modal" data-target="#delete-build-modal" class="text-danger"> + <span class="glyphicon glyphicon-trash"></span> + Delete build + </a> </ul> </div> <!-- end left sidebar container -->
lib/toaster/toastergui/templates/builddashboard.html +1 -1
@@ -34,7 +34,7 @@ <a href="#warnings" class="show-warnings"> {{build.warnings.count}} warning{{build.warnings.count|pluralize}}</a> {% endif %} {% if build.cooker_log_path %} - <a class="alert-link pull-right log" href="{% url 'build_artifact' build.id "cookerlog" build.id %}">Download build log</a> + <a class="pull-right log" href="{% url 'build_artifact' build.id "cookerlog" build.id %}">Download build log</a> {% endif %} <span class="pull-right"> Build time:
Signature · 1 line
2.6.6
PATCH 8/10 toaster: importlayer Convert success import to new notification system
In reply to Ed BartoshFrom: Michael Wood <[email protected]>
Use the simpler libtoaster method of showing a notification about successful import of a layer.
Also a number of whitespace clean ups.
--- lib/toaster/toastergui/static/js/importlayer.js | 59 ++++++++++++++++------ lib/toaster/toastergui/static/js/projectpage.js | 66 +------------------------ 2 files changed, 47 insertions(+), 78 deletions(-)
lib/toaster/toastergui/static/js/importlayer.js +45 -14
@@ -17,7 +17,9 @@ function importLayerPageInit (ctx) { var currentLayerDepSelection; var validLayerName = /^(\w|-)+$/; - libtoaster.makeTypeahead(layerDepInput, libtoaster.ctx.layersTypeAheadUrl, { include_added: "true" }, function(item){ + libtoaster.makeTypeahead(layerDepInput, + libtoaster.ctx.layersTypeAheadUrl, + { include_added: "true" }, function(item){ currentLayerDepSelection = item; });
@@ -95,7 +97,8 @@ function importLayerPageInit (ctx) { $("#layer-deps-list").append(newLayerDep); - libtoaster.getLayerDepsForProject(currentLayerDepSelection.layerdetailurl, function (data){ + libtoaster.getLayerDepsForProject(currentLayerDepSelection.layerdetailurl, + function (data){ /* These are the dependencies of the layer added as a dependency */ if (data.list.length > 0) { currentLayerDepSelection.url = currentLayerDepSelection.layerdetailurl;
@@ -150,7 +153,9 @@ function importLayerPageInit (ctx) { var body = "<strong>"+layer.name+"</strong>'s dependencies ("+ depNames.join(", ")+"</span>) require some layers that are not added to your project. Select the ones you want to add:</p>"; - showLayerDepsModal(layer, depDepsArray, title, body, false, function(layerObsList){ + showLayerDepsModal(layer, + depDepsArray, + title, body, false, function(layerObsList){ /* Add the accepted layer dependencies' ids to the allDeps array */ for (var key in layerObsList){ allDeps.push(layerObsList[key].id);
@@ -191,9 +196,8 @@ function importLayerPageInit (ctx) { if (data.error != "ok") { console.log(data.error); } else { - /* Success layer import now go to the project page */ - $.cookie('layer-imported-alert', JSON.stringify(data), { path: '/'}); - window.location.replace(libtoaster.ctx.projectPageUrl+'?notify=layer-imported'); + createImportedNotification(data); + window.location.replace(libtoaster.ctx.projectPageUrl); } }, error: function (data) {
@@ -204,6 +208,30 @@ function importLayerPageInit (ctx) { } }); + /* Layer imported notification */ + function createImportedNotification(imported){ + var message = "Layer imported"; + + if (imported.deps_added.length === 0) { + message = "You have imported <strong><a class=\"alert-link\" href=\""+imported.imported_layer.layerdetailurl+"\">"+imported.imported_layer.name+"</a></strong> and added it to your project."; + } else { + + var links = "<a href=\""+imported.imported_layer.layerdetailurl+"\">"+imported.imported_layer.name+"</a>, "; + + imported.deps_added.map (function(item, index){ + links +='<a href="'+item.layerdetailurl+'">'+item.name+'</a>'; + /*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 */ + message = 'You have imported <strong><a href="'+imported.imported_layer.layerdetailurl+'">'+imported.imported_layer.name+'</a></strong> and added <strong>'+(imported.deps_added.length+1)+'</strong> layers to your project: <strong>'+links+'</strong>'; + } + + libtoaster.setNotification("layer-imported", message); + } + function enable_import_btn(enabled) { var importAndAddHint = $("#import-and-add-hint");
@@ -230,10 +258,13 @@ function importLayerPageInit (ctx) { } if (valid) { - if ($("#local-dir-radio").prop("checked") && localDirPath.val().length > 0) { + if ($("#local-dir-radio").prop("checked") && + localDirPath.val().length > 0) { enable_import_btn(true); } - if ($("#git-repo-radio").prop("checked") && vcsURLInput.val().length > 0 && gitRefInput.val().length > 0) { + + if ($("#git-repo-radio").prop("checked") && + vcsURLInput.val().length > 0 && gitRefInput.val().length > 0) { enable_import_btn(true); } }
@@ -266,13 +297,13 @@ function importLayerPageInit (ctx) { } layerNameInput.on('blur', function() { - if (!$(this).val()){ - return; - } - var name = $(this).val(); + if (!$(this).val()){ + return; + } + var name = $(this).val(); - /* Check if the layer name exists */ - $.getJSON(libtoaster.ctx.layersTypeAheadUrl, + /* Check if the layer name exists */ + $.getJSON(libtoaster.ctx.layersTypeAheadUrl, { include_added: "true" , search: name, format: "json" }, function(layer) { if (layer.results.length > 0) {
lib/toaster/toastergui/static/js/projectpage.js +2 -64
72 lines not on this page · raw message
Signature · 1 line
2.6.6
PATCH 9/10 toaster: customrecipe Add frontend feature to delete custom image recipe
In reply to Ed BartoshFrom: Michael Wood <[email protected]>
[YOCTO #8132]
--- lib/toaster/toastergui/static/js/customrecipe.js | 32 ++++++++++++++++++++ lib/toaster/toastergui/templates/customrecipe.html | 35 +++++++++++++++++++--- 2 files changed, 63 insertions(+), 4 deletions(-)
lib/toaster/toastergui/static/js/customrecipe.js +32 -0
@@ -281,4 +281,36 @@ function customRecipePageInit(ctx) { window.location.replace(libtoaster.ctx.projectBuildsUrl); }); }); + + $("#delete-custom-recipe-confirmed").click(function(e){ + e.preventDefault(); + libtoaster.disableAjaxLoadingTimer(); + $(this).find('[data-role="submit-state"]').hide(); + $(this).find('[data-role="loading-state"]').show(); + $(this).attr("disabled", "disabled"); + + $.ajax({ + type: 'DELETE', + url: ctx.recipe.xhrCustomRecipeUrl, + headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, + success: function (data) { + if (data.error !== "ok") { + console.warn(data.error); + } else { + var msg = $('<span>You have deleted <strong>1</strong> custom image: <strong id="deleted-custom-image-name"></strong></span>'); + msg.find("#deleted-custom-image-name").text(ctx.recipe.name); + + libtoaster.setNotification("custom-image-recipe-deleted", + msg.html()); + + window.location.replace(data.gotoUrl); + } + }, + error: function (data) { + console.warn(data); + } + }); + }); + + }
lib/toaster/toastergui/templates/customrecipe.html +31 -4
@@ -28,6 +28,7 @@ includedPackagesCount: {{recipe.includes_set.count}}, baseRecipeId: {{recipe.base_recipe.pk}}, xhrPackageListUrl: "{% url 'xhr_customrecipe_packages' recipe.pk %}", + xhrCustomRecipeUrl: "{% url 'xhr_customrecipe_id' recipe.pk %}", } };
@@ -39,6 +40,32 @@ } }); </script> + +<!-- Delete recipe modal --> +<div id="delete-recipe-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="false"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-body"> + <p>Are you sure you want to delete the <strong>{{recipe.name}}</strong> + custom image?</p> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-primary" id="delete-custom-recipe-confirmed"> + <span data-role="submit-state">Delete custom image</span> + <span data-role="loading-state" style="display:none"> + <span class="fa-pulse"> + <i class="fa-pulse icon-spinner"></i> + </span> + Deleting custom image... + </span> + </button> + <button type="button" class="btn btn-link" data-dismiss="modal">Cancel</button> + </div> + </div><!-- /.modal-content --> + </div><!-- /.modal-dialog --> +</div><!-- /.modal --> +<!-- end delete recipe modal --> + <!-- package dependencies modal --> <div id="package-deps-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="false"> <div class="modal-dialog">
@@ -229,10 +256,10 @@ <span class="glyphicon glyphicon-question-sign get-help" title="All custom images have their license set to MIT. This is because the license applies only to the recipe (.bb) file, and not to the image itself. To see which licenses apply to the image you must check the license manifest generated with each build"></i> </dd> </dl> - <!-- - <i class="icon-trash no-tooltip"></i> - <a href="#" class="error" id="delete">Delete custom image</a> - --> + <i class="icon-trash text-danger"></i> + <a href="#delete-recipe-modal" data-target="#delete-recipe-modal" data-toggle="modal" class="text-danger" id="delete-recipe"> + Delete custom image + </a> </div> </div> </div>
Signature · 1 line
2.6.6
PATCH 10/10 toaster: layerdetails Update implementation of delete imported layer
In reply to Ed BartoshFrom: Michael Wood <[email protected]>
Update the implementation of delete an imported layer so that it is consistent with the other delete messages and wording. Also use the new libtoaster way of setting a notification that the delete was successful.
--- lib/toaster/toastergui/api.py | 2 +- lib/toaster/toastergui/static/js/layerdetails.js | 7 +++++-- lib/toaster/toastergui/templates/layerdetails.html | 9 ++++++--- 3 files changed, 12 insertions(+), 6 deletions(-)
lib/toaster/toastergui/api.py +1 -1
@@ -221,7 +221,7 @@ class XhrLayer(View): return JsonResponse({ "error": "ok", - "redirect": reverse('project', args=(kwargs['pid'],)) + "gotoUrl": reverse('project', args=(kwargs['pid'],)) })
lib/toaster/toastergui/static/js/layerdetails.js +5 -2
@@ -391,7 +391,9 @@ function layerDetailsPageInit (ctx) { }); $("#layer-delete-confirmed").click(function(){ - $.cookie("layer-deleted", ctx.layerVersion.name, { path: '/'}); + + var message = $('<span>You have deleted <strong>1</strong> layer from your project: <strong id="deleted-layer-name"></strong>'); + message.find("#deleted-layer-name").text(ctx.layerVersion.name); $.ajax({ type: "DELETE",
@@ -401,7 +403,8 @@ function layerDetailsPageInit (ctx) { if (data.error != "ok") { console.warn(data.error); } else { - window.location = data.redirect + "?notify=layer-deleted"; + libtoaster.setNotification("layer-deleted", message.html()); + window.location.replace(data.gotoUrl); } }, error: function(data) {
lib/toaster/toastergui/templates/layerdetails.html +6 -3
@@ -6,14 +6,16 @@ {% block title %} {{layerversion.layer.name}} - {{project.name}} - Toaster {% endblock %} {% block pagecontent %} -<div id="delete-layer-modal" class="modal fade" tabindex="-1" role="dialog"> +<div id="delete-layer-modal" class="modal fade" tabindex="-1" role="dialog" + data-keyboard="false" data-backdrop="static"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-body"> Are you sure you want to delete the <strong>{{layerversion.layer.name}}</strong> layer? </div> <div class="modal-footer"> - <button type="button" id="layer-delete-confirmed" class="btn btn-primary">Delete</button> + <button type="button" id="layer-delete-confirmed" class="btn + btn-primary">Delete layer</button> <button type="button" class="btn btn-default btn-link" data-dismiss="modal">Cancel</button> </div> </div>
@@ -358,7 +360,8 @@ {# Only show delete link for imported layers #} {% if layerversion.layer_source == layer_source.TYPE_IMPORTED %} <i class="icon-trash text-danger"></i> - <a href="#delete-layer-modal" role="button" class="text-danger" data-toggle="modal" data-target="#delete-layer-modal">Delete {{layerversion.layer.name}}</a> + <a href="#delete-layer-modal" role="button" class="text-danger" + data-toggle="modal" data-target="#delete-layer-modal">Delete layer</a> {% endif %} </div> </div>
Signature · 1 line
2.6.6