[IMP] runbot: use custom layout and improve views

Runbot layout modifies the website/portal base layout to remove navbar,
footer, overides some custom styles. A lot of assets are loaded but not
used. The only real usefull elements are base assets (bootstrap, ...)
and the login button.

Migrating to the next version of odoo is usually painfull because some
xpath may break, extra element added, or some style change may break the
page, needing to add more and more xpath, css rules, ... for very little
benefits.

This cleanup creates a custom base layout for runbot independant from
base odoo templates.

Also add a breadcrumb, navigation arrow, and improve batch links
This commit is contained in:
Xavier-Do 2021-12-10 16:09:57 +01:00 committed by xdo
parent 460e26bafc
commit 9a3c11b09f
23 changed files with 751 additions and 651 deletions

View File

@ -22,7 +22,7 @@
'security/ir.model.access.csv',
'security/ir.rule.csv',
'templates/assets.xml',
'templates/utils.xml',
'templates/badge.xml',
'templates/batch.xml',
'templates/branch.xml',
@ -34,7 +34,6 @@
'templates/frontend.xml',
'templates/git.xml',
'templates/nginx.xml',
'templates/utils.xml',
'templates/build_error.xml',
'views/branch_views.xml',

View File

@ -53,6 +53,7 @@ def route(routes, **kw):
response.qcontext['current_path'] = request.httprequest.full_path
response.qcontext['refresh'] = refresh
response.qcontext['filter_mode'] = filter_mode
response.qcontext['default_category'] = request.env['ir.model.data'].xmlid_to_res_id('runbot.default_category')
response.qcontext['qu'] = QueryURL('/runbot/%s' % (slug(project)), path_args=['search'], search=search, refresh=refresh)
if 'title' not in response.qcontext:
response.qcontext['title'] = 'Runbot %s' % project.name or ''
@ -271,23 +272,35 @@ class Runbot(Controller):
elif operation == 'wakeup':
build._wake_up()
return werkzeug.utils.redirect(build.build_url)
return str(build.id)
@route(['/runbot/build/<int:build_id>'], type='http', auth="public", website=True, sitemap=False)
def build(self, build_id, search=None, **post):
@route([
'/runbot/build/<int:build_id>',
'/runbot/batch/<int:from_batch>/build/<int:build_id>'
], type='http', auth="public", website=True, sitemap=False)
def build(self, build_id, search=None, from_batch=None, **post):
"""Events/Logs"""
Build = request.env['runbot.build']
if from_batch:
from_batch = request.env['runbot.batch'].browse(int(from_batch))
from_batch = from_batch.with_context(batch=from_batch)
Build = request.env['runbot.build'].with_context(batch=from_batch)
build = Build.browse([build_id])[0]
if not build.exists():
return request.not_found()
siblings = (build.parent_id.children_ids if build.parent_id else from_batch.slot_ids.build_id if from_batch else build).sorted('id')
context = {
'build': build,
'default_category': request.env['ir.model.data'].xmlid_to_res_id('runbot.default_category'),
'from_batch': from_batch,
'project': build.params_id.trigger_id.project_id,
'title': 'Build %s' % build.id
'title': 'Build %s' % build.id,
'siblings': siblings,
# following logic is not the most efficient but good enough
'prev_ko': next((b for b in reversed(siblings) if b.id < build.id and b.global_result != 'ok'), Build),
'prev_bu': next((b for b in reversed(siblings) if b.id < build.id), Build),
'next_bu': next((b for b in siblings if b.id > build.id), Build),
'next_ko': next((b for b in siblings if b.id > build.id and b.global_result != 'ok'), Build),
}
return request.render("runbot.build", context)
@ -426,7 +439,6 @@ class Runbot(Controller):
context = {
'build': build,
'build_stats': build_stats,
'default_category': request.env['ir.model.data'].xmlid_to_res_id('runbot.default_category'),
'project': build.params_id.trigger_id.project_id,
'title': 'Build %s statistics' % build.id
}

View File

@ -16,16 +16,18 @@
<field name="name">R&amp;D</field>
</record>
<record model="runbot.bundle" id="runbot.bundle_master">
<field name="name">master</field>
<field name="is_base">True</field>
<field name="project_id" ref="runbot.main_project"/>
</record>
<record model="runbot.bundle" id="runbot.bundle_dummy">
<field name="name">Dummy</field>
<field name="no_build">True</field>
<field name="project_id" ref="runbot.main_project"/>
</record>
<data noupdate="1">
<record model="runbot.bundle" id="runbot.bundle_master" >
<field name="name">master</field>
<field name="is_base">True</field>
<field name="project_id" ref="runbot.main_project"/>
</record>
<record model="runbot.bundle" id="runbot.bundle_dummy">
<field name="name">Dummy</field>
<field name="no_build">True</field>
<field name="project_id" ref="runbot.main_project"/>
</record>
</data>
<record model="ir.config_parameter" id="runbot.runbot_is_base_regex">
<field name="key">runbot.runbot_is_base_regex</field>

View File

@ -51,8 +51,7 @@ class Batch(models.Model):
def _url(self):
self.ensure_one()
runbot_domain = self.env['runbot.runbot']._domain()
return "http://%s/runbot/batch/%s" % (runbot_domain, self.id)
return "/runbot/batch/%s" % self.id
def _new_commit(self, branch, match_type='new'):
# if not the same hash for repo:

View File

@ -142,6 +142,7 @@ class BuildResult(models.Model):
description = fields.Char('Description', help='Informative description')
md_description = fields.Char(compute='_compute_md_description', String='MD Parsed Description', help='Informative description markdown parsed')
display_name = fields.Char(compute='_compute_display_name')
# Related fields for convenience
version_id = fields.Many2one('runbot.version', related='params_id.version_id', store=True, index=True)
@ -201,6 +202,7 @@ class BuildResult(models.Model):
parent_id = fields.Many2one('runbot.build', 'Parent Build', index=True)
parent_path = fields.Char('Parent path', index=True)
top_parent = fields.Many2one('runbot.build', compute='_compute_top_parent')
ancestors = fields.Many2many('runbot.build', compute='_compute_ancestors')
# should we add a has children stored boolean?
children_ids = fields.One2many('runbot.build', 'parent_id')
@ -220,6 +222,11 @@ class BuildResult(models.Model):
static_run = fields.Char('Static run URL')
@api.depends('description', 'params_id.config_id')
def _compute_display_name(self):
for build in self:
build.display_name = build.description or build.config_id.name
@api.depends('params_id.config_id')
def _compute_log_list(self): # storing this field because it will be access trhoug repo viewn and keep track of the list at create
for build in self:
@ -260,6 +267,10 @@ class BuildResult(models.Model):
for build in self:
build.top_parent = self.browse(int(build.parent_path.split('/')[0]))
def _compute_ancestors(self):
for build in self:
build.ancestors = self.browse([int(b) for b in build.parent_path.split('/') if b])
def _get_youngest_state(self, states):
index = min([self._get_state_score(state) for state in states])
return state_order[index]
@ -379,9 +390,15 @@ class BuildResult(models.Model):
else:
build.domain = "%s:%s" % (domain, build.port)
@api.depends_context('batch')
def _compute_build_url(self):
batch = self.env.context.get('batch')
print(self.env.context)
for build in self:
build.build_url = "/runbot/build/%s" % build.id
if batch:
build.build_url = "/runbot/batch/%s/build/%s" % (batch.id, build.id)
else:
build.build_url = "/runbot/build/%s" % build.id
@api.depends('job_start', 'job_end')
def _compute_job_time(self):

