diff options
Diffstat (limited to 'poky/bitbake/lib/toaster/toastergui')
26 files changed, 1148 insertions, 51 deletions
diff --git a/poky/bitbake/lib/toaster/toastergui/api.py b/poky/bitbake/lib/toaster/toastergui/api.py index ab6ba69e0e..564d595a1c 100644 --- a/poky/bitbake/lib/toaster/toastergui/api.py +++ b/poky/bitbake/lib/toaster/toastergui/api.py @@ -22,7 +22,9 @@ import os import re import logging import json +import subprocess from collections import Counter +from shutil import copyfile from orm.models import Project, ProjectTarget, Build, Layer_Version from orm.models import LayerVersionDependency, LayerSource, ProjectLayer @@ -38,6 +40,18 @@ from django.core.urlresolvers import reverse from django.db.models import Q, F from django.db import Error from toastergui.templatetags.projecttags import filtered_filesizeformat +from django.utils import timezone +import pytz + +# development/debugging support +verbose = 2 +def _log(msg): + if 1 == verbose: + print(msg) + elif 2 == verbose: + f1=open('/tmp/toaster.log', 'a') + f1.write("|" + msg + "|\n" ) + f1.close() logger = logging.getLogger("toaster") @@ -137,6 +151,130 @@ class XhrBuildRequest(View): return response +class XhrProjectUpdate(View): + + def get(self, request, *args, **kwargs): + return HttpResponse() + + def post(self, request, *args, **kwargs): + """ + Project Update + + Entry point: /xhr_projectupdate/<project_id> + Method: POST + + Args: + pid: pid of project to update + + Returns: + {"error": "ok"} + or + {"error": <error message>} + """ + + project = Project.objects.get(pk=kwargs['pid']) + logger.debug("ProjectUpdateCallback:project.pk=%d,project.builddir=%s" % (project.pk,project.builddir)) + + if 'do_update' in request.POST: + + # Extract any default image recipe + if 'default_image' in request.POST: + project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,str(request.POST['default_image'])) + else: + project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,'') + + logger.debug("ProjectUpdateCallback:Chain to the build request") + + # Chain to the build request + xhrBuildRequest = XhrBuildRequest() + return xhrBuildRequest.post(request, *args, **kwargs) + + logger.warning("ERROR:XhrProjectUpdate") + response = HttpResponse() + response.status_code = 500 + return response + +class XhrSetDefaultImageUrl(View): + + def get(self, request, *args, **kwargs): + return HttpResponse() + + def post(self, request, *args, **kwargs): + """ + Project Update + + Entry point: /xhr_setdefaultimage/<project_id> + Method: POST + + Args: + pid: pid of project to update default image + + Returns: + {"error": "ok"} + or + {"error": <error message>} + """ + + project = Project.objects.get(pk=kwargs['pid']) + logger.debug("XhrSetDefaultImageUrl:project.pk=%d" % (project.pk)) + + # set any default image recipe + if 'targets' in request.POST: + default_target = str(request.POST['targets']) + project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,default_target) + logger.debug("XhrSetDefaultImageUrl,project.pk=%d,project.builddir=%s" % (project.pk,project.builddir)) + return error_response('ok') + + logger.warning("ERROR:XhrSetDefaultImageUrl") + response = HttpResponse() + response.status_code = 500 + return response + + +# +# Layer Management +# +# Rules for 'local_source_dir' layers +# * Layers must have a unique name in the Layers table +# * A 'local_source_dir' layer is supposed to be shared +# by all projects that use it, so that it can have the +# same logical name +# * Each project that uses a layer will have its own +# LayerVersion and Project Layer for it +# * During the Paroject delete process, when the last +# LayerVersion for a 'local_source_dir' layer is deleted +# then the Layer record is deleted to remove orphans +# + +def scan_layer_content(layer,layer_version): + # if this is a local layer directory, we can immediately scan its content + if layer.local_source_dir: + try: + # recipes-*/*/*.bb + cmd = '%s %s' % ('ls', os.path.join(layer.local_source_dir,'recipes-*/*/*.bb')) + recipes_list = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.STDOUT).stdout.read() + recipes_list = recipes_list.decode("utf-8").strip() + if recipes_list and 'No such' not in recipes_list: + for recipe in recipes_list.split('\n'): + recipe_path = recipe[recipe.rfind('recipes-'):] + recipe_name = recipe[recipe.rfind('/')+1:].replace('.bb','') + recipe_ver = recipe_name.rfind('_') + if recipe_ver > 0: + recipe_name = recipe_name[0:recipe_ver] + if recipe_name: + ro, created = Recipe.objects.get_or_create( + layer_version=layer_version, + name=recipe_name + ) + if created: + ro.file_path = recipe_path + ro.summary = 'Recipe %s from layer %s' % (recipe_name,layer.name) + ro.description = ro.summary + ro.save() + + except Exception as e: + logger.warning("ERROR:scan_layer_content: %s" % e) + class XhrLayer(View): """ Delete, Get, Add and Update Layer information @@ -265,6 +403,7 @@ class XhrLayer(View): (csv)] """ + try: project = Project.objects.get(pk=kwargs['pid']) @@ -285,7 +424,13 @@ class XhrLayer(View): if layer_data['name'] in existing_layers: return JsonResponse({"error": "layer-name-exists"}) - layer = Layer.objects.create(name=layer_data['name']) + if ('local_source_dir' in layer_data): + # Local layer can be shared across projects. They have no 'release' + # and are not included in get_all_compatible_layer_versions() above + layer,created = Layer.objects.get_or_create(name=layer_data['name']) + _log("Local Layer created=%s" % created) + else: + layer = Layer.objects.create(name=layer_data['name']) layer_version = Layer_Version.objects.create( layer=layer, @@ -293,7 +438,7 @@ class XhrLayer(View): layer_source=LayerSource.TYPE_IMPORTED) # Local layer - if ('local_source_dir' in layer_data) and layer.local_source_dir: + if ('local_source_dir' in layer_data): ### and layer.local_source_dir: layer.local_source_dir = layer_data['local_source_dir'] # git layer elif 'vcs_url' in layer_data: @@ -325,6 +470,9 @@ class XhrLayer(View): 'layerdetailurl': layer_dep.get_detailspage_url(project.pk)}) + # Scan the layer's content and update components + scan_layer_content(layer,layer_version) + except Layer_Version.DoesNotExist: return error_response("layer-dep-not-found") except Project.DoesNotExist: @@ -529,7 +677,13 @@ class XhrCustomRecipe(View): recipe_path = os.path.join(layerpath, "recipes", "%s.bb" % recipe.name) with open(recipe_path, "w") as recipef: - recipef.write(recipe.generate_recipe_file_contents()) + content = recipe.generate_recipe_file_contents() + if not content: + # Delete this incomplete image recipe object + recipe.delete() + return error_response("recipe-parent-not-exist") + else: + recipef.write(recipe.generate_recipe_file_contents()) return JsonResponse( {"error": "ok", @@ -1014,8 +1168,24 @@ class XhrProject(View): state=BuildRequest.REQ_INPROGRESS): XhrBuildRequest.cancel_build(br) + # gather potential orphaned local layers attached to this project + project_local_layer_list = [] + for pl in ProjectLayer.objects.filter(project=project): + if pl.layercommit.layer_source == LayerSource.TYPE_IMPORTED: + project_local_layer_list.append(pl.layercommit.layer) + + # deep delete the project and its dependencies project.delete() + # delete any local layers now orphaned + _log("LAYER_ORPHAN_CHECK:Check for orphaned layers") + for layer in project_local_layer_list: + layer_refs = Layer_Version.objects.filter(layer=layer) + _log("LAYER_ORPHAN_CHECK:Ref Count for '%s' = %d" % (layer.name,len(layer_refs))) + if 0 == len(layer_refs): + _log("LAYER_ORPHAN_CHECK:DELETE orpahned '%s'" % (layer.name)) + Layer.objects.filter(pk=layer.id).delete() + except Project.DoesNotExist: return error_response("Project %s does not exist" % kwargs['project_id']) diff --git a/poky/bitbake/lib/toaster/toastergui/static/js/layerBtn.js b/poky/bitbake/lib/toaster/toastergui/static/js/layerBtn.js index 9f9eda1e1e..a5a6563d1a 100644 --- a/poky/bitbake/lib/toaster/toastergui/static/js/layerBtn.js +++ b/poky/bitbake/lib/toaster/toastergui/static/js/layerBtn.js @@ -67,6 +67,18 @@ function layerBtnsInit() { }); }); + $("td .set-default-recipe-btn").unbind('click'); + $("td .set-default-recipe-btn").click(function(e){ + e.preventDefault(); + var recipe = $(this).data('recipe-name'); + + libtoaster.setDefaultImage(null, recipe, + function(){ + /* Success */ + window.location.replace(libtoaster.ctx.projectSpecificPageUrl); + }); + }); + $(".customise-btn").unbind('click'); $(".customise-btn").click(function(e){ diff --git a/poky/bitbake/lib/toaster/toastergui/static/js/libtoaster.js b/poky/bitbake/lib/toaster/toastergui/static/js/libtoaster.js index 6f9b5d0f00..f2c45c833e 100644 --- a/poky/bitbake/lib/toaster/toastergui/static/js/libtoaster.js +++ b/poky/bitbake/lib/toaster/toastergui/static/js/libtoaster.js @@ -275,7 +275,8 @@ var libtoaster = (function () { function _addRmLayer(layerObj, add, doneCb){ if (layerObj.xhrLayerUrl === undefined){ - throw("xhrLayerUrl is undefined") + alert("ERROR: missing xhrLayerUrl object. Please file a bug."); + return; } if (add === true) { @@ -465,6 +466,108 @@ var libtoaster = (function () { $.cookie('toaster-notification', JSON.stringify(data), { path: '/'}); } + /* _updateProject: + * url: xhrProjectUpdateUrl or null for current project + * onsuccess: callback for successful execution + * onfail: callback for failed execution + */ + function _updateProject (url, targets, default_image, onsuccess, onfail) { + + if (!url) + url = libtoaster.ctx.xhrProjectUpdateUrl; + + /* Flatten the array of targets into a space spearated list */ + if (targets instanceof Array){ + targets = targets.reduce(function(prevV, nextV){ + return prev + ' ' + next; + }); + } + + $.ajax( { + type: "POST", + url: url, + data: { 'do_update' : 'True' , 'targets' : targets , 'default_image' : default_image , }, + headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, + success: function (_data) { + if (_data.error !== "ok") { + console.warn(_data.error); + } else { + if (onsuccess !== undefined) onsuccess(_data); + } + }, + error: function (_data) { + console.warn("Call failed"); + console.warn(_data); + if (onfail) onfail(data); + } }); + } + + /* _cancelProject: + * url: xhrProjectUpdateUrl or null for current project + * onsuccess: callback for successful execution + * onfail: callback for failed execution + */ + function _cancelProject (url, onsuccess, onfail) { + + if (!url) + url = libtoaster.ctx.xhrProjectCancelUrl; + + $.ajax( { + type: "POST", + url: url, + data: { 'do_cancel' : 'True' }, + headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, + success: function (_data) { + if (_data.error !== "ok") { + console.warn(_data.error); + } else { + if (onsuccess !== undefined) onsuccess(_data); + } + }, + error: function (_data) { + console.warn("Call failed"); + console.warn(_data); + if (onfail) onfail(data); + } }); + } + + /* _setDefaultImage: + * url: xhrSetDefaultImageUrl or null for current project + * targets: an array or space separated list of targets to set as default + * onsuccess: callback for successful execution + * onfail: callback for failed execution + */ + function _setDefaultImage (url, targets, onsuccess, onfail) { + + if (!url) + url = libtoaster.ctx.xhrSetDefaultImageUrl; + + /* Flatten the array of targets into a space spearated list */ + if (targets instanceof Array){ + targets = targets.reduce(function(prevV, nextV){ + return prev + ' ' + next; + }); + } + + $.ajax( { + type: "POST", + url: url, + data: { 'targets' : targets }, + headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, + success: function (_data) { + if (_data.error !== "ok") { + console.warn(_data.error); + } else { + if (onsuccess !== undefined) onsuccess(_data); + } + }, + error: function (_data) { + console.warn("Call failed"); + console.warn(_data); + if (onfail) onfail(data); + } }); + } + return { enableAjaxLoadingTimer: _enableAjaxLoadingTimer, disableAjaxLoadingTimer: _disableAjaxLoadingTimer, @@ -485,6 +588,9 @@ var libtoaster = (function () { createCustomRecipe: _createCustomRecipe, makeProjectNameValidation: _makeProjectNameValidation, setNotification: _setNotification, + updateProject : _updateProject, + cancelProject : _cancelProject, + setDefaultImage : _setDefaultImage, }; })(); diff --git a/poky/bitbake/lib/toaster/toastergui/static/js/mrbsection.js b/poky/bitbake/lib/toaster/toastergui/static/js/mrbsection.js index c0c5fa9589..f07ccf8181 100644 --- a/poky/bitbake/lib/toaster/toastergui/static/js/mrbsection.js +++ b/poky/bitbake/lib/toaster/toastergui/static/js/mrbsection.js @@ -86,7 +86,7 @@ function mrbSectionInit(ctx){ if (buildFinished(build)) { // a build finished: reload the whole page so that the build // shows up in the builds table - window.location.reload(); + window.location.reload(true); } else if (stateChanged(build)) { // update the whole template @@ -110,6 +110,8 @@ function mrbSectionInit(ctx){ // update the clone progress text selector = '#repos-cloned-percentage-' + build.id; $(selector).html(build.repos_cloned_percentage); + selector = '#repos-cloned-progressitem-' + build.id; + $(selector).html('('+build.progress_item+')'); // update the recipe progress bar selector = '#repos-cloned-percentage-bar-' + build.id; diff --git a/poky/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js b/poky/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js index dace8e3258..e55fffcef5 100644 --- a/poky/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js +++ b/poky/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js @@ -25,6 +25,8 @@ function newCustomImageModalInit(){ var duplicateNameMsg = "An image with this name already exists. Image names must be unique."; var duplicateImageInProjectMsg = "An image with this name already exists in this project." var invalidBaseRecipeIdMsg = "Please select an image to customise."; + var missingParentRecipe = "The parent recipe file was not found. Cancel this action, build any target (like 'quilt-native') to force all new layers to clone, and try again"; + var unknownError = "Unexpected error: "; // set button to "submit" state and enable text entry so user can // enter the custom recipe name @@ -62,6 +64,7 @@ function newCustomImageModalInit(){ if (nameInput.val().length > 0) { libtoaster.createCustomRecipe(nameInput.val(), baseRecipeId, function(ret) { + showSubmitState(); if (ret.error !== "ok") { console.warn(ret.error); if (ret.error === "invalid-name") { @@ -73,6 +76,10 @@ function newCustomImageModalInit(){ } else if (ret.error === "image-already-exists") { showNameError(duplicateImageInProjectMsg); return; + } else if (ret.error === "recipe-parent-not-exist") { + showNameError(missingParentRecipe); + } else { + showNameError(unknownError + ret.error); } } else { imgCustomModal.modal('hide'); diff --git a/poky/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js b/poky/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js index 69220aaf57..3f9e186708 100644 --- a/poky/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js +++ b/poky/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js @@ -14,6 +14,9 @@ function projectTopBarInit(ctx) { var newBuildTargetBuildBtn = $("#build-button"); var selectedTarget; + var updateProjectBtn = $("#update-project-button"); + var cancelProjectBtn = $("#cancel-project-button"); + /* Project name change functionality */ projectNameFormToggle.click(function(e){ e.preventDefault(); @@ -89,6 +92,25 @@ function projectTopBarInit(ctx) { }, null); }); + updateProjectBtn.click(function (e) { + e.preventDefault(); + + selectedTarget = { name: "_PROJECT_PREPARE_" }; + + /* Save current default build image, fire off the build */ + libtoaster.updateProject(null, selectedTarget.name, newBuildTargetInput.val().trim(), + function(){ + window.location.replace(libtoaster.ctx.projectSpecificPageUrl); + }, null); + }); + + cancelProjectBtn.click(function (e) { + e.preventDefault(); + + /* redirect to 'done/canceled' landing page */ + window.location.replace(libtoaster.ctx.landingSpecificCancelURL); + }); + /* Call makeProjectNameValidation function */ libtoaster.makeProjectNameValidation($("#project-name-change-input"), $("#hint-error-project-name"), $("#validate-project-name"), diff --git a/poky/bitbake/lib/toaster/toastergui/tables.py b/poky/bitbake/lib/toaster/toastergui/tables.py index dca2fa2913..9ff756bc8d 100644 --- a/poky/bitbake/lib/toaster/toastergui/tables.py +++ b/poky/bitbake/lib/toaster/toastergui/tables.py @@ -35,6 +35,8 @@ from toastergui.tablefilter import TableFilterActionToggle from toastergui.tablefilter import TableFilterActionDateRange from toastergui.tablefilter import TableFilterActionDay +import os + class ProjectFilters(object): @staticmethod def in_project(project_layers): @@ -339,6 +341,8 @@ class RecipesTable(ToasterTable): 'filter_name' : "in_current_project", 'static_data_name' : "add-del-layers", 'static_data_template' : '{% include "recipe_btn.html" %}'} + if '1' == os.environ.get('TOASTER_PROJECTSPECIFIC'): + build_col['static_data_template'] = '{% include "recipe_add_btn.html" %}' def get_context_data(self, **kwargs): project = Project.objects.get(pk=kwargs['pid']) @@ -1611,14 +1615,12 @@ class DistrosTable(ToasterTable): hidden=True, field_name="layer_version__get_vcs_reference") - wrtemplate_file_template = '''<code>conf/machine/{{data.name}}.conf</code> - <a href="{{data.get_vcs_machine_file_link_url}}" target="_blank"><span class="glyphicon glyphicon-new-window"></i></a>''' - + distro_file_template = '''<code>conf/distro/{{data.name}}.conf</code> + {% if 'None' not in data.get_vcs_distro_file_link_url %}<a href="{{data.get_vcs_distro_file_link_url}}" target="_blank"><span class="glyphicon glyphicon-new-window"></i></a>{% endif %}''' self.add_column(title="Distro file", hidden=True, static_data_name="templatefile", - static_data_template=wrtemplate_file_template) - + static_data_template=distro_file_template) self.add_column(title="Select", help_text="Sets the selected distro to the project", diff --git a/poky/bitbake/lib/toaster/toastergui/templates/base_specific.html b/poky/bitbake/lib/toaster/toastergui/templates/base_specific.html new file mode 100644 index 0000000000..e377cadd73 --- /dev/null +++ b/poky/bitbake/lib/toaster/toastergui/templates/base_specific.html @@ -0,0 +1,128 @@ +<!DOCTYPE html> +{% load static %} +{% load projecttags %} +{% load project_url_tag %} +<html lang="en"> + <head> + <title> + {% block title %} Toaster {% endblock %} + </title> + <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}" type="text/css"/> + <!--link rel="stylesheet" href="{% static 'css/bootstrap-theme.css' %}" type="text/css"/--> + <link rel="stylesheet" href="{% static 'css/font-awesome.min.css' %}" type='text/css'/> + <link rel="stylesheet" href="{% static 'css/default.css' %}" type='text/css'/> + + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" /> + <script src="{% static 'js/jquery-2.0.3.min.js' %}"> + </script> + <script src="{% static 'js/jquery.cookie.js' %}"> + </script> + <script src="{% static 'js/bootstrap.min.js' %}"> + </script> + <script src="{% static 'js/typeahead.jquery.js' %}"> + </script> + <script src="{% static 'js/jsrender.min.js' %}"> + </script> + <script src="{% static 'js/highlight.pack.js' %}"> + </script> + <script src="{% static 'js/libtoaster.js' %}"> + </script> + {% if DEBUG %} + <script> + libtoaster.debug = true; + </script> + {% endif %} + <script> + /* Set JsRender delimiters (mrb_section.html) different than Django's */ + $.views.settings.delimiters("<%", "%>"); + + /* This table allows Django substitutions to be passed to libtoaster.js */ + libtoaster.ctx = { + jsUrl : "{% static 'js/' %}", + htmlUrl : "{% static 'html/' %}", + projectsUrl : "{% url 'all-projects' %}", + projectsTypeAheadUrl: {% url 'xhr_projectstypeahead' as prjurl%}{{prjurl|json}}, + {% if project.id %} + landingSpecificURL : "{% url 'landing_specific' project.id %}", + landingSpecificCancelURL : "{% url 'landing_specific_cancel' project.id %}", + projectId : {{project.id}}, + projectPageUrl : {% url 'project' project.id as purl %}{{purl|json}}, + projectSpecificPageUrl : {% url 'project_specific' 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}}, + machinesTypeAheadUrl: {% url 'xhr_machinestypeahead' project.id as paturl%}{{paturl|json}}, + distrosTypeAheadUrl: {% url 'xhr_distrostypeahead' project.id as paturl%}{{paturl|json}}, + projectBuildsUrl: {% url 'projectbuilds' project.id as pburl %}{{pburl|json}}, + xhrCustomRecipeUrl : "{% url 'xhr_customrecipe' %}", + projectId : {{project.id}}, + xhrBuildRequestUrl: "{% url 'xhr_buildrequest' project.id %}", + mostRecentBuildsUrl: "{% url 'most_recent_builds' %}?project_id={{project.id}}", + xhrProjectUpdateUrl: "{% url 'xhr_projectupdate' project.id %}", + xhrProjectCancelUrl: "{% url 'landing_specific_cancel' project.id %}", + xhrSetDefaultImageUrl: "{% url 'xhr_setdefaultimage' project.id %}", + {% else %} + mostRecentBuildsUrl: "{% url 'most_recent_builds' %}", + projectId : undefined, + projectPageUrl : undefined, + projectName : undefined, + {% endif %} + }; + </script> + {% block extraheadcontent %} + {% endblock %} + </head> + + <body> + + {% csrf_token %} + <div id="loading-notification" class="alert alert-warning lead text-center" style="display:none"> + Loading <i class="fa-pulse icon-spinner"></i> + </div> + + <div id="change-notification" class="alert alert-info alert-dismissible change-notification" style="display:none"> + <button type="button" class="close" id="hide-alert" data-toggle="alert">×</button> + <span id="change-notification-msg"></span> + </div> + + <nav class="navbar navbar-default navbar-fixed-top"> + <div class="container-fluid"> + <div class="navbar-header"> + <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#global-nav" aria-expanded="false"> + <span class="sr-only">Toggle navigation</span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + </button> + <div class="toaster-navbar-brand"> + {% if project_specific %} + <img class="logo" src="{% static 'img/logo.png' %}" class="" alt="Yocto Project logo"/> + Toaster + {% else %} + <a href="/"> + </a> + <a href="/"> + <img class="logo" src="{% static 'img/logo.png' %}" class="" alt="Yocto Project logo"/> + </a> + <a class="brand" href="/">Toaster</a> + {% endif %} + {% if DEBUG %} + <span class="glyphicon glyphicon-info-sign" title="<strong>Toaster version information</strong>" data-content="<dl><dt>Git branch</dt><dd>{{TOASTER_BRANCH}}</dd><dt>Git revision</dt><dd>{{TOASTER_REVISION}}</dd></dl>"></i> + {% endif %} + </div> + </div> + <div class="collapse navbar-collapse" id="global-nav"> + <ul class="nav navbar-nav"> + <h3> Project Configuration Page </h3> + </div> + </div> + </nav> + + <div class="container-fluid"> + {% block pagecontent %} + {% endblock %} + </div> + </body> +</html> diff --git a/poky/bitbake/lib/toaster/toastergui/templates/baseprojectspecificpage.html b/poky/bitbake/lib/toaster/toastergui/templates/baseprojectspecificpage.html new file mode 100644 index 0000000000..d0b588de98 --- /dev/null +++ b/poky/bitbake/lib/toaster/toastergui/templates/baseprojectspecificpage.html @@ -0,0 +1,48 @@ +{% extends "base_specific.html" %} + +{% load projecttags %} +{% load humanize %} + +{% block title %} {{title}} - {{project.name}} - Toaster {% endblock %} + +{% block pagecontent %} + +<div class="row"> + {% include "project_specific_topbar.html" %} + <script type="text/javascript"> +$(document).ready(function(){ + $("#config-nav .nav li a").each(function(){ + if (window.location.pathname === $(this).attr('href')) + $(this).parent().addClass('active'); + else + $(this).parent().removeClass('active'); + }); + + $("#topbar-configuration-tab").addClass("active") + }); + </script> + + <!-- only on config pages --> + <div id="config-nav" class="col-md-2"> + <ul class="nav nav-pills nav-stacked"> + <li><a class="nav-parent" href="{% url 'project' project.id %}">Configuration</a></li> + <li class="nav-header">Compatible metadata</li> + <li><a href="{% url 'projectcustomimages' project.id %}">Custom images</a></li> + <li><a href="{% url 'projectimagerecipes' project.id %}">Image recipes</a></li> + <li><a href="{% url 'projectsoftwarerecipes' project.id %}">Software recipes</a></li> + <li><a href="{% url 'projectmachines' project.id %}">Machines</a></li> + <li><a href="{% url 'projectlayers' project.id %}">Layers</a></li> + <li><a href="{% url 'projectdistros' project.id %}">Distros</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> + </ul> + </div> + <div class="col-md-10"> + {% block projectinfomain %}{% endblock %} + </div> + +</div> +{% endblock %} + diff --git a/poky/bitbake/lib/toaster/toastergui/templates/customise_btn.html b/poky/bitbake/lib/toaster/toastergui/templates/customise_btn.html index 38c258ac32..ce462401c7 100644 --- a/poky/bitbake/lib/toaster/toastergui/templates/customise_btn.html +++ b/poky/bitbake/lib/toaster/toastergui/templates/customise_btn.html @@ -5,7 +5,11 @@ > Customise </button> -<button class="btn btn-default btn-block layer-add-{{data.layer_version.pk}} layerbtn" data-layer='{ "id": {{data.layer_version.pk}}, "name": "{{data.layer_version.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.layer_version.pk%}"}' data-directive="add" +<button class="btn btn-default btn-block layer-add-{{data.layer_version.pk}} layerbtn" + data-layer='{ "id": {{data.layer_version.pk}}, "name": "{{data.layer_version.layer.name}}", + "layerdetailurl": "{%url 'layerdetails' extra.pid data.layer_version.pk%}", + "xhrLayerUrl": "{% url "xhr_layer" extra.pid data.layer_version.pk %}"}' + data-directive="add" {% if data.layer_version.pk in extra.current_layers %} style="display:none;" {% endif %} diff --git a/poky/bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html b/poky/bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html index b3eabe1a26..99fbb38970 100644 --- a/poky/bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html +++ b/poky/bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html @@ -1,4 +1,4 @@ -{% extends "baseprojectpage.html" %} +{% extends project_specific|yesno:"baseprojectspecificpage.html,baseprojectpage.html" %} {% load projecttags %} {% load humanize %} {% load static %} diff --git a/poky/bitbake/lib/toaster/toastergui/templates/importlayer.html b/poky/bitbake/lib/toaster/toastergui/templates/importlayer.html index 97d52c76c1..e0c987eef1 100644 --- a/poky/bitbake/lib/toaster/toastergui/templates/importlayer.html +++ b/poky/bitbake/lib/toaster/toastergui/templates/importlayer.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends project_specific|yesno:"baseprojectspecificpage.html,base.html" %} {% load projecttags %} {% load humanize %} {% load static %} @@ -6,7 +6,7 @@ {% block pagecontent %} <div class="row"> - {% include "projecttopbar.html" %} + {% include project_specific|yesno:"project_specific_topbar.html,projecttopbar.html" %} {% if project and project.release %} <script src="{% static 'js/layerDepsModal.js' %}"></script> <script src="{% static 'js/importlayer.js' %}"></script> diff --git a/poky/bitbake/lib/toaster/toastergui/templates/landing_specific.html b/poky/bitbake/lib/toaster/toastergui/templates/landing_specific.html new file mode 100644 index 0000000000..e289c7d4a5 --- /dev/null +++ b/poky/bitbake/lib/toaster/toastergui/templates/landing_specific.html @@ -0,0 +1,50 @@ +{% extends "base_specific.html" %} + +{% load static %} +{% load projecttags %} +{% load humanize %} + +{% block title %} Welcome to Toaster {% endblock %} + +{% block pagecontent %} + + <div class="container"> + <div class="row"> + <!-- Empty - no build module --> + <div class="page-header top-air"> + <h1> + Configuration {% if status == "cancel" %}Canceled{% else %}Completed{% endif %}! You can now close this window. + </h1> + </div> + <div class="alert alert-info lead"> + <p> + Your project configuration {% if status == "cancel" %}changes have been canceled{% else %}has completed!{% endif %} + <br> + <br> + <ul> + <li> + The Toaster instance for project configuration has been shut down + </li> + <li> + You can start Toaster independently for advanced project management and analysis: + <pre><code> + Set up bitbake environment: + $ cd {{install_dir}} + $ . oe-init-build-env [toaster_server] + + Option 1: Start a local Toaster server, open local browser to "localhost:8000" + $ . toaster start webport=8000 + + Option 2: Start a shared Toaster server, open any browser to "[host_ip]:8000" + $ . toaster start webport=0.0.0.0:8000 + + To stop the Toaster server: + $ . toaster stop + </code></pre> + </li> + </ul> + </p> + </div> + </div> + +{% endblock %} diff --git a/poky/bitbake/lib/toaster/toastergui/templates/layerdetails.html b/poky/bitbake/lib/toaster/toastergui/templates/layerdetails.html index e0069db80c..1e26e31c8b 100644 --- a/poky/bitbake/lib/toaster/toastergui/templates/layerdetails.html +++ b/poky/bitbake/lib/toaster/toastergui/templates/layerdetails.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends project_specific|yesno:"baseprojectspecificpage.html,base.html" %} {% load projecttags %} {% load humanize %} {% load static %} @@ -310,6 +310,7 @@ {% endwith %} {% endwith %} </div> + </div> <!-- end tab content --> </div> <!-- end tabable --> diff --git a/poky/bitbake/lib/toaster/toastergui/templates/mrb_section.html b/poky/bitbake/lib/toaster/toastergui/templates/mrb_section.html index c5b9fe90d3..98d9fac822 100644 --- a/poky/bitbake/lib/toaster/toastergui/templates/mrb_section.html +++ b/poky/bitbake/lib/toaster/toastergui/templates/mrb_section.html @@ -119,7 +119,7 @@ title="Toaster is cloning the repos required for your build"> </span> - Cloning <span id="repos-cloned-percentage-<%:id%>"><%:repos_cloned_percentage%></span>% complete + Cloning <span id="repos-cloned-percentage-<%:id%>"><%:repos_cloned_percentage%></span>% complete <span id="repos-cloned-progressitem-<%:id%>">(<%:progress_item%>)</span> <%include tmpl='#cancel-template'/%> </div> diff --git a/poky/bitbake/lib/toaster/toastergui/templates/newcustomimage.html b/poky/bitbake/lib/toaster/toastergui/templates/newcustomimage.html index 980179a406..0766e5e4cf 100644 --- a/poky/bitbake/lib/toaster/toastergui/templates/newcustomimage.html +++ b/poky/bitbake/lib/toaster/toastergui/templates/newcustomimage.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends project_specific|yesno:"baseprojectspecificpage.html,base.html" %} {% load projecttags %} {% load humanize %} {% load static %} @@ -8,7 +8,7 @@ <div class="row"> - {% include "projecttopbar.html" %} + {% include project_specific|yesno:"project_specific_topbar.html,projecttopbar.html" %} <div class="col-md-12"> {% url table_name project.id as xhr_table_url %} diff --git a/poky/bitbake/lib/toaster/toastergui/templates/newproject.html b/poky/bitbake/lib/toaster/toastergui/templates/newproject.html index bd03bb55d7..7e1ebb382a 100644 --- a/poky/bitbake/lib/toaster/toastergui/templates/newproject.html +++ b/poky/bitbake/lib/toaster/toastergui/templates/newproject.html @@ -20,23 +20,19 @@ <input type="text" class="form-control" required id="new-project-name" name="projectname"> </div> <p class="help-block text-danger" style="display: none;" id="hint-error-project-name">A project with this name exists. Project names must be unique.</p> -<!-- - <fieldset> - <label class="project-form">Project type</label> - <label class="project-form radio"><input type="radio" name="ptype" value="analysis" checked/> Analysis Project</label> + <label class="project-form">Project type:</label> {% if releases.count > 0 %} - <label class="project-form radio"><input type="radio" name="ptype" value="build" checked /> Build Project</label> + <label class="project-form radio" style="padding-left: 35px;"><input id='type-new' type="radio" name="ptype" value="new"/> New project</label> {% endif %} - </fieldset> --> - <input type="hidden" name="ptype" value="build" /> + <label class="project-form radio" style="padding-left: 35px;"><input id='type-import' type="radio" name="ptype" value="import"/> Import command line project</label> {% if releases.count > 0 %} - <div class="release form-group"> + <div class="release form-group"> {% if releases.count > 1 %} <label class="control-label"> Release - <span class="glyphicon glyphicon-question-sign get-help" title="The version of the build system you want to use"></span> + <span class="glyphicon glyphicon-question-sign get-help" title="The version of the build system you want to use for this project"></span> </label> <select name="projectversion" id="projectversion" class="form-control"> {% for release in releases %} @@ -59,28 +55,26 @@ {% else %} <input type="hidden" name="projectversion" value="{{releases.0.id}}"/> {% endif %} - </div> + + <input type="checkbox" class="checkbox-mergeattr" name="mergeattr" value="mergeattr"> Merged Toaster settings (Command line user compatibility) + <span class="glyphicon glyphicon-question-sign get-help" title="Place the Toaster settings into the standard 'local.conf' and 'bblayers.conf' instead of 'toaster_bblayers.conf' and 'toaster.conf'"></span> + + </div> {% endif %} + + <div class="build-import form-group" id="import-project"> + <label class="control-label">Import existing project directory + <span class="glyphicon glyphicon-question-sign get-help" title="Enter a path to an existing build directory, import the existing settings, and create a Toaster Project for it."></span> + </label> + <input style="width: 33%;"type="text" class="form-control" required id="import-project-dir" name="importdir"> + </div> + <div class="top-air"> <input type="submit" id="create-project-button" class="btn btn-primary btn-lg" value="Create project"/> <span class="help-inline" style="vertical-align:middle;">To create a project, you need to enter a project name</span> </div> </form> - <!-- - <div class="col-md-5 well"> - <span class="help-block"> - <h4>Toaster project types</h4> - <p>With a <strong>build project</strong> you configure and run your builds from Toaster.</p> - <p>With an <strong>analysis project</strong>, the builds are configured and run by another tool - (something like Buildbot or Jenkins), and the project only collects the information about the - builds (packages, recipes, dependencies, logs, etc). </p> - <p>You can read more on <a href="#">how to set up an analysis project</a> - in the Toaster manual.</p> - <h4>Release</h4> - <p>If you create a <strong>build project</strong>, you will need to select a <strong>release</strong>, - which is the version of the build system you want to use to run your builds.</p> - </div> --> </div> </div> @@ -89,6 +83,7 @@ // hide the new project button $("#new-project-button").hide(); $('.btn-primary').attr('disabled', 'disabled'); + $('#type-new').attr('checked', 'checked'); // enable submit button when all required fields are populated $("input#new-project-name").on('input', function() { @@ -118,20 +113,24 @@ $(".btn-primary")); -/* // Hide the project release when you select an analysis project + // Hide the project release when you select an analysis project function projectType() { - if ($("input[type='radio']:checked").val() == 'build') { + if ($("input[type='radio']:checked").val() == 'new') { + $('.build-import').fadeOut(); $('.release').fadeIn(); + $('#import-project-dir').removeAttr('required'); } else { $('.release').fadeOut(); + $('.build-import').fadeIn(); + $('#import-project-dir').attr('required', 'required'); } } projectType(); $('input:radio').change(function(){ projectType(); - }); */ + }); }); </script> diff --git a/poky/bitbake/lib/toaster/toastergui/templates/newproject_specific.html b/poky/bitbake/lib/toaster/toastergui/templates/newproject_specific.html new file mode 100644 index 0000000000..cfa77f2e40 --- /dev/null +++ b/poky/bitbake/lib/toaster/toastergui/templates/newproject_specific.html @@ -0,0 +1,95 @@ +{% extends "base.html" %} +{% load projecttags %} +{% load humanize %} + +{% block title %} Create a new project - Toaster {% endblock %} + +{% block pagecontent %} +<div class="row"> + <div class="col-md-12"> + <div class="page-header"> + <h1>Create a new project</h1> + </div> + {% if alert %} + <div class="alert alert-danger" role="alert">{{alert}}</div> + {% endif %} + + <form method="POST" action="{%url "newproject_specific" project_pk %}">{% csrf_token %} + <div class="form-group" id="validate-project-name"> + <label class="control-label">Project name <span class="text-muted">(required)</span></label> + <input type="text" class="form-control" required id="new-project-name" name="display_projectname" value="{{projectname}}" disabled> + </div> + <p class="help-block text-danger" style="display: none;" id="hint-error-project-name">A project with this name exists. Project names must be unique.</p> + <input type="hidden" name="ptype" value="build" /> + <input type="hidden" name="projectname" value="{{projectname}}" /> + + {% if releases.count > 0 %} + <div class="release form-group"> + {% if releases.count > 1 %} + <label class="control-label"> + Release + <span class="glyphicon glyphicon-question-sign get-help" title="The version of the build system you want to use"></span> + </label> + <select name="projectversion" id="projectversion" class="form-control"> + {% for release in releases %} + <option value="{{release.id}}" + {%if defaultbranch == release.name %} + selected + {%endif%} + >{{release.description}}</option> + {% endfor %} + </select> + <div class="row"> + <div class="col-md-4"> + {% for release in releases %} + <div class="helptext" id="description-{{release.id}}" style="display: none"> + <span class="help-block">{{release.helptext|safe}}</span> + </div> + {% endfor %} + {% else %} + <input type="hidden" name="projectversion" value="{{releases.0.id}}"/> + {% endif %} + </div> + </div> + </fieldset> + {% endif %} + <div class="top-air"> + <input type="submit" id="create-project-button" class="btn btn-primary btn-lg" value="Create project"/> + <span class="help-inline" style="vertical-align:middle;">To create a project, you need to specify the release</span> + </div> + + </form> + </div> + </div> + + <script type="text/javascript"> + $(document).ready(function () { + // hide the new project button, name is preset + $("#new-project-button").hide(); + + // enable submit button when all required fields are populated + $("input#new-project-name").on('input', function() { + if ($("input#new-project-name").val().length > 0 ){ + $('.btn-primary').removeAttr('disabled'); + $(".help-inline").css('visibility','hidden'); + } + else { + $('.btn-primary').attr('disabled', 'disabled'); + $(".help-inline").css('visibility','visible'); + } + }); + + // show relevant help text for the selected release + var selected_release = $('select').val(); + $("#description-" + selected_release).show(); + + $('select').change(function(){ + var new_release = $('select').val(); + $(".helptext").hide(); + $('#description-' + new_release).fadeIn(); + }); + + }); + </script> + +{% endblock %} diff --git a/poky/bitbake/lib/toaster/toastergui/templates/project.html b/poky/bitbake/lib/toaster/toastergui/templates/project.html index 11603d1e12..fa41e3c909 100644 --- a/poky/bitbake/lib/toaster/toastergui/templates/project.html +++ b/poky/bitbake/lib/toaster/toastergui/templates/project.html @@ -1,4 +1,4 @@ -{% extends "baseprojectpage.html" %} +{% extends project_specific|yesno:"baseprojectspecificpage.html,baseprojectpage.html" %} {% load projecttags %} {% load humanize %} @@ -18,7 +18,7 @@ try { projectPageInit(ctx); } catch (e) { - document.write("Sorry, An error has occurred loading this page"); + document.write("Sorry, An error has occurred loading this page (project):"+e); console.warn(e); } }); @@ -93,6 +93,7 @@ </form> </div> + {% if not project_specific %} <div class="well well-transparent"> <h3>Most built recipes</h3> @@ -105,6 +106,7 @@ </ul> <button class="btn btn-primary" id="freq-build-btn" disabled="disabled">Build selected recipes</button> </div> + {% endif %} <div class="well well-transparent"> <h3>Project release</h3> @@ -157,5 +159,6 @@ <ul class="list-unstyled lead" id="layers-in-project-list"> </ul> </div> + </div> {% endblock %} diff --git a/poky/bitbake/lib/toaster/toastergui/templates/project_specific.html b/poky/bitbake/lib/toaster/toastergui/templates/project_specific.html new file mode 100644 index 0000000000..f625d18baf --- /dev/null +++ b/poky/bitbake/lib/toaster/toastergui/templates/project_specific.html @@ -0,0 +1,162 @@ +{% extends "baseprojectspecificpage.html" %} + +{% load projecttags %} +{% load humanize %} +{% load static %} + +{% block title %} Configuration - {{project.name}} - Toaster {% endblock %} +{% block projectinfomain %} + +<script src="{% static 'js/layerDepsModal.js' %}"></script> +<script src="{% static 'js/projectpage.js' %}"></script> +<script> + $(document).ready(function (){ + var ctx = { + testReleaseChangeUrl: "{% url 'xhr_testreleasechange' project.id %}", + }; + + try { + projectPageInit(ctx); + } catch (e) { + document.write("Sorry, An error has occurred loading this page"); + console.warn(e); + } + }); +</script> + +<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"> + <h4>Are you sure you want to delete this project?</h4> + </div> + <div class="modal-body"> + <p>Deleting the <strong class="project-name"></strong> project + will:</p> + <ul> + <li>Cancel its builds currently in progress</li> + <li>Remove its configuration information</li> + <li>Remove its imported layers</li> + <li>Remove its custom images</li> + <li>Remove all its build information</li> + </ul> + </div> + <div class="modal-footer"> + <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> + + +<div class="row" id="project-page" style="display:none"> + <div class="col-md-6"> + <div class="well well-transparent" id="machine-section"> + <h3>Machine</h3> + + <p class="lead"><span id="project-machine-name"></span> <span class="glyphicon glyphicon-edit" id="change-machine-toggle"></span></p> + + <form id="select-machine-form" style="display:none;" class="form-inline"> + <span class="help-block">Machine suggestions come from the list of layers added to your project. If you don't see the machine you are looking for, <a href="{% url 'projectmachines' project.id %}">check the full list of machines</a></span> + <div class="form-group" id="machine-input-form"> + <input class="form-control" id="machine-change-input" autocomplete="off" value="" data-provide="typeahead" data-minlength="1" data-autocomplete="off" type="text"> + </div> + <button id="machine-change-btn" class="btn btn-default" type="button">Save</button> + <a href="#" id="cancel-machine-change" class="btn btn-link">Cancel</a> + <span class="help-block text-danger" id="invalid-machine-name-help" style="display:none">A valid machine name cannot include spaces.</span> + <p class="form-link"><a href="{% url 'projectmachines' project.id %}">View compatible machines</a></p> + </form> + </div> + + <div class="well well-transparent" id="distro-section"> + <h3>Distro</h3> + + <p class="lead"><span id="project-distro-name"></span> <span class="glyphicon glyphicon-edit" id="change-distro-toggle"></span></p> + + <form id="select-distro-form" style="display:none;" class="form-inline"> + <span class="help-block">Distro suggestions come from the Layer Index</a></span> + <div class="form-group"> + <input class="form-control" id="distro-change-input" autocomplete="off" value="" data-provide="typeahead" data-minlength="1" data-autocomplete="off" type="text"> + </div> + <button id="distro-change-btn" class="btn btn-default" type="button">Save</button> + <a href="#" id="cancel-distro-change" class="btn btn-link">Cancel</a> + <p class="form-link"><a href="{% url 'projectdistros' project.id %}">View compatible distros</a></p> + </form> + </div> + + <div class="well well-transparent"> + <h3>Most built recipes</h3> + + <div class="alert alert-info" style="display:none" id="no-most-built"> + <h4>You haven't built any recipes yet</h4> + <p class="form-link"><a href="{% url 'projectimagerecipes' project.id %}">Choose a recipe to build</a></p> + </div> + + <ul class="list-unstyled lead" id="freq-build-list"> + </ul> + <button class="btn btn-primary" id="freq-build-btn" disabled="disabled">Build selected recipes</button> + </div> + + <div class="well well-transparent"> + <h3>Project release</h3> + + <p class="lead"><span id="project-release-title"></span> + + <!-- Comment out the ability to change the project release, until we decide what to do with this functionality --> + + <!--i title="" data-original-title="" id="release-change-toggle" class="icon-pencil"></i--> + </p> + + <!-- Comment out the ability to change the project release, until we decide what to do with this functionality --> + + <!--form class="form-inline" id="change-release-form" style="display:none;"> + <select></select> + <button class="btn" style="margin-left:5px;" id="change-release-btn">Change</button> <a href="#" id="cancel-release-change" class="btn btn-link">Cancel</a> + </form--> + </div> + </div> + + <div class="col-md-6"> + <div class="well well-transparent" id="layer-container"> + <h3>Layers <span class="counter">(<span id="project-layers-count"></span>)</span> + <span title="OpenEmbedded organises recipes and machines into thematic groups called <strong>layers</strong>. Click on a layer name to see the recipes and machines it includes." class="glyphicon glyphicon-question-sign get-help"></span> + </h3> + + <div class="alert alert-warning" id="no-layers-in-project" style="display:none"> + <h4>This project has no layers</h4> + In order to build this project you need to add some layers first. For that you can: + <ul> + <li><a href="{% url 'projectlayers' project.id %}">Choose from the layers compatible with this project</a></li> + <li><a href="{% url 'importlayer' project.id %}">Import a layer</a></li> + <li><a href="http://www.yoctoproject.org/docs/current/dev-manual/dev-manual.html#understanding-and-creating-layers" target="_blank">Read about layers in the documentation</a></li> + <li>Or type a layer name below</li> + </ul> + </div> + + <form class="form-inline"> + <div class="form-group"> + <input id="layer-add-input" class="form-control" autocomplete="off" placeholder="Type a layer name" data-minlength="1" data-autocomplete="off" data-provide="typeahead" data-source="" type="text"> + </div> + <button id="add-layer-btn" class="btn btn-default" disabled>Add layer</button> + <p class="form-link"> + <a href="{% url 'projectlayers' project.id %}" id="view-compatible-layers">View compatible layers</a> + <span class="text-muted">|</span> + <a href="{% url 'importlayer' project.id %}">Import layer</a> + </p> + </form> + + <ul class="list-unstyled lead" id="layers-in-project-list"> + </ul> + </div> + +</div> +{% endblock %} diff --git a/poky/bitbake/lib/toaster/toastergui/templates/project_specific_topbar.html b/poky/bitbake/lib/toaster/toastergui/templates/project_specific_topbar.html new file mode 100644 index 0000000000..622787c4bc --- /dev/null +++ b/poky/bitbake/lib/toaster/toastergui/templates/project_specific_topbar.html @@ -0,0 +1,80 @@ +{% load static %} +<script src="{% static 'js/projecttopbar.js' %}"></script> +<script> + $(document).ready(function () { + var ctx = { + numProjectLayers : {{project.get_project_layer_versions.count}}, + machine : "{{project.get_current_machine_name|default_if_none:""}}", + } + + try { + projectTopBarInit(ctx); + } catch (e) { + document.write("Sorry, An error has occurred loading this page (pstb):"+e); + console.warn(e); + } + }); +</script> + +<div class="col-md-12"> + <div class="alert alert-success alert-dismissible change-notification" id="project-created-notification" style="display:none"> + <button type="button" class="close" data-dismiss="alert">×</button> + <p>Your project <strong>{{project.name}}</strong> has been created. You can now <a class="alert-link" href="{% url 'projectmachines' project.id %}">select your target machine</a> and <a class="alert-link" href="{% url 'projectimagerecipes' project.id %}">choose image recipes</a> to build.</p> + </div> + <!-- project name --> + <div class="page-header"> + <h1 id="project-name-container"> + <span class="project-name">{{project.name}}</span> + {% if project.is_default %} + <span class="glyphicon glyphicon-question-sign get-help" title="This project shows information about the builds you start from the command line while Toaster is running"></span> + {% endif %} + </h1> + <form id="project-name-change-form" class="form-inline" style="display: none;"> + <div class="form-group"> + <input class="form-control input-lg" type="text" id="project-name-change-input" autocomplete="off" value="{{project.name}}"> + </div> + <button id="project-name-change-btn" class="btn btn-default btn-lg" type="button">Save</button> + <a href="#" id="project-name-change-cancel" class="btn btn-lg btn-link">Cancel</a> + </form> + </div> + + {% with mrb_type='project' %} + {% include "mrb_section.html" %} + {% endwith %} + + {% if not project.is_default %} + <div id="project-topbar"> + <ul class="nav nav-tabs"> + <li id="topbar-configuration-tab"> + <a href="{% url 'project_specific' project.id %}"> + Configuration + </a> + </li> + <li> + <a href="{% url 'importlayer' project.id %}"> + Import layer + </a> + </li> + <li> + <a href="{% url 'newcustomimage' project.id %}"> + New custom image + </a> + </li> + <li class="pull-right"> + <form class="form-inline"> + <div class="form-group"> + <span class="glyphicon glyphicon-question-sign get-help" data-placement="left" title="Type the name of one or more recipes you want to build, separated by a space. You can also specify a task by appending a colon and a task name to the recipe name, like so: <code>busybox:clean</code>"></span> + <input id="build-input" type="text" class="form-control input-lg" placeholder="Select the default image recipe" autocomplete="off" disabled value="{{project.get_default_image}}"> + </div> + {% if project.get_is_new %} + <button id="update-project-button" class="btn btn-primary btn-lg" data-project-id="{{project.id}}">Prepare Project</button> + {% else %} + <button id="cancel-project-button" class="btn info btn-lg" data-project-id="{{project.id}}">Cancel</button> + <button id="update-project-button" class="btn btn-primary btn-lg" data-project-id="{{project.id}}">Update</button> + {% endif %} + </form> + </li> + </ul> + </div> + {% endif %} +</div> diff --git a/poky/bitbake/lib/toaster/toastergui/templates/projectconf.html b/poky/bitbake/lib/toaster/toastergui/templates/projectconf.html index 933c588f34..fb20b26f22 100644 --- a/poky/bitbake/lib/toaster/toastergui/templates/projectconf.html +++ b/poky/bitbake/lib/toaster/toastergui/templates/projectconf.html @@ -1,4 +1,4 @@ -{% extends "baseprojectpage.html" %} +{% extends project_specific|yesno:"baseprojectspecificpage.html,baseprojectpage.html" %} {% load projecttags %} {% load humanize %} @@ -438,8 +438,11 @@ function onEditPageUpdate(data) { var_context='m'; } } + if (configvars_sorted[i][0].startsWith("INTERNAL_")) { + var_context='m'; + } if (var_context == undefined) { - orightml += '<dt><span id="config_var_entry_'+configvars_sorted[i][2]+'" class="js-config-var-name"></span><span class="glyphicon glyphicon-trash js-icon-trash-config_var" id="config_var_trash_'+configvars_sorted[i][2]+'" x-data="'+configvars_sorted[i][2]+'"></span> </dt>' + orightml += '<dt><span id="config_var_entry_'+configvars_sorted[i][2]+'" class="js-config-var-name"></span><span class="glyphicon glyphicon-trash js-icon-trash-config_var" id="config_var_trash_'+configvars_sorted[i][2]+'" x-data="'+configvars_sorted[i][2]+'"></span> </dt>' orightml += '<dd class="variable-list">' orightml += ' <span class="lead" id="config_var_value_'+configvars_sorted[i][2]+'"></span>' orightml += ' <span class="glyphicon glyphicon-edit js-icon-pencil-config_var" x-data="'+configvars_sorted[i][2]+'"></span>' diff --git a/poky/bitbake/lib/toaster/toastergui/templates/recipe_add_btn.html b/poky/bitbake/lib/toaster/toastergui/templates/recipe_add_btn.html new file mode 100644 index 0000000000..06c464561e --- /dev/null +++ b/poky/bitbake/lib/toaster/toastergui/templates/recipe_add_btn.html @@ -0,0 +1,23 @@ +<a data-recipe-name="{{data.name}}" class="btn btn-default btn-block layer-exists-{{data.layer_version.pk}} set-default-recipe-btn" style="margin-top: 5px; + {% if data.layer_version.pk not in extra.current_layers %} + display:none; + {% endif %}" + > + Set recipe +</a> +<a class="btn btn-default btn-block layerbtn layer-add-{{data.layer_version.pk}}" + data-layer='{ + "id": {{data.layer_version.pk}}, + "name": "{{data.layer_version.layer.name}}", + "layerdetailurl": "{%url "layerdetails" extra.pid data.layer_version.pk%}", + "xhrLayerUrl": "{% url "xhr_layer" extra.pid data.layer_version.pk %}" + }' data-directive="add" + {% if data.layer_version.pk in extra.current_layers %} + style="display:none;" + {% endif %} +> + <span class="glyphicon glyphicon-plus"></span> + Add layer + <span class="glyphicon glyphicon-question-sign get-help" title="To set this + recipe you must first add the {{data.layer_version.layer.name}} layer to your project"></i> +</a> diff --git a/poky/bitbake/lib/toaster/toastergui/urls.py b/poky/bitbake/lib/toaster/toastergui/urls.py index e07b0efc1f..dc03e30356 100644 --- a/poky/bitbake/lib/toaster/toastergui/urls.py +++ b/poky/bitbake/lib/toaster/toastergui/urls.py @@ -116,6 +116,11 @@ urlpatterns = [ tables.ProjectBuildsTable.as_view(template_name="projectbuilds-toastertable.html"), name='projectbuilds'), + url(r'^newproject_specific/(?P<pid>\d+)/$', views.newproject_specific, name='newproject_specific'), + url(r'^project_specific/(?P<pid>\d+)/$', views.project_specific, name='project_specific'), + url(r'^landing_specific/(?P<pid>\d+)/$', views.landing_specific, name='landing_specific'), + url(r'^landing_specific_cancel/(?P<pid>\d+)/$', views.landing_specific_cancel, name='landing_specific_cancel'), + # the import layer is a project-specific functionality; url(r'^project/(?P<pid>\d+)/importlayer$', views.importlayer, name='importlayer'), @@ -233,6 +238,14 @@ urlpatterns = [ api.XhrBuildRequest.as_view(), name='xhr_buildrequest'), + url(r'^xhr_projectupdate/project/(?P<pid>\d+)$', + api.XhrProjectUpdate.as_view(), + name='xhr_projectupdate'), + + url(r'^xhr_setdefaultimage/project/(?P<pid>\d+)$', + api.XhrSetDefaultImageUrl.as_view(), + name='xhr_setdefaultimage'), + url(r'xhr_project/(?P<project_id>\d+)$', api.XhrProject.as_view(), name='xhr_project'), diff --git a/poky/bitbake/lib/toaster/toastergui/views.py b/poky/bitbake/lib/toaster/toastergui/views.py index 34ed2b2e3c..c712b06a6e 100755..100644 --- a/poky/bitbake/lib/toaster/toastergui/views.py +++ b/poky/bitbake/lib/toaster/toastergui/views.py @@ -25,6 +25,7 @@ import re from django.db.models import F, Q, Sum from django.db import IntegrityError from django.shortcuts import render, redirect, get_object_or_404 +from django.utils.http import urlencode from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe from orm.models import LogMessage, Variable, Package_Dependency, Package from orm.models import Task_Dependency, Package_File @@ -51,6 +52,7 @@ logger = logging.getLogger("toaster") # Project creation and managed build enable project_enable = ('1' == os.environ.get('TOASTER_BUILDSERVER')) +is_project_specific = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC')) class MimeTypeFinder(object): # setting this to False enables additional non-standard mimetypes @@ -70,6 +72,7 @@ class MimeTypeFinder(object): # single point to add global values into the context before rendering def toaster_render(request, page, context): context['project_enable'] = project_enable + context['project_specific'] = is_project_specific return render(request, page, context) @@ -1395,6 +1398,86 @@ if True: mandatory_fields = ['projectname', 'ptype'] try: ptype = request.POST.get('ptype') + if ptype == "import": + mandatory_fields.append('importdir') + else: + mandatory_fields.append('projectversion') + # make sure we have values for all mandatory_fields + missing = [field for field in mandatory_fields if len(request.POST.get(field, '')) == 0] + if missing: + # set alert for missing fields + raise BadParameterException("Fields missing: %s" % ", ".join(missing)) + + if not request.user.is_authenticated(): + user = authenticate(username = request.POST.get('username', '_anonuser'), password = 'nopass') + if user is None: + user = User.objects.create_user(username = request.POST.get('username', '_anonuser'), email = request.POST.get('email', ''), password = "nopass") + + user = authenticate(username = user.username, password = 'nopass') + login(request, user) + + # save the project + if ptype == "import": + if not os.path.isdir('%s/conf' % request.POST['importdir']): + raise BadParameterException("Bad path or missing 'conf' directory (%s)" % request.POST['importdir']) + from django.core import management + management.call_command('buildimport', '--command=import', '--name=%s' % request.POST['projectname'], '--path=%s' % request.POST['importdir'], interactive=False) + prj = Project.objects.get(name = request.POST['projectname']) + prj.merged_attr = True + prj.save() + else: + release = Release.objects.get(pk = request.POST.get('projectversion', None )) + prj = Project.objects.create_project(name = request.POST['projectname'], release = release) + prj.user_id = request.user.pk + if 'mergeattr' == request.POST.get('mergeattr', ''): + prj.merged_attr = True + prj.save() + + return redirect(reverse(project, args=(prj.pk,)) + "?notify=new-project") + + except (IntegrityError, BadParameterException) as e: + # fill in page with previously submitted values + for field in mandatory_fields: + context.__setitem__(field, request.POST.get(field, "-- missing")) + if isinstance(e, IntegrityError) and "username" in str(e): + context['alert'] = "Your chosen username is already used" + else: + context['alert'] = str(e) + return toaster_render(request, template, context) + + raise Exception("Invalid HTTP method for this page") + + # new project + def newproject_specific(request, pid): + if not project_enable: + return redirect( landing ) + + project = Project.objects.get(pk=pid) + template = "newproject_specific.html" + context = { + 'email': request.user.email if request.user.is_authenticated() else '', + 'username': request.user.username if request.user.is_authenticated() else '', + 'releases': Release.objects.order_by("description"), + 'projectname': project.name, + 'project_pk': project.pk, + } + + # WORKAROUND: if we already know release, redirect 'newproject_specific' to 'project_specific' + if '1' == project.get_variable('INTERNAL_PROJECT_SPECIFIC_SKIPRELEASE'): + return redirect(reverse(project_specific, args=(project.pk,))) + + try: + context['defaultbranch'] = ToasterSetting.objects.get(name = "DEFAULT_RELEASE").value + except ToasterSetting.DoesNotExist: + pass + + if request.method == "GET": + # render new project page + return toaster_render(request, template, context) + elif request.method == "POST": + mandatory_fields = ['projectname', 'ptype'] + try: + ptype = request.POST.get('ptype') if ptype == "build": mandatory_fields.append('projectversion') # make sure we have values for all mandatory_fields @@ -1417,10 +1500,10 @@ if True: else: release = Release.objects.get(pk = request.POST.get('projectversion', None )) - prj = Project.objects.create_project(name = request.POST['projectname'], release = release) + prj = Project.objects.create_project(name = request.POST['projectname'], release = release, existing_project = project) prj.user_id = request.user.pk prj.save() - return redirect(reverse(project, args=(prj.pk,)) + "?notify=new-project") + return redirect(reverse(project_specific, args=(prj.pk,)) + "?notify=new-project") except (IntegrityError, BadParameterException) as e: # fill in page with previously submitted values @@ -1437,9 +1520,87 @@ if True: # Shows the edit project page def project(request, pid): project = Project.objects.get(pk=pid) + + if '1' == os.environ.get('TOASTER_PROJECTSPECIFIC'): + if request.GET: + #Example:request.GET=<QueryDict: {'setMachine': ['qemuarm']}> + params = urlencode(request.GET).replace('%5B%27','').replace('%27%5D','') + return redirect("%s?%s" % (reverse(project_specific, args=(project.pk,)),params)) + else: + return redirect(reverse(project_specific, args=(project.pk,))) context = {"project": project} return toaster_render(request, "project.html", context) + # Shows the edit project-specific page + def project_specific(request, pid): + project = Project.objects.get(pk=pid) + + # Are we refreshing from a successful project specific update clone? + if Project.PROJECT_SPECIFIC_CLONING_SUCCESS == project.get_variable(Project.PROJECT_SPECIFIC_STATUS): + return redirect(reverse(landing_specific,args=(project.pk,))) + + context = { + "project": project, + "is_new" : project.get_variable(Project.PROJECT_SPECIFIC_ISNEW), + "default_image_recipe" : project.get_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE), + "mru" : Build.objects.all().filter(project=project,outcome=Build.IN_PROGRESS), + } + if project.build_set.filter(outcome=Build.IN_PROGRESS).count() > 0: + context['build_in_progress_none_completed'] = True + else: + context['build_in_progress_none_completed'] = False + return toaster_render(request, "project.html", context) + + # perform the final actions for the project specific page + def project_specific_finalize(cmnd, pid): + project = Project.objects.get(pk=pid) + callback = project.get_variable(Project.PROJECT_SPECIFIC_CALLBACK) + if "update" == cmnd: + # Delete all '_PROJECT_PREPARE_' builds + for b in Build.objects.all().filter(project=project): + delete_build = False + for t in b.target_set.all(): + if '_PROJECT_PREPARE_' == t.target: + delete_build = True + if delete_build: + from django.core import management + management.call_command('builddelete', str(b.id), interactive=False) + # perform callback at this last moment if defined, in case Toaster gets shutdown next + default_target = project.get_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE) + if callback: + callback = callback.replace("<IMAGE>",default_target) + if "cancel" == cmnd: + if callback: + callback = callback.replace("<IMAGE>","none") + callback = callback.replace("--update","--cancel") + # perform callback at this last moment if defined, in case this Toaster gets shutdown next + ret = '' + if callback: + ret = os.system('bash -c "%s"' % callback) + project.set_variable(Project.PROJECT_SPECIFIC_CALLBACK,'') + # Delete the temp project specific variables + project.set_variable(Project.PROJECT_SPECIFIC_ISNEW,'') + project.set_variable(Project.PROJECT_SPECIFIC_STATUS,Project.PROJECT_SPECIFIC_NONE) + # WORKAROUND: Release this workaround flag + project.set_variable('INTERNAL_PROJECT_SPECIFIC_SKIPRELEASE','') + + # Shows the final landing page for project specific update + def landing_specific(request, pid): + project_specific_finalize("update", pid) + context = { + "install_dir": os.environ['TOASTER_DIR'], + } + return toaster_render(request, "landing_specific.html", context) + + # Shows the related landing-specific page + def landing_specific_cancel(request, pid): + project_specific_finalize("cancel", pid) + context = { + "install_dir": os.environ['TOASTER_DIR'], + "status": "cancel", + } + return toaster_render(request, "landing_specific.html", context) + def jsunittests(request): """ Provides a page for the js unit tests """ bbv = BitbakeVersion.objects.filter(branch="master").first() diff --git a/poky/bitbake/lib/toaster/toastergui/widgets.py b/poky/bitbake/lib/toaster/toastergui/widgets.py index feef7c5d95..db5c3aa00b 100644 --- a/poky/bitbake/lib/toaster/toastergui/widgets.py +++ b/poky/bitbake/lib/toaster/toastergui/widgets.py @@ -89,6 +89,10 @@ class ToasterTable(TemplateView): # global variables context['project_enable'] = ('1' == os.environ.get('TOASTER_BUILDSERVER')) + try: + context['project_specific'] = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC')) + except: + context['project_specific'] = '' return context @@ -524,6 +528,8 @@ class MostRecentBuildsView(View): else: build['repos_cloned_percentage'] = 0 + build['progress_item'] = build_obj.progress_item + tasks_complete_percentage = 0 if build_obj.outcome in (Build.SUCCEEDED, Build.FAILED): tasks_complete_percentage = 100 |