View File

@ -184,6 +184,11 @@ class Bundle(models.Model):
for batch in batchs:
batch.bundle_id.last_done_batch = batch
def _url(self):
self.ensure_one()
return "/runbot/bundle/%s" % self.id
def create(self, values_list):
res = super().create(values_list)
if res.is_base:

View File

@ -146,7 +146,7 @@ class Runbot(models.AbstractModel):
return self.env.cr.fetchall()
def _domain(self):
return self.env.get('ir.config_parameter').get_param('runbot.runbot_domain', fqdn())
return self.env.get('ir.config_parameter').sudo().get_param('runbot.runbot_domain', fqdn())
def _reload_nginx(self):
env = self.env

View File

@ -0,0 +1,323 @@
body {
margin: 0;
font-size: 0.875rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: white;
}
form {
margin: 0;
}
table {
font-size: 0.875rem;
}
.fa {
line-height: inherit; /* reset fa icon line height to body height*/
}
a {
color: #00A09D;
text-decoration: none;
}
a:hover {
color: #005452;
text-decoration: underline;
}
.breadcrumb-item.active a {
color: #6c757d;
}
.breadcrumb {
background-color: inherit;
margin-bottom: 0;
}
.build_details {
padding: 5px;
}
.separator {
border-top: 2px solid #666;
}
[data-toggle="collapse"] .fa:before {
content: "\f139";
}
[data-toggle="collapse"].collapsed .fa:before {
content: "\f13a";
}
body, .table {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #444;
}
.btn-default {
background-color: #fff;
color: #444;
border-color: #ccc;
}
.btn-default:hover {
background-color: #ccc;
color: #444;
border-color: #ccc;
}
.btn-sm, .btn-group-sm > .btn {
padding: 0.25rem 0.5rem;
font-size: 0.89rem;
line-height: 1.5;
border-radius: 0.2rem;
}
.btn-ssm, .btn-group-ssm > .btn {
padding: 0.22rem 0.4rem;
font-size: 0.82rem;
line-height: 1;
border-radius: 0.2rem;
}
.killed, .bg-killed, .bg-killed-light {
background-color: #aaa;
}
.dropdown-toggle:after {
content: none;
}
.one_line {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.batch_tile {
padding: 6px;
}
.branch_time {
float: right;
margin-left: 10px;
}
:root {
--info-light: #d9edf7;
}
.bg-success-light {
background-color: #dff0d8;
}
.bg-danger-light {
background-color: #f2dede;
}
.bg-info-light {
background-color: var(--info-light);
}
.bg-warning-light {
background-color: #fff9e6;
}
.text-info {
color: #096b72 !important;
}
.build_subject_buttons {
display: flex;
}
.build_buttons {
margin-left: auto;
}
.bg-killed {
background-color: #aaa;
}
.badge-killed {
background-color: #aaa;
}
.table-condensed td {
padding: 0.25rem;
}
.line-through {
text-decoration: line-through;
}
.badge-light {
border: 1px solid #AAA;
}
.slot_button_group {
display: flex;
padding: 0 1px;
}
.slot_button_group .btn {
flex: 0 0 25px;
}
.slot_button_group .btn.slot_name {
width: 40px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1 1 auto;
text-align: left;
}
.batch_header {
padding: 6px;
}
.batch_header:hover {
background-color: rgba(0, 0, 0, 0.1)
}
.header_hover {
visibility: hidden;
}
.batch_header:hover .header_hover {
visibility: visible;
}
.batch_slots {
display: flex;
flex-wrap: wrap;
padding: 6px;
}
.batch_commits {
background-color: white;
}
.batch_commits {
padding: 2px;
}
.match_type_new {
background-color: var(--info-light);
}
.batch_row .slot_container {
flex: 1 0 200px;
padding: 0 4px;
}
.batch_row .slot_filler {
width: 100px;
height: 0px;
flex: 1 0 200px;
padding: 0 4px;
}
.bundle_row {
border-bottom: 1px solid var(--gray);
}
.bundle_row .batch_commits {
font-size: 80%;
}
.bundle_row .slot_container {
flex: 1 0 50%;
}
.bundle_row .slot_filler {
flex: 1 0 50%;
}
.bundle_row .more .batch_commits {
display: block;
}
/*.bundle_row .nomore .batch_commits {
display: none;
padding: 8px;
}
.bundle_row .nomore.batch_tile:hover .batch_commits {
display: block;
position: absolute;
bottom: 1px;
transform: translateY(100%);
z-index: 100;
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 0.2rem;
box-sizing: border-box;
margin-left: -1px;
}*/
.chart-legend {
max-height: calc(100vh - 160px);
overflow-y: scroll;
overflow-x: hidden;
cursor: pointer;
padding: 5px;
}
.chart-legend .label {
margin-left: 5px;
font-weight: bold;
}
.chart-legend .disabled .color {
visibility: hidden;
}
.chart-legend .disabled .label {
font-weight: normal;
text-decoration: line-through;
margin-left: 5px;
}
.chart-legend ul {
list-style-type: none;
margin: 0;
padding: 0;
}
.limited-height {
max-height: 180px;
overflow: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
}
.limited-height > hr {
margin: 2px 0px;
}
.limited-height:before {
content: '';
width: 100%;
height: 30px;
position: absolute;
left: 0;
bottom: 0;
background: linear-gradient(transparent 0px, white 27px);
}
.limited-height::-webkit-scrollbar {
display: none;
}
.limited-height-toggle:hover {
background-color: #DDD;
}

View File

@ -1,255 +0,0 @@
.separator {
border-top: 2px solid #666;
}
[data-toggle="collapse"] .fa:before {
content: "\f139";
}
[data-toggle="collapse"].collapsed .fa:before {
content: "\f13a";
}
body, .table{
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color:#444;
}
.btn-default {
background-color: #fff;
color: #444;
border-color: #ccc;
}
.btn-default:hover {
background-color: #ccc;
color: #444;
border-color: #ccc;
}
.btn-sm, .btn-group-sm > .btn {
padding: 0.25rem 0.5rem;
font-size: 0.89rem;
line-height: 1.5;
border-radius: 0.2rem;
}
.btn-ssm, .btn-group-ssm > .btn {
padding: 0.22rem 0.4rem;
font-size: 0.82rem;
line-height: 1;
border-radius: 0.2rem;
}
.killed, .bg-killed, .bg-killed-light {
background-color: #aaa;
}
.dropdown-toggle:after { content: none }
.one_line {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.batch_tile {
padding: 6px;
}
.branch_time {
float:right;
margin-left:10px;
}
:root {
--info-light: #d9edf7;
}
.bg-success-light {
background-color: #dff0d8;
}
.bg-danger-light {
background-color: #f2dede;
}
.bg-info-light {
background-color: var(--info-light);
}
.text-info{
color: #096b72 !important;
}
.build_subject_buttons {
display: flex;
}
.build_buttons {
margin-left: auto
}
.bg-killed {
background-color: #aaa;
}
.badge-killed {
background-color: #aaa;
}
.table-condensed td {
padding: 0.25rem;
}
.line-through {
text-decoration: line-through;
}
.badge-light{
border: 1px solid #AAA;
}
.arrow{
display: none;
}
.badge-light:hover .arrow{
display: inline;
}
.slot_button_group {
display: flex;
padding: 0 1px;
}
.slot_button_group .btn {
flex: 0 0 25px;
}
.slot_button_group .btn.slot_name {
width: 40px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1 1 auto;
text-align: left;
}
.batch_header {
padding: 6px;
padding-bottom: 0px;
}
.batch_slots {
display: flex;
flex-wrap: wrap;
padding: 6px;
}
.batch_commits {
background-color: white;
}
.batch_commits {
padding: 2px;
}
.match_type_new {
background-color: var(--info-light);
}
.batch_row {
.slot_container{
flex: 1 0 200px;
padding: 0 4px;
}
.slot_filler {
width: 100px;
height: 0px;
flex: 1 0 200px;
padding: 0 4px;
}
}
.bundle_row {
border-bottom: 1px solid var(--gray);
.batch_commits {
font-size: 80%;
}
.slot_container{
flex:1 0 50%;
}
.slot_filler {
flex:1 0 50%;
}
.more {
.batch_commits {
display: block;
}
}
.nomore {
.batch_commits {
display: none;
padding:8px;
}
}
.nomore.batch_tile:hover {
.batch_commits {
display: block;
position: absolute;
bottom: 1px;
transform: translateY(100%);
z-index: 100;
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 0.2rem;
box-sizing: border-box;
margin-left:-1px;
}
}
}
.chart-legend {
max-height: calc(100vh - 160px);
overflow-y: scroll;
overflow-x: hidden;
cursor: pointer;
padding: 5px;
.label {
margin-left: 5px;
font-weight: bold;
}
.disabled{
.color {
visibility: hidden;
}
.label {
font-weight: normal;
text-decoration: line-through;
margin-left: 5px;
}
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
}
}
.limited-height {
max-height: 180px;
overflow: scroll;
>hr {
margin: 2px 0px;
}
&:before {
content:'';
width:100%;
height:30px;
position:absolute;
left:0;
bottom:0;
background:linear-gradient(transparent 0px, white 27px);
}
-ms-overflow-style: none;
scrollbar-width: none;
}
.limited-height::-webkit-scrollbar {
display: none;
}
.limited-height-toggle:hover {
background-color: #DDD;
}

View File

@ -1,52 +1,32 @@
(function($) {
"use strict";
var OPMAP = {
'rebuild': {operation: 'rebuild', then: 'redirect'},
'kill': {operation: 'kill', then: 'reload'},
'wakeup': {operation: 'wakeup', then: 'reload'}
};
"use strict";
$(function () {
$(document).on('click', '[data-runbot]', function (e) {
e.preventDefault();
var data = $(this).data();
var segment = OPMAP[data.runbot];
if (!segment) { return; }
// window.location.pathname but compatibility is iffy
var currentPath = window.location.href.replace(window.location.protocol + '//' + window.location.host, '').split('?')[0];
var buildPath = _.str.sprintf('/runbot/build/%s', data.runbotBuild);
// no responseURL on $.ajax so use native object
var operation = data.runbot;
if (!operation) {
return;
}
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', function () {
switch (segment.then) {
case 'redirect':
if (currentPath === buildPath && xhr.responseURL) {
window.location.href = xhr.responseURL;
break;
}
// fallthrough to reload if no responseURL or we're
// not on the build's page
case 'reload':
if (operation == 'rebuild' && window.location.href.split('?')[0].endsWith('/build/' + data.runbotBuild)){
window.location.href = window.location.href.replace('/build/' + data.runbotBuild, '/build/' + xhr.responseText);
} else {
window.location.reload();
break;
}
});
xhr.open('POST', _.str.sprintf('%s/%s', buildPath, segment.operation));
xhr.open('POST', '/runbot/build/' + data.runbotBuild + '/' + operation);
xhr.send();
});
});
//$(function() {
// new Clipboard('.clipbtn');
//});
})(jQuery);
function copyToClipboard(text) {
if (!navigator.clipboard) {
console.error('Clipboard not supported');
return;
}
navigator.clipboard.writeText(text);
if (!navigator.clipboard) {
console.error('Clipboard not supported');
return;
}
navigator.clipboard.writeText(text);
}

View File

@ -1,11 +0,0 @@
<odoo>
<data>
<template id="assets_frontend" inherit_id="website.assets_frontend" name="runbot.assets.frontend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/runbot/static/src/css/runbot.scss"/>
<script type="text/javascript" src="/runbot/static/src/js/runbot.js"/>
<script type="text/javascript" src="/web/static/lib/Chart/Chart.js"></script>
</xpath>
</template>
</data>
</odoo>

View File

@ -2,7 +2,7 @@
<odoo>
<data>
<template id="runbot.batch">
<t t-call="website.layout">
<t t-call="runbot.layout">
<div class="row">
<div class="col-lg-6">
<table class="table table-stripped">
@ -14,18 +14,12 @@
</tr>
<tr t-if="batch.category_id.id != default_category">
<td>Category</td>
<td t-esc="batch.category_id.name">
<i t-attf-class="fa fa-{{batch.category_id.name}}"/>
</td>
<td t-esc="batch.category_id.name"></td>
</tr>
<tr>
<td>Version</td>
<td t-esc="batch.slot_ids[0].params_id.version_id.name if batch.slot_ids else batch.bundle_id.version_id.name"/>
</tr>
<tr>
<td>State</td>
<td t-esc="batch.state"/>
</tr>
<tr>
<td>Create date</td>
<td t-esc="batch.create_date"/>

View File

@ -2,7 +2,7 @@
<odoo>
<data>
<template id="runbot.branch">
<t t-call='website.layout'>
<t t-call='runbot.layout'>
<div class="container-fluid">
<div class="row">
<div class='col-md-12'>

View File

@ -2,7 +2,7 @@
<odoo>
<data>
<template id="runbot.build">
<t t-call='website.layout'>
<t t-call='runbot.layout'>
<t t-set="nav_form">
<form class="form-inline">
<div class="btn-group">
@ -18,189 +18,206 @@
<div class="col-md-12">
<t t-set="batches" t-value="build.top_parent.with_context(active_test=False).slot_ids.mapped('batch_id')"/>
<t t-set="bundles" t-value="batches.mapped('bundle_id')"/>
<t t-if="batches">
<t t-if="len(bundles) == 1">
<t t-if="len(batches) == 1">
<b>Batch:</b>
<a t-esc="bundles.name" t-attf-href="/runbot/batch/{{batches[0].id}}"/>
</t>
<t t-else="">
<b>Bundle:</b>
<t t-esc="bundles.name" t-attf-href="/runbot/bundle/{{bundle.id}}"/>
<br/>
</t>
</t>
<t t-else="">
This build is referenced in
<t t-esc="len(bundles)"/>
bundles
<t t-if="more">
:
<a t-foreach="bundles" class="badge badge-light" t-as="bundle" t-esc="bundle.name" t-attf-href="/runbot/bundle/{{bundle.id}}"/>
</t>
<br/>
<t t-if="from_batch" t-set="unique_batch" t-value="from_batch"/>
<t t-if="from_batch" t-set="unique_bundle" t-value="from_batch.bundle_id"/>
<t t-if="not unique_batch and len(batches) == 1" t-set="unique_batch" t-value="batches"/>
<t t-if="not unique_bundle and len(bundles) == 1" t-set="unique_bundle" t-value="bundles"/>
<!-- Breadcrumbs & Previous/Next-->
<nav aria-label="breadcrumb" class="d-flex justify-content-between align-items-center">
<ol class="breadcrumb mb-0">
<li t-attf-class="breadcrumb-item">
<a t-attf-href="/runbot/{{build.params_id.project_id.id}}">
<t t-esc="build.params_id.project_id.name"/>
</a>
</li>
<li t-if="unique_bundle" t-attf-class="breadcrumb-item">
<a t-att-href="unique_bundle._url()">
<t t-esc="unique_bundle.name"/>
</a>
</li>
<li t-if="unique_batch" t-attf-class="breadcrumb-item">
<a t-att-href="unique_batch._url()">
batch-<t t-esc="unique_batch.id"/> (<t t-esc="build.params_id.trigger_id.name"/>)
</a>
</li>
<li t-foreach="build.ancestors" t-as="ancestor" t-attf-class="breadcrumb-item{{' active' if ancestor == build else ''}}">
<a t-att-href="ancestor.build_url">
<t t-esc="ancestor.description or ancestor.config_id.name"/>
</a>
</li>
</ol>
<span class="btn-group pr-3">
<a t-att-href="prev_ko.build_url" role="button" t-attf-title="Previous ko {{prev_ko.display_name}}"
t-attf-class="{{'' if prev_ko else 'disabled '}}btn btn-default fa fa-angle-double-left"></a>
<a t-att-href="prev_bu.build_url" role="button" t-attf-title="Previous {{prev_bu.display_name}}"
t-attf-class="{{'' if prev_bu else 'disabled '}}btn btn-default fa fa-chevron-left"></a>
<a t-att-href="next_bu.build_url" role="button" t-attf-title="Next {{next_bu.display_name}}"
t-attf-class="{{'' if next_bu else 'disabled '}}btn btn-default fa fa-chevron-right"></a>
<a t-att-href="next_ko.build_url" role="button" t-attf-title="Next ko {{next_ko.display_name}}"
t-attf-class="{{'' if next_ko else 'disabled '}}btn btn-default fa fa-angle-double-right"></a>
</span>
</nav>
</div>
<!-- Build details-->
<t t-set="rowclass">
<t t-call="runbot.build_class">
<t t-set="build" t-value="build"/>
</t>
</t>
<div t-attf-class="bg-{{rowclass.strip()}}-light {{'col-md-6' if build.children_ids else 'col-md-12'}}">
<div class="build_details">
<!-- Batch/bundles links-->
<t t-if="len(bundles) > 1">
This build is referenced in <t t-esc="len(bundles)"/> bundles
<ul>
<li t-foreach="bundles" t-as="bundle" ><a t-esc="bundle.name" t-attf-href="/runbot/bundle/{{bundle.id}}"/></li>
</ul>
</t>
<t t-if="len(batches) > 1">
First apparition:
<a t-esc="batches[0].bundle_id.name" t-attf-href="/runbot/batch/{{batches[0].id}}"/>
<br/>
Last apparition:
<a t-esc="batches[-1].bundle_id.name" t-attf-href="/runbot/batch/{{batches[-1].id}}"/>
<br/>
<b>First apparition:</b> <a t-esc="batches[0].bundle_id.name" t-attf-href="/runbot/batch/{{batches[0].id}}"/><br/>
<b>Last apparition:</b> <a t-esc="batches[-1].bundle_id.name" t-attf-href="/runbot/batch/{{batches[-1].id}}"/><br/>
</t>
</t>
<div t-if="build.parent_id">
<b>Parent build:</b>
<a t-attf-href="/runbot/build/#{build.parent_id.id}">
<t t-esc="build.parent_id.dest"/>
</a>
<t t-if="build.orphan_result">
&amp;nbsp;
<!-- Parent -->
<div t-if="build.parent_id and build.orphan_result">
<i class="fa fa-chain-broken" title="Build result ignored for parent" />
&amp;nbsp;Orphaned build, the result does not affect parent build result
</div>
<t t-if="build.description">
<b>Description:</b>
<t t-raw="build.md_description"/>
<br/>
</t>
<!-- Commits -->
<t t-foreach="build.params_id.sudo().commit_link_ids" t-as="build_commit">
<b>Commit:</b>
<a t-attf-href="/runbot/commit/{{build_commit.commit_id.id}}">
<t t-esc="build_commit.commit_id.dname"/>
</a>
&amp;nbsp;
<a t-att-href="'https://%s/commit/%s' % (build_commit.branch_id.remote_id.base_url, build_commit.commit_id.name)" title="View Commit on Github"><i class="fa fa-github"/></a>
<t t-if="build_commit.match_type in ('default', 'pr_target', 'prefix') ">
from base branch
<br/>
</t>
<div t-else="" class="ml-3">
<b>Subject:</b>
<t t-esc="build_commit.commit_id.subject"/>
<br/>
<b>Author:</b>
<t t-esc="build_commit.commit_id.author"/>
<br/>
<b>Committer:</b>
<t t-esc="build_commit.commit_id.committer"/>
<br/>
</div>
</t>
<b>Version:</b>
<t t-esc="build.params_id.version_id.name"/>
<br/>
<b>Config:</b>
<t t-esc="build.params_id.config_id.name"/>
<br/>
<t t-if='more'>
<b>Trigger:</b>
<t t-esc="build.params_id.trigger_id.name"/>
<br/>
<b>Config data:</b>
<t t-esc="build.params_id.config_data.dict"/>
<br/>
<b>Modules:</b>
<t t-esc="build.params_id.modules"/>
<br/>
<b>Extra params:</b>
<t t-esc="build.params_id.extra_params"/>
<br/>
<t t-if="len(build.params_id.builds_reference_ids) > 1">
<b>Reference batch:</b>
<t t-foreach="build.params_id.builds_reference_ids" t-as="reference">
<span t-esc="reference.id"/>
</t>
<br/>
</t>
<t t-if="len(build.params_id.build_ids) > 1">
<b>Similar builds:</b>
<t t-foreach="build.params_id.build_ids" t-as="simbuild">
<a t-if="simbuild.id != build.id" t-attf-href="/runbot/build/#{simbuild.id}">
<span
t-attf-class="badge badge-{{simbuild.get_color_class()}}"
t-esc="simbuild.id"/>
</a>
</t>
<br/>
</t>
<b>Host:</b>
<t t-esc="build.host"/>
<br/>
</t>
<b>Total time:</b>
<t t-esc="build.get_formated_build_time()"/>
<br/>
<t t-if="build.stat_ids">
<b>Stats:</b>
<a t-attf-href="/runbot/build/stats/{{build.id}}">Build <t t-esc="build.id"/></a>
<br/>
</t>
</div>
</div>
<div class="col-md-12">
<table class="table table-condensed tabel-bordered">
<tr>
<div class="col-md-6" t-if="build.children_ids">
Children:
<table class="table table-condensed">
<t t-foreach="build.children_ids.sorted('id')" t-as="child">
<t t-set="rowclass">
<t t-call="runbot.build_class">
<t t-set="build" t-value="build"/>
<t t-set="build" t-value="child"/>
</t>
</t>
<td t-attf-class="bg-{{rowclass.strip()}}-light">
<t t-if="build.description">
<b>Description:</b>
<t t-raw="build.md_description"/>
<br/>
</t>
<t t-foreach="build.params_id.sudo().commit_link_ids" t-as="build_commit">
<b>Commit:</b>
<a t-attf-href="/runbot/commit/{{build_commit.commit_id.id}}">
<t t-esc="build_commit.commit_id.dname"/>
<tr t-attf-class="bg-{{rowclass.strip()}}-light{{' line-through' if child.orphan_result else ''}}">
<td>
<a t-attf-href="/runbot/{{'batch/%s/' % from_batch.id if from_batch else ''}}build/{{child.id}}">
Build
<t t-esc="child.id"/>
</a>
<a t-att-href="'https://%s/commit/%s' % (build_commit.branch_id.remote_id.base_url, build_commit.commit_id.name)" class="btn btn-sm text-left" title="View Commit on Github"><i class="fa fa-github"/></a>
<t t-if="build_commit.match_type in ('default', 'pr_target', 'prefix') ">
from base branch
<br/>
<t t-if="child.description">
<t t-raw="child.md_description" />
</t>
<div t-else="" class="ml-3">
<b>Subject:</b>
<t t-esc="build_commit.commit_id.subject"/>
<br/>
<b>Author:</b>
<t t-esc="build_commit.commit_id.author"/>
<br/>
<b>Committer:</b>
<t t-esc="build_commit.commit_id.committer"/>
<br/>
</div>
</t>
<b>Version:</b>
<t t-esc="build.params_id.version_id.name"/>
<br/>
<b>Config:</b>
<t t-esc="build.params_id.config_id.name"/>
<br/>
<t t-if='more'>
<b>Trigger:</b>
<t t-esc="build.params_id.trigger_id.name"/>
<br/>
<b>Config data:</b>
<t t-esc="build.params_id.config_data.dict"/>
<br/>
<b>Modules:</b>
<t t-esc="build.params_id.modules"/>
<br/>
<b>Extra params:</b>
<t t-esc="build.params_id.extra_params"/>
<br/>
<t t-if="len(build.params_id.builds_reference_ids) > 1">
<b>Reference batch:</b>
<t t-foreach="build.params_id.builds_reference_ids" t-as="reference">
<span t-esc="reference.id"/>
</t>
<br/>
<t t-else="">
with config
<t t-esc="child.params_id.config_id.name"/>
</t>
<a groups="runbot.group_build_config_user" t-attf-href="/web#id={{child.params_id.config_id.id}}&amp;view_type=form&amp;model=runbot.build.config">...</a>
<t t-if="child.orphan_result">
<i class="fa fa-chain-broken" title="Build result ignored for parent" />
</t>
<t t-if="child.job">
Running step:
<t t-esc="child.job"/>
</t>
<t t-if="child.global_state in ['testing', 'waiting']">
<i class="fa fa-spinner fa-spin"/>
<t t-esc="child.global_state"/>
</t>
</td>
<td>
<span t-attf-class="badge badge-info" t-esc="child.get_formated_build_time()"/>
</td>
<td>
<t t-call="runbot.build_button">
<t t-set="bu" t-value="child"/>
<t t-set="klass" t-value="'btn-group-ssm'"/>
</t>
<t t-if="len(build.params_id.build_ids) > 1">
<b>Similar builds:</b>
<t t-foreach="build.params_id.build_ids" t-as="simbuild">
<a t-if="simbuild.id != build.id" t-attf-href="/runbot/build/#{simbuild.id}">
<span
t-attf-class="badge badge-{{simbuild.get_color_class()}}"
t-esc="simbuild.id"/>
</a>
</t>
<br/>
</t>
<b>Host:</b>
<t t-esc="build.host"/>
<br/>
</t>
<b>Total time:</b>
<t t-esc="build.get_formated_build_time()"/>
<br/>
<t t-if="build.stat_ids">
<b>Stats:</b>
<a t-attf-href="/runbot/build/stats/{{build.id}}">Build <t t-esc="build.id"/></a>
<br/>
</t>
<br/>
</td>
<td t-if="build.children_ids">
Children:
<table class="table table-condensed">
<t t-foreach="build.children_ids.sorted('id')" t-as="child">
<t t-set="rowclass">
<t t-call="runbot.build_class">
<t t-set="build" t-value="child"/>
</t>
</t>
<tr t-attf-class="bg-{{rowclass.strip()}}-light{{' line-through' if child.orphan_result else ''}}">
<td>
<a t-attf-href="/runbot/build/{{child.id}}">
Build
<t t-esc="child.id"/>
</a>
<t t-if="child.description">
<t t-raw="child.md_description" />
</t>
<t t-else="">
with config
<t t-esc="child.params_id.config_id.name"/>
</t>
<a groups="runbot.group_build_config_user" t-attf-href="/web#id={{child.params_id.config_id.id}}&amp;view_type=form&amp;model=runbot.build.config">...</a>
<t t-if="child.orphan_result">
<i class="fa fa-chain-broken" title="Build result ignored for parent" />
</t>
<t t-if="child.job">
Running step:
<t t-esc="child.job"/>
</t>
<t t-if="child.global_state in ['testing', 'waiting']">
<i class="fa fa-spinner fa-spin"/>
<t t-esc="child.global_state"/>
</t>
</td>
<td>
<span t-attf-class="badge badge-info" t-esc="child.get_formated_build_time()"/>
</td>
<td>
<t t-call="runbot.build_button">
<t t-set="bu" t-value="child"/>
<t t-set="klass" t-value="'btn-group-ssm'"/>
</t>
</td>
</tr>
</t>
</table>
</td>
</tr>
</td>
</tr>
</t>
</table>
</div>
<div class="col-md-12">
<table class="table table-condensed">
<tr>
<th>Date</th>

View File

@ -55,7 +55,7 @@
</template>
<template id="runbot.build_error">
<t t-call='website.layout'>
<t t-call='runbot.layout'>
<div class="container-fluid">
<div class="row">
<div class='col-md-12'>
@ -96,7 +96,7 @@
</template>
<template id="runbot.team">
<t t-call='website.layout'>
<t t-call='runbot.layout'>
<div class="container-fluid bg-light">
<div class="row">
<div t-if="team" class='col-md-12'>

View File

@ -2,7 +2,7 @@
<odoo>
<data>
<template id="runbot.build_stats">
<t t-call='website.layout'>
<t t-call='runbot.layout'>
<t t-set="bundles" t-value="build.slot_ids.mapped('batch_id.bundle_id')"/>
<div>
<div class="row">
@ -63,7 +63,7 @@
</template>
<template id="runbot.modules_stats">
<t t-call='website.layout'>
<t t-call='runbot.layout'>
<input type="hidden" id="bundle_id" t-att-value="bundle.id"/>
<input type="hidden" id="trigger_id" t-att-value="trigger.id"/>
<div class="container-fluid">

View File

@ -2,7 +2,7 @@
<odoo>
<data>
<template id="runbot.bundle">
<t t-call='website.layout'>
<t t-call='runbot.layout'>
<div class="container-fluid">
<div class="row">
<div class='col-md-12'>

View File

@ -21,7 +21,7 @@
</template>
<template id="runbot.commit">
<t t-call='website.layout'>
<t t-call='runbot.layout'>
<div class="row">
<!-- Commit base informations -->
<div class="col-md-6">

View File

@ -2,7 +2,7 @@
<odoo>
<data>
<template id="runbot.glances">
<t t-call='portal.frontend_layout'>
<t t-call="runbot.layout">
<t t-set="head">
<t t-if="refresh">
<meta http-equiv="refresh" t-att-content="refresh"/>
@ -37,7 +37,7 @@
</t>
</template>
<template id="frontend_no_nav" inherit_id="portal.frontend_layout" primary="True">
<template id="frontend_no_nav" inherit_id="runbot.layout" primary="True">
<xpath expr="//header" position="replace">
</xpath>
</template>

View File

@ -2,11 +2,11 @@
<odoo>
<data>
<template id="runbot.bundles">
<t t-call='web.frontend_layout'>
<t t-call='runbot.layout'>
<t t-set="nav_form">
<form class="form-inline my-2 my-lg-0" role="search" t-att-action="qu(search='')" method="get">
<div class="input-group md-form form-sm form-2 pl-0">
<input class="form-control my-0 py-1 red-border" type="text" placeholder="Search" aria-label="Search" name="search" t-att-value="search"/>
<input class="form-control my-0 py-1" type="text" placeholder="Search" aria-label="Search" name="search" t-att-value="search"/>
<div class="input-group-append">
<button type='submit' class="input-group-text red lighten-3" id="basic-text1">
<i class="fa fa-search text-grey"/>
@ -79,15 +79,17 @@
<t t-if="batch.state=='done' and all(slot.build_id.global_result == 'ok' for slot in batch.slot_ids if slot.build_id)" t-set="klass">success</t>
<t t-if="batch.state=='done' and any(slot.build_id.global_result in ('ko', 'warn') for slot in batch.slot_ids)" t-set="klass">danger</t>
<div t-attf-class="batch_tile {{'more' if more else 'nomore'}}">
<div t-attf-class="batch_tile if more">
<div t-attf-class="card bg-{{klass}}-light">
<div class="batch_header">
<a t-attf-href="/runbot/batch/#{batch.id}" t-attf-class="badge badge-{{'warning' if batch.has_warning else 'light'}}" title="View Batch">
<t t-esc="batch.get_formated_age()"/>
<i class="fa fa-exclamation-triangle" t-if="batch.has_warning"/>
<i class="arrow fa fa-window-maximize"/>
</a>
</div>
<a t-attf-href="/runbot/batch/#{batch.id}" title="View Batch">
<div class="batch_header">
<span t-attf-class="badge badge-{{'warning' if batch.has_warning else 'light'}}">
<t t-esc="batch.get_formated_age()"/>
<i class="fa fa-exclamation-triangle" t-if="batch.has_warning"/>
</span>
<span class="float-right header_hover">View batch...</span>
</div>
</a>
<t t-if="batch.state=='preparing'">
<span><i class="fa fa-cog fa-spin fa-fw"/> preparing</span>
</t>
@ -100,7 +102,7 @@
</t>
<div class="slot_filler" t-foreach="range(10)" t-as="x"/>
</div>
<div class="batch_commits">
<div t-if='more' class="batch_commits">
<div t-foreach="batch.commit_link_ids.sorted(lambda cl: (cl.commit_id.repo_id.sequence, cl.commit_id.repo_id.id))" t-as="commit_link" class="one_line">
<a t-attf-href="/runbot/commit/#{commit_link.commit_id.id}" t-attf-class="badge badge-light batch_commit match_type_{{commit_link.match_type}}">

View File

@ -2,136 +2,153 @@
<odoo>
<data>
<!-- base layout -->
<template id="runbot.layout" inherit_id="website.layout" name="Custom website layout">
<xpath expr="//head/meta[last()]" position="after">
<t t-if="refresh">
<meta http-equiv="refresh" t-att-content="refresh"/>
</t>
</xpath>
<xpath expr="//footer" position="replace">
</xpath>
<xpath expr="//nav[hasclass('navbar')]" position="replace">
<nav class="navbar navbar-expand-md navbar-light bg-light">
<a t-if="project" t-att-href="qu(search=search)">
<b style="color:#777;">
<t t-esc="project.name"/>
</b>
</a>
<button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#top_menu_collapse">
<span class="navbar-toggler-icon"/>
</button>
<div class="collapse navbar-collapse" id="top_menu_collapse">
<ul class="nav navbar-nav ml-auto text-right" id="top_menu">
<t t-if="projects">
<t t-foreach="projects" t-as="l_project">
<li class="nav-item">
<a class="nav-link" t-att-href="qu('/runbot/%s' % slug(l_project), search=search)">
<t t-esc="l_project.name"/>
</a>
</li>
</t>
</t>
<li class="nav-item divider"/>
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-gear"/>
</a>
<div class="dropdown-menu" role="menu">
<form class="px-4 py-3" method="post" action="/runbot/submit">
<input type="hidden" name="save" value="1"/>
<input type="hidden" name="redirect" t-att-value="current_path"/>
<div class="text-nowrap">
<input type="checkbox" name="more" id="more" t-att-checked="more"/>
<label for="more">More info</label>
</div>
<div class="text-nowrap">
<input type="checkbox" name="keep_search" id="keep_search" t-att-checked="keep_search"/>
<label for="keep_search">Persistent search</label>
</div>
<hr class="separator"/>
<div class="text-nowrap">
<label for="filter_mode">Filter</label>
<select class="form-control" name="filter_mode" id="filter_mode">
<option value="all" t-att-selected="filter_mode=='all'">All</option>
<option value="sticky" t-att-selected="filter_mode=='sticky'">Sticky only</option>
<option value="nosticky" t-att-selected="filter_mode=='nosticky'">Dev only</option>
</select>
</div>
<div t-if="categories" class="text-nowrap">
<label for="category">Category</label>
<select class="form-control" name="category" id="category">
<option t-foreach="categories" t-as="category" t-att-value="category.id" t-esc="category.name" t-att-selected="category.id==active_category_id"/>
</select>
</div>
<hr class="separator"/>
<t t-if="triggers">
<input type="hidden" name="update_triggers" t-att-value="project.id"/>
<t t-foreach="triggers" t-as="trigger">
<div class="text-nowrap">
<input type="checkbox" t-attf-name="trigger_{{trigger.id}}" t-attf-id="trigger_{{trigger.id}}" t-att-checked="trigger_display is None or trigger.id in trigger_display"/>
<label t-attf-for="trigger_{{trigger.id}}" t-esc="trigger.name"/>
</div>
</t>
</t>
<template id="runbot.base_page">
<html>
<head>
<title t-esc="title or 'Runbot'"/>
<link rel="stylesheet" type="text/css" href="/web/static/lib/bootstrap/css/bootstrap.css"/>
<link rel="stylesheet" type="text/css" href="/web/static/lib/fontawesome/css/font-awesome.css"/>
<link rel="stylesheet" type="text/css" href="/runbot/static/src/css/runbot.css"/>
<button type="submit" class="btn btn-primary">Save</button>
</form>
</div>
</li>
<li class="nav-item divider" t-ignore="true"/>
<t t-if="not user_id._is_public()">
<t t-if="nb_assigned_errors and nb_assigned_errors > 0">
<script src="/web/static/lib/jquery/jquery.js" type="text/javascript"/>
<script type="text/javascript" src="/web/static/lib/popper/popper.js"/>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/util.js"/>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dropdown.js"/>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/collapse.js"/>
<script type="text/javascript" src="/runbot/static/src/js/runbot.js"/>
<t t-if="refresh">
<meta http-equiv="refresh" t-att-content="refresh"/>
</t>
</head>
<body>
<t t-raw="0"/>
</body>
</html>
</template>
<template id="runbot.layout" inherit_id="runbot.base_page" primary="True">
<xpath expr="//body" position="replace">
<body>
<header>
<nav class="navbar navbar-expand-md navbar-light bg-light">
<a t-if="project" t-att-href="qu(search=search)">
<b style="color:#777;">
<t t-esc="project.name"/>
</b>
</a>
<button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#top_menu_collapse">
<span class="navbar-toggler-icon"/>
</button>
<div class="collapse navbar-collapse" id="top_menu_collapse">
<ul class="nav navbar-nav ml-auto text-right" id="top_menu">
<t t-if="projects">
<t t-foreach="projects" t-as="l_project">
<li class="nav-item">
<a class="nav-link" t-att-href="qu('/runbot/%s' % slug(l_project), search=search)">
<t t-esc="l_project.name"/>
</a>
</li>
</t>
</t>
<li class="nav-item divider"/>
<li class="nav-item">
<a href="/runbot/errors" class="nav-link text-danger" t-attf-title="You have {{nb_assigned_errors}} random bug assigned"><i class="fa fa-bug"/><t t-esc="nb_assigned_errors"/></a>
</li>
</t>
<t t-elif="nb_build_errors and nb_build_errors > 0">
<li class="nav-item divider"/>
<li class="nav-item">
<a href="/runbot/errors" class="nav-link" title="Random Bugs"><i class="fa fa-bug"/></a>
</li>
</t>
<li class="nav-item dropdown" t-ignore="true">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown">
<b>
<span t-esc="user_id.name[:23] + '...' if user_id.name and len(user_id.name) &gt; 25 else user_id.name"/>
</b>
</a>
<div class="dropdown-menu js_usermenu" role="menu">
<a class="dropdown-item" id="o_logout" role="menuitem" t-attf-href="/web/session/logout?redirect=/">Logout</a>
<a class="dropdown-item" role="menuitem" t-attf-href="/web">Web</a>
<div t-if="user_id.runbot_team_ids" class="dropdown-divider"/>
<div t-if="user_id.runbot_team_ids" class="dropdown-header">Teams</div>
<a t-foreach="user_id.runbot_team_ids" t-as="team" class="dropdown-item" role="menuitem" t-attf-href="/runbot/teams/{{team.id}}">
<t t-esc="team.name.capitalize()"/>
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-gear"/>
</a>
</div>
</li>
</t>
<t t-else="">
<li class="nav-item dropdown" t-ignore="true">
<b>
<a class="nav-link" t-attf-href="/web/login?redirect=/">Login</a>
</b>
</li>
</t>
</ul>
<t t-raw="nav_form or ''">
</t>
</div>
</nav>
<div class="dropdown-menu" role="menu">
<form class="px-4 py-3" method="post" action="/runbot/submit">
<input type="hidden" name="save" value="1"/>
<input type="hidden" name="redirect" t-att-value="current_path"/>
<div class="text-nowrap">
<input type="checkbox" name="more" id="more" t-att-checked="more"/>
<label for="more">More info</label>
</div>
<div class="text-nowrap">
<input type="checkbox" name="keep_search" id="keep_search" t-att-checked="keep_search"/>
<label for="keep_search">Persistent search</label>
</div>
<hr class="separator"/>
<div class="text-nowrap">
<label for="filter_mode">Filter</label>
<select class="form-control" name="filter_mode" id="filter_mode">
<option value="all" t-att-selected="filter_mode=='all'">All</option>
<option value="sticky" t-att-selected="filter_mode=='sticky'">Sticky only</option>
<option value="nosticky" t-att-selected="filter_mode=='nosticky'">Dev only</option>
</select>
</div>
<div t-if="categories" class="text-nowrap">
<label for="category">Category</label>
<select class="form-control" name="category" id="category">
<option t-foreach="categories" t-as="category" t-att-value="category.id" t-esc="category.name" t-att-selected="category.id==active_category_id"/>
</select>
</div>
<hr class="separator"/>
<t t-if="triggers">
<input type="hidden" name="update_triggers" t-att-value="project.id"/>
<t t-foreach="triggers" t-as="trigger">
<div class="text-nowrap">
<input type="checkbox" t-attf-name="trigger_{{trigger.id}}" t-attf-id="trigger_{{trigger.id}}" t-att-checked="trigger_display is None or trigger.id in trigger_display"/>
<label t-attf-for="trigger_{{trigger.id}}" t-esc="trigger.name"/>
</div>
</t>
</t>
<button type="submit" class="btn btn-primary">Save</button>
</form>
</div>
</li>
<li class="nav-item divider" t-ignore="true"/>
<t t-if="not user_id._is_public()">
<t t-call="runbot.build_errors_link"/>
<li class="nav-item dropdown" t-ignore="true">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown">
<b>
<span t-esc="user_id.name[:23] + '...' if user_id.name and len(user_id.name) &gt; 25 else user_id.name"/>
</b>
</a>
<div class="dropdown-menu js_usermenu" role="menu">
<a class="dropdown-item" id="o_logout" role="menuitem" t-attf-href="/web/session/logout?redirect=/">Logout</a>
<a class="dropdown-item" role="menuitem" t-attf-href="/web">Web</a>
<div t-if="user_id.runbot_team_ids" class="dropdown-divider"/>
<div t-if="user_id.runbot_team_ids" class="dropdown-header">Teams</div>
<a t-foreach="user_id.runbot_team_ids" t-as="team" class="dropdown-item" role="menuitem" t-attf-href="/runbot/teams/{{team.id}}">
<t t-esc="team.name.capitalize()"/>
</a>
</div>
</li>
</t>
<t t-else="">
<li class="nav-item dropdown" t-ignore="true">
<b>
<a class="nav-link" t-attf-href="/web/login?redirect=/">Login</a>
</b>
</li>
</t>
</ul>
<t t-raw="nav_form or ''">
</t>
</div>
</nav>
</header>
<t t-raw="0"/>
</body>
</xpath>
</template>
<!-- remove black bar with app switcher -->
<template id="inherits_no_black_bar" inherit_id="website.user_navbar" name="Inherits No black user_navbar">
<xpath expr="//nav[@id='oe_main_menu_navbar']" position="attributes">
<attribute name="groups">base.group_website_publisher</attribute>
</xpath>
<xpath expr="//t[@t-set='body_classname']" position="attributes">
<attribute name="t-value">'o_connected_user' if env['ir.ui.view'].user_has_groups('base.group_website_publisher') else None</attribute>
</xpath>
<template id="runbot.build_errors_link">
<t t-if="nb_assigned_errors and nb_assigned_errors > 0">
<li class="nav-item divider"/>
<li class="nav-item">
<a href="/runbot/errors" class="nav-link text-danger" t-attf-title="You have {{nb_assigned_errors}} random bug assigned"><i class="fa fa-bug"/><t t-esc="nb_assigned_errors"/></a>
</li>
</t>
<t t-elif="nb_build_errors and nb_build_errors > 0">
<li class="nav-item divider"/>
<li class="nav-item">
<a href="/runbot/errors" class="nav-link" title="Random Bugs"><i class="fa fa-bug"/></a>
</li>
</t>
</template>
<template id="runbot.slots_infos" name="Hosts slot nb pending/testing/slots">
@ -163,7 +180,7 @@
<span t-attf-class="btn btn-{{color}} disabled" t-att-title="slot.link_type">
<i t-attf-class="fa fa-{{slot.fa_link_type()}}"/>
</span>
<a t-if="bu" t-attf-href="/runbot/build/#{bu.id}" t-attf-class="btn btn-default slot_name">
<a t-if="bu" t-attf-href="/runbot/batch/{{slot.batch_id.id}}/build/#{bu.id}" t-attf-class="btn btn-default slot_name">
<span t-esc="slot.trigger_id.name"/>
</a>
<span t-else="" t-attf-class="btn btn-default disabled slot_name">
@ -181,10 +198,10 @@
<template id="runbot.build_button">
<div t-attf-class="pull-right">
<div t-attf-class="btn-group {{klass}}">
<a t-if="bu.local_state == 'running' and bu.database_ids" t-attf-href="http://{{sorted(bu.mapped('database_ids.name'))[0]}}.{{bu.host}}" class="btn btn-primary" title="Sign in on this build" aria-label="Sign in on this build">
<a t-if="bu.local_state == 'running' and bu.database_ids" t-attf-href="http://{{sorted(bu.mapped('database_ids.name'))[0]}}.{{bu.host}}" class="btn btn-info" title="Sign in on this build" aria-label="Sign in on this build">
<i class="fa fa-sign-in"/>
</a>
<a t-if="bu.static_run" t-att-href="bu.static_run" class="btn btn-primary" title="View result" aria-label="View result">
<a t-if="bu.static_run" t-att-href="bu.static_run" class="btn btn-info" title="View result" aria-label="View result">
<i class="fa fa-sign-in"/>
</a>
<a t-if="bu.local_state=='done' and bu.requested_action != 'wake_up' and bu.database_ids" href="#" data-runbot="wakeup" t-att-data-runbot-build="bu.id" class="btn btn-default" title="Wake up this build" aria-label="Wake up this build">
@ -255,7 +272,7 @@
</a>
</t>
<t t-else="">
<a groups="base.group_user" data-runbot="kill" class="dropdown-item disabled">
<a groups="base.group_user" class="dropdown-item disabled">
<i class="fa fa-spinner fa-spin"/>
Killing
<i class="fa fa-crosshairs"/>
@ -270,7 +287,7 @@
</a>
</t>
<t t-else="">
<a groups="base.group_user" class="dropdown-item disabled" data-runbot="wakeup">
<a groups="base.group_user" class="dropdown-item disabled">
<i class="fa fa-spinner fa-spin"/>
Waking up
<i class="fa fa-crosshairs"/>

View File

@ -108,7 +108,6 @@
</record>
<record id="bundle_runbot_13_dev_tri" model="runbot.bundle">
<field name="name">13.0-dev-tri</field>
<field name="is_base">True</field>
<field name="project_id" ref="project_runbot"/>
</record>

View File

@ -24,7 +24,7 @@ class Runbot(models.AbstractModel):
assert bundles|self.env.ref('runbot.bundle_dummy') == bundles.search([])
if False and bundles.branch_ids:
if bundles.branch_ids:
# only populate data if no branch are found
return