--wip-- [skip ci]

This commit is contained in:
William Braeckman 2024-12-30 17:10:20 +01:00
parent 998163ee03
commit d305d11e7c
16 changed files with 1427 additions and 1332 deletions

View File

@ -23,6 +23,7 @@
'security/ir.model.access.csv',
'security/ir.rule.csv',
'templates/layout.xml',
'templates/utils.xml',
'templates/badge.xml',
'templates/batch.xml',
@ -32,10 +33,13 @@
'templates/bundle.xml',
'templates/commit.xml',
'templates/dashboard.xml',
'templates/frontend.xml',
'templates/git.xml',
'templates/nginx.xml',
'templates/build_error.xml',
'templates/navbar.xml',
'templates/slot.xml',
'templates/toolbar.xml',
'templates/trigger.xml',
'views/branch_views.xml',
'views/build_error_views.xml',
@ -66,15 +70,18 @@
'runbot/static/src/js/fields/*',
],
'runbot.assets_frontend': [
'/web/static/lib/bootstrap/dist/css/bootstrap.css',
'/web/static/src/libs/fontawesome/css/font-awesome.css',
('include', 'web.assets_frontend_minimal'),
# New stuff
'/runbot/static/src/css/runbot_new.scss',
# Old stuff
'/runbot/static/src/libs/bootstrap/css/bootstrap.css',
'/runbot/static/src/libs/fontawesome/css/font-awesome.css',
'/runbot/static/src/css/runbot.css',
'/web/static/lib/jquery/jquery.js',
'/web/static/lib/popper/popper.js',
#'/web/static/lib/bootstrap/js/dist/util.js',
'/web/static/lib/bootstrap/js/dist/dropdown.js',
'/web/static/lib/bootstrap/js/dist/collapse.js',
'/runbot/static/src/libs/jquery/jquery.js',
'/runbot/static/src/libs/popper/popper.js',
'/runbot/static/src/libs/bootstrap/js/bootstrap.bundle.js',
'/runbot/static/src/js/runbot.js',
],
},

View File

@ -3,6 +3,7 @@ import datetime
import werkzeug
import logging
import functools
from typing import TypedDict, Optional, List, NamedTuple
import werkzeug.utils
import werkzeug.urls
@ -18,13 +19,39 @@ from odoo.osv import expression
_logger = logging.getLogger(__name__)
class Breadcrumb(NamedTuple):
url: str
name: str
Breadcrumbs = List[Breadcrumb]
class ToolbarContext(TypedDict):
"""
Context used by 'runbot.layout_toolbar', should be provided through the 'toolbar' context key.
"""
sticky: Optional[bool] # Defines if the toolbar is sticky or not, defaults to true
start_template: Optional[str] # Default to 'runbot.layout_toolbar_start_section'
# start_template default expected values
breadcrumbs: Optional[Breadcrumbs]
middle_template: Optional[str] # Defaults to 'runbot.layout_toolbar_middle_section'
# middle_template default expected values
message: Optional[str]
end_template: Optional[str] # Defaults to 'runbot.layout_toolbar_end_section'
# end_template expected values
pending_count: Optional[int]
pending_level: Optional[int]
pending_assigned_count: Optional[int]
# hosts_data: Optional[request.env['runbot.host']]
def route(routes, **kw):
def decorator(f):
@o_route(routes, **kw)
@functools.wraps(f)
def response_wrap(*args, **kwargs):
projects = request.env['runbot.project'].search([('hidden', '=', False)])
more = request.httprequest.cookies.get('more', False) == '1'
more = request.httprequest.cookies.get('more', False) in ('1', 'true')
filter_mode = request.httprequest.cookies.get('filter_mode', 'all')
keep_search = request.httprequest.cookies.get('keep_search', False) == '1'
cookie_search = request.httprequest.cookies.get('search', '')
@ -47,7 +74,6 @@ def route(routes, **kw):
project = response.qcontext.get('project') or projects and projects[0]
response.qcontext['theme'] = kwargs.get('theme', request.httprequest.cookies.get('theme', 'legacy'))
response.qcontext['projects'] = projects
response.qcontext['more'] = more
response.qcontext['keep_search'] = keep_search
@ -72,6 +98,22 @@ def route(routes, **kw):
class Runbot(Controller):
def _get_default_toolbar(self, *, include_message=False):
pending_count, level, _, pending_assigned_count = self._pending()
if include_message:
message = request.env['ir.config_parameter'].sudo().get_param('runbot.runbot_message')
else:
message = None
return ToolbarContext(
pending_count=pending_count,
pending_level=level,
pending_assigned_count=pending_assigned_count,
hosts_data=request.env['runbot.host'].search([('assigned_only', '=', False)]),
message=message,
)
def _pending(self):
ICP = request.env['ir.config_parameter'].sudo().get_param
warn = int(ICP('runbot.pending.warning', 5))
@ -82,27 +124,6 @@ class Runbot(Controller):
level = ['info', 'warning', 'danger'][int(pending_count > warn) + int(pending_count > crit)]
return pending_count, level, scheduled_count, pending_assigned_count
@o_route([
'/runbot/submit'
], type='http', auth="public", methods=['GET', 'POST'], csrf=False)
def submit(self, more=False, redirect='/', keep_search=False, category=False, filter_mode=False, update_triggers=False, **kwargs):
assert redirect.startswith('/')
response = werkzeug.utils.redirect(redirect)
response.set_cookie('more', '1' if more else '0')
if update_triggers:
enabled_triggers = []
project_id = int(update_triggers)
for key in kwargs.keys():
if key.startswith('trigger_'):
enabled_triggers.append(key.replace('trigger_', ''))
key = 'trigger_display_%s' % project_id
if len(request.env['runbot.trigger'].search([('project_id', '=', project_id)])) == len(enabled_triggers):
response.delete_cookie(key)
else:
response.set_cookie(key, '-'.join(enabled_triggers))
return response
@route(['/',
'/runbot',
'/runbot/<model("runbot.project"):project>',
@ -114,15 +135,10 @@ class Runbot(Controller):
if not project and projects:
project = projects[0]
pending_count, level, scheduled_count, pending_assigned_count = self._pending()
context = {
'categories': categories,
'search': search,
'message': request.env['ir.config_parameter'].sudo().get_param('runbot.runbot_message'),
'pending_count': pending_count,
'pending_assigned_count': pending_assigned_count,
'pending_level': level,
'scheduled_count': scheduled_count,
'toolbar': self._get_default_toolbar(include_message=True),
'hosts_data': request.env['runbot.host'].search([('assigned_only', '=', False)]),
}
if project:
@ -185,7 +201,6 @@ class Runbot(Controller):
'search': search,
})
context.update({'message': request.env['ir.config_parameter'].sudo().get_param('runbot.runbot_message')})
# request.is_frontend = False # remove inherit branding
return request.render('runbot.bundles', context)
@ -201,13 +216,18 @@ class Runbot(Controller):
raise NotFound
slug = request.env['ir.http']._slug
return werkzeug.utils.redirect(f'/runbot/bundle/{slug(bundle)}')
if isinstance(limit, str):
limit = int(limit)
domain = [('bundle_id', '=', bundle.id), ('hidden', '=', False)]
batch_count = request.env['runbot.batch'].search_count(domain)
pager = request.website.pager(
url='/runbot/bundle/%s' % bundle.id,
total=batch_count,
page=page,
step=50,
step=limit,
url_args={
'limit': limit,
}
)
batchs = request.env['runbot.batch'].search(domain, limit=limit, offset=pager.get('offset', 0), order='id desc')
@ -218,6 +238,14 @@ class Runbot(Controller):
'project': bundle.project_id,
'title': 'Bundle %s' % bundle.name,
'page_info_state': bundle.last_batch._get_global_result(),
'toolbar': ToolbarContext(
breadcrumbs=[
Breadcrumb('/runbot/%s' % request.env['ir.http']._slug(bundle.project_id), bundle.project_id.display_name),
Breadcrumb('/runbot/bundle/%s' % bundle.id, bundle.display_name),
],
middle_template='runbot.bundle_toolbar_middle_section',
end_template='runbot.pager',
)
}
return request.render('runbot.bundle', context)
@ -239,11 +267,21 @@ class Runbot(Controller):
@route(['/runbot/batch/<int:batch_id>'], website=True, auth='public', type='http', sitemap=False)
def batch(self, batch_id=None, **kwargs):
batch = request.env['runbot.batch'].browse(batch_id)
bundle = batch.bundle_id
project = bundle.project_id
context = {
'batch': batch,
'project': batch.bundle_id.project_id,
'project': project,
'title': 'Batch %s (%s)' % (batch.id, batch.bundle_id.name),
'page_info_state': batch._get_global_result(),
'toolbar': ToolbarContext(
breadcrumbs=[
Breadcrumb('/runbot/%s' % request.env['ir.http']._slug(project), project.display_name),
Breadcrumb('/runbot/bundle/%s' % bundle.id, bundle.display_name),
Breadcrumb('/runbot/bundle/batch/%s' % batch.id, batch.display_name),
],
middle_template='runbot.batch_toolbar_middle_section',
),
}
return request.render('runbot.batch', context)
@ -275,7 +313,7 @@ class Runbot(Controller):
'reflogs': request.env['runbot.ref.log'].search([('commit_id', '=', commit.id)]),
'status_list': status_list,
'last_status_by_context': last_status_by_context,
'title': 'Commit %s' % commit.name[:8]
'title': 'Commit %s' % commit.name[:8],
}
return request.render('runbot.commit', context)
@ -329,10 +367,34 @@ class Runbot(Controller):
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')
project = build.params_id.trigger_id.project_id
breadcrumbs: Breadcrumbs = [Breadcrumb('/runbot/%s' % request.env['ir.http']._slug(project), project.display_name)]
batch = bundle = None
if from_batch:
batch = from_batch
batches = batch
bundle = batch.bundle_id
bundles = bundle
else:
batches = build.top_parent.with_context(active_test=False).slot_ids.batch_id
bundles = batches.bundle_id
if len(batches) == 1:
batch = batches
if len(bundles) == 1:
bundle = bundles
if bundle:
breadcrumbs.append(Breadcrumb(bundle._url(), bundle.display_name))
if batch:
breadcrumbs.append(Breadcrumb(batch._url(), batch.display_name))
breadcrumbs.extend(
Breadcrumb(ancestor.build_url, ancestor.description or ancestor.config_id.name) for ancestor in build.ancestors
)
context = {
'build': build,
'batches': batches,
'bundles': bundles,
'from_batch': from_batch,
'project': build.params_id.trigger_id.project_id,
'project': project,
'title': 'Build %s' % build.id,
'siblings': siblings,
'page_info_state': build.global_result,
@ -341,8 +403,13 @@ class Runbot(Controller):
'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),
'toolbar': ToolbarContext(
breadcrumbs=breadcrumbs,
middle_template='runbot.build_toolbar_middle_section',
end_template='runbot.build_toolbar_end_section',
)
}
return request.render("runbot.build", context)
return request.render('runbot.build', context)
@route([
'/runbot/build/search',

View File

@ -1269,3 +1269,22 @@ class BuildResult(models.Model):
def _parse_config(self):
return set(findall(self._server("tools/config.py"), r'--[\w-]+', ))
def _get_view_class(self):
"""
Returns the color class to use according to bootstrap (+ killed).
"""
self.ensure_one()
if self.global_state in ('running', 'done'):
if self.global_result == 'ok':
return 'success'
elif self.global_result == 'skipped':
return 'skipped'
elif self.global_result in ('killed', 'manually_killed'):
return 'killed'
if self.global_result == 'ko':
return 'danger'
elif self.global_result == 'warn':
return 'warning'
return 'info'

View File

@ -0,0 +1,3 @@
// New scss rules to be merged and cleaned up with runbot.css once ready.

View File

@ -1,174 +1,209 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="runbot.batch">
<t t-call="runbot.layout">
<div class="row g-0">
<div class="col-lg-6">
<t t-set="batch_class" t-value="'bg-info-subtle' if batch.state=='preparing' else 'bg-success-subtle' if not any(log.level != 'INFO' for log in batch.log_ids) else 'bg-warning-subtle'"/>
<table class="table table-stripped">
<tr>
<td>Bundle</td>
<td>
<a t-out="batch.bundle_id.name" t-attf-href="/runbot/bundle/{{batch.bundle_id.id}}"/>
&amp;emsp;
<a groups="runbot.group_runbot_advanced_user"
t-attf-href="/odoo/runbot.batch/{{batch.id}}?menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}" class="btn btn-default btn-sm" target="new" title="View Batch in Backend">
<i class="fa fa-list"/>
</a>
</td>
</tr>
<tr t-if="batch.category_id.id != default_category">
<td>Category</td>
<td t-out="batch.category_id.name"></td>
</tr>
<tr>
<td>Version</td>
<td t-out="batch.slot_ids[0].params_id.version_id.name if batch.slot_ids else batch.bundle_id.version_id.name"/>
</tr>
<tr>
<td>Create date</td>
<td t-out="batch.create_date"/>
</tr>
<tr t-if="more">
<td>Last update</td>
<td>
<t t-out="batch.last_update"/>
<span class="badge text-bg-info" t-out="s2human(batch.last_update - batch.create_date)"/>
</td>
</tr>
<tr t-if="more and batch.reference_batch_ids">
<td>Version reference batches (for upgrade)</td>
<td>
<t t-foreach="batch.reference_batch_ids" t-as="reference_batch"/>
<div><a t-attf-href="/runbot/batch/{{reference_batch.id}}"><t t-out="reference_batch.bundle_id.version_id.name"/> (<t t-out="reference_batch.id"/>)</a></div>
</td>
</tr>
<tr>
<td t-att-class="batch_class" >Commits</td>
<td t-att-class="batch_class" >
<div t-foreach="batch.commit_link_ids.sorted(key=lambda lc: (lc.commit_id.repo_id.sequence, lc.commit_id.repo_id.id))" t-as="commit_link">
<t t-set="commit" t-value="commit_link.commit_id"/>
<span/>
<a t-attf-href="/runbot/commit/#{commit.id}">
<i class="fa fa-fw fa-hashtag" t-if="commit_link.match_type == 'new'" title="This commit is a new head"/>
<i class="fa fa-fw fa-link" t-if="commit_link.match_type == 'head'" title="This commit is an existing head from bundle branches"/>
<i class="fa fa-fw fa-code-fork" t-if="commit_link.match_type == 'base_match'" title="This commit is matched from a base batch with matching merge_base"/>
<i class="fa fa-fw fa-clock-o" t-if="commit_link.match_type == 'base_head'" title="This commit is the head of a base branch"/>
<span class="label" t-out="commit.dname"/>
</a>
<a t-att-href="'https://%s/commit/%s' % (commit_link.branch_id.remote_id.base_url, commit_link.commit_id.name)" title="View Commit on Github"><i class="fa fa-github"/></a>
<small t-if="commit_link.match_type and commit_link.match_type.startswith('base')">
from base:
<span t-out="commit_link.branch_id.name"/>
<br/>
</small>
<small t-else="">
found in branch
<span t-out="commit_link.branch_id.name"/>
<t t-if="batch.state != 'preparing'">
<span t-out="'+%s' % commit_link.diff_add" class="text-success"/>
<span t-out="'-%s' % commit_link.diff_remove" class="text-danger"/>
<span class="text-info">
(
<span t-out="commit_link.file_changed"/>
<i class="fa fa-file"/>
)
<span io="behind">(
<span t-out="'%s ahead' % commit_link.base_ahead" class="text-success"/>
,
<span t-out="'%s behind' % commit_link.base_behind" class="text-danger"/>
)</span>
</span>
</t>
<br/>
<t t-if="more">
Base head:
<span t-out="commit_link.base_commit_id.name"/>
<br/>
Merge base:
<span t-out="commit_link.merge_base_commit_id.name"/>
<br/>
</t>
</small>
<b t-if="commit.rebase_on_id">Automatic rebase on <t t-out="commit.rebase_on_id.name"/><br/></b>
<t t-if="more or not (commit_link.match_type and commit_link.match_type.startswith('base'))">
Subject:
<span t-out="commit.subject"/>
<br/>
Author:
<span t-out="commit.author"/>
(
<span t-out="commit.author_email"/>
)
<br/>
<t t-if="commit.author != commit.committer">
Committer:
<span t-out="commit.committer"/>
(
<span t-out="commit.committer_email"/>
)
<br/>
</t>
Commit date:
<span t-out="commit.date"/>
<br/>
</t>
<hr/>
</div>
</td>
</tr>
</table>
</div>
<div class="col-lg-6">
<table class="table table-stripped">
<tr>
<td>Builds</td>
<td>
<t t-foreach="batch.slot_ids.filtered(lambda s: not s.trigger_id.manual)" t-as="slot">
<t t-call="runbot.slot_button"/>
</t>
</td>
</tr>
<tr>
<td>Manual</td>
<td>
<t t-foreach="batch.slot_ids.filtered(lambda s: s.trigger_id.manual)" t-as="slot">
<t t-if="slot.build_id or (not slot.trigger_id.team_ids) or (user_id in slot.trigger_id.team_ids.user_ids)">
<t t-call="runbot.slot_button"/>
</t>
</t>
</td>
</tr>
<tr t-if="more">
<td>Old builds</td>
<td>
<t t-foreach="batch.with_context(active_test=False).slot_ids.filtered(lambda s: not s.active)" t-as="slot">
<s>
<t t-call="runbot.slot_button"/>
</s>
</t>
</td>
</tr>
</table>
</div>
</div>
<t t-foreach="batch.log_ids" t-as="log">
<t t-set="logclass" t-value="dict(ERROR='danger', WARNING='warning', INFO='info').get(log.level, 'warning')"/>
<div t-attf-class="alert alert-{{logclass}}">
<b t-out="log.level"/>
--
<t t-out="log._markdown()"/>
</div>
</t>
<template id="runbot.batch_card">
<t t-set="klass">info</t>
<t t-if="batch.state=='skipped'" t-set="klass">killed</t>
<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="o_runbot_batch_card card w-100 h-100">
<a t-attf-class="card-header px-2 d-flex justify-content-between alig-items-center text-bg-{{klass}}"
t-attf-href="/runbot/batch/#{batch.id}" title="View Batch"
>
<span class="lh-sm">Batch #<t t-out="batch.id" /></span>
<span t-attf-class="badge text-bg-{{'warning' if batch.has_warning else 'default'}}">
<t t-out="batch._get_formated_age()" />
<i t-if="batch.has_warning" class="fa fa-exclamation-triangle" />
</span>
</a>
<div class="o_runbot_batch_card_content d-flex flex-column justify-content-between h-100">
<div class="o_runbot_batch_card_slots card-body row g-1 p-1 flex-grow-0">
<div t-if="batch.state == 'preparing'" class="col-12 btn-group btn-group-ssm">
<span class="btn btn-default disabled" titled="Preparing">
<i class="fa fa-cog fa-spin fa-fw" /> Preparing </span>
</div>
<t t-foreach="batch.slot_ids" t-as="slot">
<t t-if="slot.build_id">
<div class="o_runbot_batch_card_slot col-auto"
t-if="((not slot.trigger_id.hide and trigger_display is None) or (trigger_display and slot.trigger_id.id in trigger_display)) or slot.build_id.global_result != 'ok'">
<t t-call="runbot.slot_btn_group" />
</div>
</t>
</template>
</t>
</div>
<div class="o_runbot_batch_card_commits row g-0 card-footer p-1" t-if="more">
<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="col-12 text-truncate"
>
<t t-set="match_class" t-value="'info' if commit_link.match_type == 'new' else 'secondary'" />
<a
title="View Commit on Github"
t-att-href="'https://%s/commit/%s' % (commit_link.branch_id.remote_id.base_url, commit_link.commit_id.name)"
t-attf-class="badge text-bg-{{match_class}}"
>
<i class="fa fa-github" />
</a>
<a t-attf-href="/runbot/commit/#{commit_link.commit_id.id}" t-attf-class="badge text-bg-{{match_class}}">
<i class="fa fa-fw fa-hashtag" t-if="commit_link.match_type == 'new'" title="This commit is a new head" />
<i class="fa fa-fw fa-link" t-if="commit_link.match_type == 'head'" title="This commit is an existing head from bundle branches" />
<i class="fa fa-fw fa-code-fork" t-if="commit_link.match_type == 'base_match'"
title="This commit is matched from a base batch with matching merge_base" />
<i class="fa fa-fw fa-clock-o" t-if="commit_link.match_type == 'base_head'" title="This commit is the head of a base branch" />
<t t-out="commit_link.commit_id.dname" />
</a>
<span class="font-monospace text-nowrap text-truncate overflow-hidden" t-att-title="commit_link.commit_id.subject"
t-out="commit_link.commit_id.subject" />
</div>
</div>
</div>
</div>
</template>
<template id="runbot.trigger_report">
<t t-call="runbot.layout">
<t t-call="{{trigger.report_view_id.id}}">
</t>
<template id="runbot.batch_commit_link_cell">
<t t-set="commit" t-value="commit_link.commit_id" />
<div class="row">
<div>
<a t-attf-href="/runbot/commit/#{commit.id}">
<i class="fa fa-fw fa-hashtag" t-if="commit_link.match_type == 'new'" title="This commit is a new head" />
<i class="fa fa-fw fa-link" t-if="commit_link.match_type == 'head'" title="This commit is an existing head from bundle branches" />
<i class="fa fa-fw fa-code-fork" t-if="commit_link.match_type == 'base_match'"
title="This commit is matched from a base batch with matching merge_base" />
<i class="fa fa-fw fa-clock-o" t-if="commit_link.match_type == 'base_head'" title="This commit is the head of a base branch" />
<span class="label" t-out="commit.dname" />
</a>
<a t-att-href="'https://%s/commit/%s' % (commit_link.branch_id.remote_id.base_url, commit_link.commit_id.name)" title="View Commit on Github">
<i class="fa fa-github" />
</a>
<small>
<t t-if="commit_link.match_type and commit_link.match_type.startswith('base')"> from base: <t t-out="commit_link.branch_id.name" />
</t>
<t t-else=""> found in branch <t t-out="commit_link.branch_id.name" />
<t t-if="batch.state != 'preparing'">
<span t-out="'+%s' % commit_link.diff_add" class="text-success" />
<span t-out="'-%s' % commit_link.diff_remove" class="text-danger" />
<span class="text-info"> ( <t t-out="commit_link.file_changed" />
<i class="fa fa-file" /> ) <span>( <span
t-out="'%s ahead' % commit_link.base_ahead" class="text-success" /> , <span t-out="'%s behind' % commit_link.base_behind"
class="text-danger" /> )</span>
</span>
</t>
</template>
</data>
</t>
</small>
</div>
<small class="d-block">Base head: <t t-out="commit_link.base_commit_id.name" /></small>
<small class="d-block">Merge head: <t t-out="commit_link.merge_base_commit_id.name" /></small>
<b t-if="commit.rebase_on_id">Automatic rebase on <t t-out="commit.rebase_on_id.name" /><br /></b>
<t t-if="more or not (commit_link.match_type and commit_link.match_type.startswith('base'))">
<span class="d-block"> Subject: <t t-out="commit.subject" />
</span>
<span class="d-block"> Author: <t t-out="commit.author" /> (<t t-out="commit.author_email" />) </span>
<span class="d-block" t-if="commit.author != commit.committer"> Committer: <t t-out="commit.committer" /> (<t t-out="commit.committer_email" />) </span>
<span class="d-block"> Commit date: <t t-out="commit.date" />
</span>
</t>
</div>
</template>
<template id="runbot.batch_toolbar_middle_section">
<div class="btn-group btn-group-sm" role="group" groups="runbot.group_runbot_advanced_user">
<a
t-attf-href="/web/#id={{batch.id}}&amp;view_type=form&amp;model=runbot.batch&amp;menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}"
class="btn btn-default" target="new" title="View Batch in Backend">
<i class="fa fa-list" />
</a>
</div>
</template>
<template id="runbot.batch">
<t t-call="runbot.layout">
<div class="o_runbot_batch row">
<div class="o_runbot_batch_details col-12 col-lg-6">
<t t-set="batch_class"
t-value="'bg-info-subtle' if batch.state=='preparing' else 'bg-success-subtle' if not any(log.level != 'INFO' for log in batch.log_ids) else 'bg-warning-subtle'" />
<div class="card mb-2">
<div class="card-header px-2 py-1">Batch #<t t-out="batch.id" /></div>
<div class="o_runbot_grid o_runbot_grid_2">
<div>Bundle</div>
<div>
<a t-out="batch.bundle_id.name" t-attf-href="/runbot/bundle/{{batch.bundle_id.id}}" />
</div>
<t t-if="batch.category_id.id != default_category">
<div>Category</div>
<div t-out="batch.category_id.name"></div>
</t>
<div>Version</div>
<div t-out="batch.slot_ids[0].params_id.version_id.name if batch.slot_ids else batch.bundle_id.version_id.name" />
<div>Create date</div>
<div t-out="batch.create_date" />
<t t-if="more">
<div>Last update</div>
<div>
<t t-out="batch.last_update" />
<span class="badge text-bg-info pull-right" t-out="s2human_long(batch.last_update - batch.create_date)" />
</div>
</t>
<t t-if="more and batch.reference_batch_ids">
<div>Version reference batches (for upgrade)</div>
<div>
<t t-foreach="batch.reference_batch_ids" t-as="reference_batch" />
<div>
<a t-attf-href="/runbot/batch/{{reference_batch.id}}"><t t-out="reference_batch.bundle_id.version_id.name" /> (<t t-out="reference_batch.id" />
)</a>
</div>
</div>
</t>
<t t-set="commit_links" t-value="batch.commit_link_ids.sorted(key=lambda lc: (lc.commit_id.repo_id.sequence, lc.commit_id.repo_id.id))" />
<div t-att-class="batch_class">Commits</div>
<div class="p-0">
<ul class="list-group list-group-flush">
<li t-foreach="commit_links" t-as="commit_link" t-attf-class="list-group-item p-1 {{batch_class}}">
<t t-call="runbot.batch_commit_link_cell" />
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="o_runbot_batch_slots col-12 col-lg-6">
<div class="card mb-2">
<div class="card-header px-2 py-1">Builds</div>
<div class="o_runbot_grid o_runbot_grid_2">
<div>Automatic</div>
<div>
<div class="row g-1">
<t t-foreach="batch.slot_ids.filtered(lambda s: not s.trigger_id.manual)" t-as="slot" t-call="runbot.slot_btn_group" />
</div>
</div>
<div>Manual</div>
<div>
<div class="row g-1">
<t t-foreach="batch.slot_ids.filtered(lambda s: s.trigger_id.manual)" t-as="slot">
<t t-if="slot.build_id or (not slot.trigger_id.team_ids) or (user_id in slot.trigger_id.team_ids.user_ids)">
<t t-call="runbot.slot_btn_group" />
</t>
</t>
</div>
</div>
<div>Old builds</div>
<div>
<div class="row g-1 text-decoration-line-through">
<t t-foreach="batch.with_context(active_test=False).slot_ids.filtered(lambda s: not s.active)" t-as="slot">
<t t-call="runbot.slot_btn_group" />
</t>
</div>
</div>
</div>
</div>
</div>
<div class="o_runbot_batch_logs col-12">
<t t-foreach="batch.log_ids" t-as="log">
<t t-set="logclass" t-value="dict(ERROR='danger', WARNING='warning', INFO='info').get(log.level, 'warning')" />
<div t-attf-class="alert alert-{{logclass}}">
<b t-out="log.level" /> -- <t t-out="log._markdown()" />
</div>
</t>
</div>
</div>
</t>
</template>
</odoo>

View File

@ -1,412 +1,337 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="runbot.build">
<t t-call='runbot.layout'>
<t t-set="nav_form">
<form class="form-inline">
<div class="btn-group">
<t t-call="runbot.build_button">
<t t-set="bu" t-value="build"/>
<t t-set="klass" t-value="''"/>
<t t-set="show_commit_button" t-value="True"/>
</t>
<template id="runbot.build_buttons">
<a t-if="build.local_state == 'running' and build.database_ids" t-attf-href="/runbot/run/{{build.id}}" class="btn btn-info o_runbot_slot_btn_small"
title="Sign in on this build" aria-label="Sign in on this build">
<i class="fa fa-sign-in" />
</a>
<a t-if="build.static_run" t-att-href="build.static_run" class="btn btn-info o_runbot_slot_btn_small" title="View result" aria-label="View result">
<i class="fa fa-sign-in" />
</a>
<a groups="base.group_user"
t-if="build.local_state=='done' and (not build.parent_id.database_ids or user_id.has_group('runbot.group_runbot_advanced_user')) and build.requested_action != 'wake_up' and build.database_ids"
href="#" data-runbot="wakeup" t-att-data-runbot-build="build.id" class="btn btn-default o_runbot_slot_btn_small" title="Wake up this build"
aria-label="Wake up this build">
<i class="fa fa-coffee" />
</a>
<a t-attf-href="/runbot/build/{{build.id}}" class="btn btn-default o_runbot_slot_btn_small" title="Build details" aria-label="Build details">
<i class="fa fa-file-text-o" />
</a>
<t t-call="runbot.build_menu">
<t t-set="bu" t-value="build" />
</t>
</template>
<template id="runbot.build_toolbar_middle_section">
<div class="btn-group btn-group-sm">
<t t-call="runbot.build_buttons" />
</div>
</template>
<template id="runbot.build_toolbar_end_section">
<div class="btn-group btn-group-sm">
<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>
</div>
</template>
<template id="runbot.build">
<t t-call="runbot.layout">
<div class="o_runbot_build row gy-2">
<t t-set="rowclass" t-value="build._get_view_class()" />
<div t-attf-class="o_runbot_build_information col-12 col-md-{{'6' if build.children_ids else '12'}}">
<div t-attf-class="card h-100">
<div class="card-header px-2 py-1">
Build
</div>
</form>
</t>
<div class="row g-0">
<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="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-out="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-out="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-out="unique_batch.id"/> (<t t-out="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-out="ancestor.description or ancestor.config_id.name"/>
</a>
</li>
</ol>
<span class="btn-group pe-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()}}-subtle {{'col-md-6' if build.children_ids else 'col-md-12'}}">
<div class="build_details">
<div t-attf-class="card-body bg-{{rowclass}}-subtle p-2 overflow-auto text-nowrap">
<!-- Batch/bundles links-->
<t t-if="len(bundles) > 1">
This build is referenced in <t t-out="len(bundles)"/> bundles
<ul>
<li t-foreach="bundles" t-as="bundle" ><a t-out="bundle.name" t-attf-href="/runbot/bundle/{{bundle.id}}"/></li>
<div t-if="len(bundles) > 1"> This build is referenced in <t t-out="len(bundles)" /> bundles <ul>
<li t-foreach="bundles" t-as="bundle">
<a t-out="bundle.name" t-attf-href="/runbot/bundle/{{bundle.id}}" />
</li>
</ul>
</t>
</div>
<t t-if="len(batches) > 1">
<b>First apparition:</b> <a t-out="batches[0].bundle_id.name" t-attf-href="/runbot/batch/{{batches[0].id}}"/><br/>
<b>Last apparition:</b> <a t-out="batches[-1].bundle_id.name" t-attf-href="/runbot/batch/{{batches[-1].id}}"/><br/>
<div>
<b>First apparition:</b>
<a t-out="batches[0].bundle_id.name" t-att-href="batches[0]._url()" />
<br />
</div>
<div>
<b>Last apparition:</b>
<a t-out="batches[-1].bundle_id.name" t-att-href="batches[-1]._url()" />
<br />
</div>
</t>
<!-- 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>
<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-out="build.md_description"/>
<br/>
<t t-out="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-out="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>
<b>Commit:</b>
<a t-attf-href="/runbot/commit/{{build_commit.commit_id.id}}">
<t t-out="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>
</div>
<div t-if="build_commit.match_type in ('default', 'pr_target', 'prefix')">
<div>from base branch</div>
</div>
<div t-else="" class="ms-3">
<b>Subject:</b>
<t t-out="build_commit.commit_id.subject"/>
<br/>
<b>Author:</b>
<t t-out="build_commit.commit_id.author"/>
<br/>
<b>Committer:</b>
<t t-out="build_commit.commit_id.committer"/>
<br/>
<div>
<b>Subject:</b>
<span class="font-monospace" t-out="build_commit.commit_id.subject" />
</div>
<div>
<b>Author:</b>
<t t-out="build_commit.commit_id.author" />
</div>
<div>
<b>Committer:</b>
<t t-out="build_commit.commit_id.committer" />
</div>
</div>
</t>
<b>Version:</b>
<t t-out="build.params_id.version_id.name"/>
<br/>
<b>Config:</b>
<t t-out="build.params_id.config_id.name"/>
<br/>
<t t-if='more'>
<b>Trigger:</b>
<t t-out="build.params_id.trigger_id.name"/>
<br/>
<b>Config data:</b>
<t t-out="build.params_id.config_data.dict"/>
<br/>
<b>Modules:</b>
<t t-out="build.params_id.modules"/>
<br/>
<b>Extra params:</b>
<t t-out="build.params_id.extra_params"/>
<br/>
<t t-if="len(build.params_id.builds_reference_ids) > 1">
<div>
<b>Version:</b>
<t t-out="build.params_id.version_id.name" />
</div>
<div>
<b>Config:</b>
<t t-out="build.params_id.config_id.name" />
</div>
<t t-if="more">
<div>
<b>Trigger:</b>
<t t-out="build.params_id.trigger_id.name" />
</div>
<div>
<b>Config data:</b>
<span class="font-monospace" t-out="build.params_id.config_data.dict" />
</div>
<div>
<b>Modules:</b>
<t t-out="build.params_id.modules" />
</div>
<div>
<b>Extra params:</b>
<span class="font-monospace" t-out="build.params_id.extra_params" />
</div>
<div t-if="len(build.params_id.builds_reference_ids) > 1">
<b>Reference builds:</b>
<t t-foreach="build.params_id.builds_reference_ids" t-as="reference">
<span t-out="reference.id"/>
</t>
<br/>
</t>
<t t-if="len(build.params_id.build_ids) > 1">
<span t-foreach="build.params_id.builds_reference_ids" t-as="reference" t-out="reference.id" />
</div>
<div 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 text-bg-{{simbuild._get_color_class()}}"
t-out="simbuild.id"/>
t-out="simbuild.id" />
</a>
</t>
<br/>
</t>
<b>Host:</b>
<t t-out="build.host"/>
</div>
<div>
<b>Host:</b>
<t t-out="build.host" />
</div>
</t>
<div>
<b title="Execution time of this build, without child time">
Build time:
</b>
<t t-att-tile='build.build_time' t-out="s2human(build.build_time)"/>
<i t-if='more'>(<t t-out="build.build_time"/>s)</i>
<t t-att-tile='build.build_time' t-out="s2human(build.build_time)" />
<i t-if='more'>(<t t-out="build.build_time" />s)</i>
</div>
<div>
<b title='Time from creation to finish (queue time + completion time)'>
Wait time:
</b>
<t t-att-tile='build.wait_time' t-out="s2human(build.wait_time)"/>
<i t-if='more'>(<t t-out="build.wait_time"/>s)</i>
<t t-att-tile='build.wait_time' t-out="s2human(build.wait_time)" />
<i t-if='more'>(<t t-out="build.wait_time" />s)</i>
</div>
<div>
<b title='Total time '>
Load time:
</b>
<t t-att-tile='build.load_time' t-out="s2human(build.load_time)"/>
<i t-if='more'>(<t t-out="build.load_time"/>s)</i>
<t t-att-tile='build.load_time' t-out="s2human(build.load_time)" />
<i t-if='more'>(<t t-out="build.load_time" />s)</i>
</div>
<div>
<t t-if="build.stat_ids">
<b>Stats:</b>
<a t-attf-href="/runbot/build/stats/{{build.id}}">Build <t t-out="build.id"/></a>
<br/>
</t>
<div t-if="build.stat_ids">
<b>Stats: <a t-attf-href="/runbot/build/stats/{{build.id}}">Build <t t-out="build.id" /></a></b>
</div>
</div>
</div>
<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="child"/>
</div>
<div class="o_runbot_build_child_information col-12 col-md-6" t-if="build.children_ids">
<div class="card">
<div t-attf-class="card-header px-2 py-1">
Children
</div>
<ul class="list-group list-group-flush">
<li t-foreach="build.children_ids" t-as="child"
t-attf-class="list-group-item px-2 py-1 bg-{{child._get_view_class()}}{{' text-decoration-line-through' if child.orphan_result else ''}}">
<div class="btn-group btn-group-ssm d-flex flex-grow-1">
<a t-attf-href="/runbot/{{'batch/%s/' % from_batch.id if from_batch else ''}}build/{{child.id}}"
t-attf-class="btn btn-default flex-grow-1 text-start text-truncate bg-{{child._get_view_class()}}-subtle">
<div class="d-flex flex-row align-items-center gap-2">
<div> Build <t t-out="child.id" />
</div>
<div>
<div>
<t t-if="child.description" t-out="child.md_description" />
<t t-else=""> with config <t t-out="child.params_id.config_id.name" />
</t>
<i t-if="child.orphan_result" class="fa fa-chain-broken" title="Build result ignored for parent" />
</div>
<div t-if="child.job">
<small>Running step: <t t-out="child.job" /></small>
</div>
</div>
<div t-if="child.global_state in ('testing', 'waiting')">
<i class="fa fa-spinner fa-spin" />
<t t-out="child.global_state" />
</div>
</div>
</a>
<t t-call="runbot.build_buttons">
<t t-set="build" t-value="child" />
</t>
</t>
<tr t-attf-class="bg-{{rowclass.strip()}}-subtle{{' 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-out="child.id"/>
</a>
<t t-if="child.description">
<t t-out="child.md_description" />
</t>
<t t-else="">
with config
<t t-out="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-out="child.job"/>
</t>
<t t-if="child.global_state in ['testing', 'waiting']">
<i class="fa fa-spinner fa-spin"/>
<t t-out="child.global_state"/>
</t>
</td>
<td>
<span t-attf-class="badge text-bg-info" t-out="s2human(child.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>
</div>
<t t-set="nb_subbuild" t-value="len(build.children_ids)"/>
<div class="col-md-12">
<table class="table table-condensed">
<tr>
<th>Date</th>
<th>Level</th>
<th>Type</th>
<th>Message</th>
</tr>
<t t-set="commit_link_per_name" t-value="{commit_link.commit_id.repo_id.name:commit_link for commit_link in build.params_id.commit_link_ids}"/>
<t t-foreach="build.sudo().log_ids" t-as="l">
<t t-set="subbuild" t-value="(([child for child in build.children_ids if child.id == int(l.path)] if l.type == 'subbuild' else False) or [build.browse()])[0]"/>
<t t-set="logclass" t-value="dict(CRITICAL='danger', ERROR='danger', WARNING='warning', OK='success', SEPARATOR='separator').get(l.level)"/>
<tr t-att-class="'separator' if logclass == 'separator' else ''" t-att-title="l.active_step_id.description or ''">
<td style="white-space: nowrap; width:1%;">
<t t-out="l.create_date.strftime('%Y-%m-%d %H:%M:%S')"/>
</td>
<td style="white-space: nowrap; width:1%;">
<b t-if="l.level != 'SEPARATOR' and l.type not in ['link', 'markdown']" t-out="l.level"/>
</td>
<td style="white-space: nowrap; width:1%;">
<t t-if="l.level != 'SEPARATOR' and l.type not in ['link', 'markdown']" t-out="l.type"/>
</td>
<t t-set="message_class" t-value="''"/>
<t t-if="subbuild" t-set="message_class">
<t t-call="runbot.build_class">
<t t-set="build" t-value="subbuild"/>
</t>
</t>
<td t-attf-class="bg-{{message_class.strip() or logclass}}-subtle">
<t t-if="l.type not in ('runbot', 'link', 'markdown')">
<t t-if="l.type == 'subbuild'">
<a t-attf-href="/runbot/build/{{l.path}}">
Build #
<t t-out="l.path"/>
</a>
</t>
<t t-else="">
<t t-set="repo_name" t-value="l.path.replace('/data/build/', '').split('/')[0] "/>
<t t-set="href" t-value=""/>
<t t-if="repo_name in commit_link_per_name">
<t t-set="repo_base_url" t-value="commit_link_per_name[repo_name].branch_id.remote_id.base_url if repo_name in commit_link_per_name else ''"/>
<t t-set="commit_hash" t-value="commit_link_per_name[repo_name].commit_id.name if repo_name in commit_link_per_name else ''"/>
<t t-set="path" t-value="l.path.replace('/data/build/%s/' % repo_name, '')"/>
<t t-set="href" t-value="'https://%s/blob/%s/%s#L%s' % (repo_base_url, commit_hash, path, l.line)"/>
</t>
<a t-att-href="href" t-attf-title="Func: {{l.func}}"><t t-out="l.name"/>:<t t-out="l.line"/></a>
</t>
</t>
<!-- DEPRECATED: Will be removed once no ir.logging is concerned. -->
<span class="log_message" t-if="l.type == 'link' and len(l.message.split('$$')) == 3">
<t t-set="message" t-value="l.message.split('$$')"/>
<t t-if="message[1].startswith('fa-')">
<t t-out="message[0]"/>
<a t-attf-href="{{l.path}}">
<i t-attf-class="fa {{message[1]}}"/>
</a>
<t t-out="message[2]"/>
</t>
<t t-else="">
<t t-out="message[0]"/>
<a t-attf-href="{{l.path}}">
<t t-out="message[1]"/>
</a>
<t t-out="message[2]"/>
</t>
</span>
<span class="log_message" t-elif="l.type == 'markdown'" t-out="l._markdown()"/>
<span class="log_message" t-else="">
<t t-if="'\n' not in l.message" t-out="l.message"/>
<pre t-if="'\n' in l.message" style="margin:0;padding:0; border: none;"><t t-out="l.message"/></pre>
</span>
</td>
<td t-attf-class="bg-{{message_class.strip() or logclass}}-subtle">
<t t-if="l.level in ('CRITICAL', 'ERROR', 'WARNING') and not l.with_context(active_test=False).error_content_id">
<small>
<a groups="runbot.group_runbot_5admin" t-attf-href="/runbot/parse_log/{{l.id}}" class="sm" title="Parse this log line to follow this error.">
<i t-attf-class="fa fa-magic"/>
</a>
</small>
</t>
</td>
</tr>
<t t-if="l.error_content_id">
<t t-set="error_content" t-value="l.error_content_id"/>
<t t-set="error" t-value="error_content.error_id"/>
<tr>
<td/><td/><td/>
<td t-attf-class="bg-{{'info' if error.active else 'success'}}-subtle" colspan="2">
This error is already <em t-attf-title="{{'Was detected by runbot in nightly builds.' if error.active else 'Either the error is not properly fixed or the branch does not contain the fix.'}}"><t t-out="'known' if error.active else 'fixed'"/></em>.
<!--a groups="runbot.group_user" t-attf-href="/web#id={{error_content.id}}&amp;view_type=form&amp;model=runbot.build.error.content&amp;menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}" title="View in Backend" target="new">
<i t-attf-class="fa fa-search"/>
</a-->
<a groups="runbot.group_user" t-attf-href="/web#id={{error.id}}&amp;view_type=form&amp;model=runbot.build.error&amp;menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}" title="View in Backend" target="new">
<i t-attf-class="fa fa-list"/>
</a>
<span groups="runbot.group_runbot_admin" t-if="error.responsible or error.responsible.id == uid">(<i t-out="error.responsible.name"/>)</span>
</td>
</tr>
</t>
</t>
</table>
</div>
</li>
</ul>
</div>
</div>
</t>
</template>
<template id="runbot.build_search">
<t t-call='runbot.layout'>
<div class="row g-0">
<div class="col-md-12">
<table class="table table-condensed">
<t t-foreach="builds" t-as="build">
<t t-set="rowclass">
<t t-call="runbot.build_class">
<t t-set="build" t-value="build"/>
</div>
<div class="o_runbot_logs row">
<div class="o_runbot_log_grid gx-0">
<div class="o_runbot_log_grid_header o_runbot_log_grid_date">Date</div>
<div class="o_runbot_log_grid_header">Level</div>
<div class="o_runbot_log_grid_header">Type</div>
<div class="o_runbot_log_grid_header o_runbot_log_grid_message">Message</div>
<t t-set="commit_link_per_name" t-value="{commit_link.commit_id.repo_id.name:commit_link for commit_link in build.params_id.commit_link_ids}" />
<t t-foreach="build.sudo().log_ids" t-as="l">
<div t-if="l.level == 'SEPARATOR'" class="separator" />
<div class="o_runbot_log_grid_date text-truncate">
<span t-attf-id="log_line_{{l.id}}" class="d-inline-block" style="scroll-margin-top: 120px;" t-out="l.create_date.strftime('%Y-%m-%d %H:%M:%S')" />
</div>
<div class="fw-bold" t-out="l.level if l.type not in ('link', 'markdown') and l.level != 'SEPARATOR' else ''" />
<div t-out="l.type if l.type not in ('link', 'markdown') and l.level != 'SEPARATOR' else ''" />
<div class="o_runbot_log_grid_message">
<!-- TODO: check logic exact logic -->
<t t-set="subbuild" t-value="build.children_ids &amp; build.browse(int(l.path)) if l.type == 'subbuild' else build.browse()" />
<!-- <t t-set="subbuild" t-value="(([child for child in build.children_ids if child.id == int(l.path)] if l.type == 'subbuild' else False) or
[build.browse()])[0]"/> -->
<t t-set="logclass" t-value="dict(CRITICAL='danger', ERROR='danger', WARNING='warning', OK='success', SEPARATOR='separator').get(l.level)" />
<t t-set="message_class" t-value="subbuild and subbuild._get_view_class() or ''" />
<div t-attf-class="bg-{{message_class or logclass}}-subtle p-1">
<div class="o_runbot_log_message_toolbox btn-group btn-group-ssm" role="group" t-if="l.level in ('CRITICAL', 'ERROR', 'WARNING') and None">
<t t-set="error_content" t-value="l.error_content_id" />
<t t-set="error" t-value="error_content.error_id" />
<a t-if="error" class="btn btn-default"
t-attf-href="/web#id={{error.id}}&amp;view_type=form&amp;model=runbot.build.error&amp;menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}"
target="new"> This error is already <em
t-attf-title="{{'Was detected by runbot in nightly builds.' if error.active else 'Either the error is not properly fixed or the branch does not contain the fix.'}}">
<t t-out="'known' if error.active else 'fixed'" />
</em>
<span groups="runbot.group_runbot_admin"
t-if="error.responsible or error.responsible.id == uid">(<i t-out="error.responsible.name" />)</span>
</a>
<a t-if="error" t-attf-class="btn btn-default bg-{{'info' if error.active else 'success'}}-subtle fa fa-list"
t-attf-href="/web#id={{error.id}}&amp;view_type=form&amp;model=runbot.build.error&amp;menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}"
title="View in Backend" target="new"
/>
<a t-if="not error" groups="runbot.group_runbot_admin" t-attf-href="/runbot/parse_log/{{l.id}}" class="btn btn-default fa fa-magic sm"
title="Parse this log line to follow this error." />
<a class="btn btn-default fa fa-link o_runbot_copy_link" t-attf-href="#log_line_{{l.id}}" title="Copy link to line" />
</div>
<t t-if="l.type not in ('runbot', 'link', 'markdown')">
<t t-if="l.type == 'subbuild'">
<a t-attf-href="/runbot/build/{{l.path}}"> Build # <t t-out="l.path" />
</a>
</t>
<t t-else="">
<t t-set="repo_name" t-value="l.path.replace('/data/build/', '').split('/')[0] " />
<t t-set="href" t-value="" />
<t t-if="repo_name in commit_link_per_name">
<t t-set="repo_base_url"
t-value="commit_link_per_name[repo_name].branch_id.remote_id.base_url if repo_name in commit_link_per_name else ''" />
<t t-set="commit_hash" t-value="commit_link_per_name[repo_name].commit_id.name if repo_name in commit_link_per_name else ''" />
<t t-set="path" t-value="l.path.replace('/data/build/%s/' % repo_name, '')" />
<t t-set="href" t-value="'https://%s/blob/%s/%s#L%s' % (repo_base_url, commit_hash, path, l.line)" />
</t>
<a t-att-href="href" t-attf-title="Func: {{l.func}}"><t t-out="l.name" />:<t t-out="l.line" /></a>
</t>
</t>
<tr t-attf-class="bg-{{rowclass.strip()}}-subtle{{' line-through' if build.orphan_result else ''}}">
<td>
<t t-out="build.create_date"/>
</td>
<td>
<a t-attf-href="/runbot/{{'batch/%s/' % from_batch.id if from_batch else ''}}build/{{build.id}}">
<t t-out="build.id"/>
<!-- DEPRECATED: Will be removed once no ir.logging is concerned. -->
<span class="log_message" t-if="l.type == 'link' and len(l.message.split('$$')) == 3">
<t t-set="message" t-value="l.message.split('$$')" />
<t t-if="message[1].startswith('fa-')">
<t t-out="message[0]" />
<a t-attf-href="{{l.path}}">
<i t-attf-class="fa {{message[1]}}" />
</a>
</td>
<td>
<t t-if="build.description">
<t t-out="build.md_description" />
</t>
</td>
<td>
<t t-if="build.global_state in ['testing', 'waiting']">
<i class="fa fa-spinner fa-spin"/>
<t t-out="build.global_state"/>
</t>
</td>
<td>
<span t-out="build.params_id.config_id.name"/>
</td>
<td>
<span t-out="build.params_id.version_id.name"/>
</td>
<td>
<span t-out="s2human(build.build_time)"/>
</td>
<td>
<t t-call="runbot.build_button">
<t t-set="bu" t-value="build"/>
<t t-set="klass" t-value="'btn-group-ssm'"/>
</t>
</td>
<td>
<t t-set="commits" t-value="build.params_id.commit_link_ids.commit_id.sorted(key=lambda c: c.repo_id.id)"/>
<t t-if="build_index+1 &lt; len(builds)" t-set="previous_commits" t-value="list(builds[build_index+1].params_id.commit_link_ids.commit_id.sorted(key=lambda c: c.repo_id.id))"/>
<t t-else="" t-set="previous_commits" t-value="[]"/>
<t t-foreach="zip(previous_commits, commits)" t-as="compare">
<t t-set="previous_commit" t-value="compare[0]"/>
<t t-set="commit" t-value="compare[1]"/>
<a t-attf-href="https://{{commit.repo_id.main_remote_id.base_url}}/compare/{{previous_commit.name}}..{{commit.name}}" t-att-title="commit.repo_id.name">
<i class="fa fa-plus"/>
</a>
</t>
</td>
</tr>
</t>
</table>
</div>
<t t-out="message[2]" />
</t>
<t t-else="">
<t t-out="message[0]" />
<a t-attf-href="{{l.path}}">
<t t-out="message[1]" />
</a>
<t t-out="message[2]" />
</t>
</span>
<span class="log_message" t-elif="l.type == 'markdown'" t-out="l._markdown()" />
<span class="log_message" t-else="">
<t t-if="'\n' not in l.message" t-out="l.message" />
<pre t-if="'\n' in l.message" style="margin:0;padding:0; border: none;"><t t-out="l.message"/></pre>
</span>
<div class="o_runbot_log_message_toolbox btn-group btn-group-ssm" role="group" t-if="l.level in ('CRITICAL', 'ERROR', 'WARNING')">
<t t-set="error_content" t-value="l.error_content_id" />
<t t-set="error" t-value="error_content.error_id" />
<a t-if="error" class="btn btn-default"
t-attf-href="/web#id={{error.id}}&amp;view_type=form&amp;model=runbot.build.error&amp;menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}"
target="new"> This error is already <em
t-attf-title="{{'Was detected by runbot in nightly builds.' if error.active else 'Either the error is not properly fixed or the branch does not contain the fix.'}}">
<t t-out="'known' if error.active else 'fixed'" />
</em>
<span groups="runbot.group_runbot_admin"
t-if="error.responsible or error.responsible.id == uid">(<i t-out="error.responsible.name" />)</span>
</a>
<a t-if="error" t-attf-class="btn btn-default bg-{{'info' if error.active else 'success'}}-subtle fa fa-list"
t-attf-href="/web#id={{error.id}}&amp;view_type=form&amp;model=runbot.build.error&amp;menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}"
title="View in Backend" target="new"
/>
<a t-if="not error" groups="runbot.group_runbot_admin" t-attf-href="/runbot/parse_log/{{l.id}}" class="btn btn-default fa fa-magic sm"
title="Parse this log line to follow this error." />
<a class="btn btn-default fa fa-link o_runbot_copy_link" t-attf-href="#log_line_{{l.id}}" title="Copy link to line" />
</div>
</div>
</div>
</t>
</div>
</t>
</template>
</data>
</div>
</t>
</template>
<!-- TODO: missing runbot.build_search -->
</odoo>

View File

@ -1,112 +1,152 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="runbot.bundle">
<t t-call='runbot.layout'>
<div class="container-fluid">
<div class="row">
<div class='col-md-12'>
<div class="navbar navbar-default">
<span class="text-center" style="font-size: 18px;">
<t t-out="bundle.name"/>
<i t-if="bundle.sticky" class="fa fa-star" style="color: #f0ad4e" />
<div class="btn-group" role="group">
<a groups="runbot.group_runbot_advanced_user"
t-attf-href="/odoo/runbot.bundle/{{bundle.id}}?menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}" class="btn btn-default" target="new" title="View in Backend">
<i class="fa fa-list"/>
</a>
<a groups="runbot.group_runbot_advanced_user" class="btn btn-default" t-attf-href="/runbot/bundle/{{bundle.id}}/force" title="Force A New Batch">
<i class="fa fa-refresh"/>
</a>
<a t-if="bundle.env.user.has_group('runbot.group_runbot_advanced_user') or (bundle.env.user.has_group('runbot.group_user') and ':' in bundle.name)" class="btn btn-default" t-attf-href="/runbot/bundle/{{bundle.id}}/force/1" title="Force A New Batch with automatic rebase">
<i class="fa fa-fast-forward"/>
</a>
<t t-call="runbot.branch_copy_button">
<t t-set="btn_size" t-value="'btn'"/>
</t>
<t t-call="runbot.bundle_stats_dropdown"/>
</div>
</span>
<span class="pull-right">
<t t-call="website.pager" />
</span>
</div>
<div>
<table class="table table-condensed table-responsive table-stripped">
<tr groups="base.group_user" t-if="not bundle.sticky and not bundle.is_base">
<t t-if="bundle.no_build">
<td class="text-danger">Build disabled</td>
<td>
<a class="btn btn-primary btn-ssm"
t-attf-href="/runbot/bundle/toggle_no_build/{{bundle.id}}/0">
Enable builds
</a>
</td>
</t>
<t t-else="">
<td>Build enabled</td>
<td>
<a class="btn btn-secondary btn-ssm"
t-attf-href="/runbot/bundle/toggle_no_build/{{bundle.id}}/1">
Disable builds
</a>
</td>
</t>
</tr>
<tr>
<td>Version</td>
<td>
<t t-out="bundle.version_id.name"/>
</td>
</tr>
<tr>
<td>Branches</td>
<td>
<t t-foreach="bundle._branch_groups().items()" t-as="group">
<t t-foreach="group[1]" t-as="branch">
<small>
<div class="btn-toolbar mb-1" role="toolbar">
<div class="btn-group btn-group-ssm" role="group">
<a t-att-href="branch.branch_url" class="btn btn-default text-start" title="View Branch on Github"><i class="fa fa-github"/></a>
<a groups="runbot.group_runbot_admin" class="btn btn-default fa fa-list text-start"
t-attf-href="/odoo/runbot.branch/{{branch.id}}?menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}" target="new" title="View Branch in Backend"/>
<a href="#" t-out="branch.remote_id.short_name" class="btn btn-default disabled text-start"/>
<a t-attf-href="/runbot/branch/{{branch.id}}" class="btn btn-default text-start" title="View Branch Details"><span t-att-class="'' if branch.alive else 'line-through'" t-out="branch.name"/> <i t-if="not branch.alive" title="deleted/closed" class="fa fa-ban text-danger"/></a>
<t t-if="not any (b.is_pr and b.alive for b in group[1]) and not branch.is_pr">
<a t-attf-href="https://{{group[0].main_remote_id.base_url}}/compare/{{bundle.version_id.name}}...{{branch.remote_id.owner}}:{{branch.name}}?expand=1" class="btn btn-default text-start" title="Create pr"><i class="fa fa-code-fork"/> Create pr</a>
</t>
</div>
</div>
</small>
</t>
</t>
</td>
</tr>
<tr t-if="more">
<td>Project</td>
<td t-out="bundle.project_id.name"/>
</tr>
<tr t-if="more">
<td>New build enabled</td>
<td>
<i t-attf-class="fa fa-{{'times' if bundle.no_build else 'check'}}"/>
</td>
</tr>
<tr t-if="more">
<td>Modules</td>
<td t-out="bundle.modules or '/'"/>
</tr>
</table>
</div>
<div t-foreach="bundle._consistency_warning()" t-as="warning" t-out="warning[1]" t-attf-class="alert alert-{{warning[0]}}"/>
<div class="batch_row" t-foreach="batchs" t-as="batch">
<t t-call="runbot.batch_tile"/>
</div>
</div>
</div>
</div>
<!-- Homepage and bundles search -->
<template id="runbot.bundles">
<t t-call="runbot.layout">
<div class="o_runbot_bundle_row row" t-foreach="bundles" t-as="bundle" t-if="bundle.last_batchs">
<!-- TODO: missing style -->
<div class="o_runbot_bundle_info col-12 col-md-auto me-1 mb-2">
<div class="text-truncate">
<i t-if="bundle.sticky" class="o_runbot_sticky_star fa fa-star" />
<a t-attf-href="/runbot/bundle/#{bundle.id}" t-attf-title="View Bundle {{bundle.name}}" class="fw-bold" t-out="bundle.name" />
</div>
<div class="btn-toolbar" role="toolbar">
<!-- Fetch last batch for each category -->
<t t-set="categories_and_batch"
t-value="[(category, bundle.with_context(category_id=category.id).last_done_batch) for category in categories if category.id != active_category_id]"
/>
<t t-set="categories_and_batch" t-value="[tpl for tpl in categories_and_batch if tpl[1]]" />
<div class="btn-group btn-group-ssm" role="group" t-if="categories_and_batch">
<t t-foreach="categories_and_batch" t-as="category_and_batch">
<t t-if="category_and_batch[0].view_id" t-call="{{category_and_batch[0].view_id.sudo().key}}">
<t t-set="category" t-value="category_and_batch[0]" />
<t t-set="last_category_batch" t-value="category_and_batch[1]" />
</t>
<a t-else=""
t-attf-title="View last {{category_and_batch[0].name}} batch"
t-attf-href="/runbot/batch/{{category_and_batch[1].id}}"
t-attf-class="btn btn-default fa fa-{{category_and_batch[0].icon}}"
/>
</t>
</div>
<div class="btn-group" role="group">
<t t-if="not bundle.sticky" t-call="runbot.branch_copy_button" />
<!-- TODO: check branch_github_menu -->
<t t-call="runbot.branch_github_menu" />
</div>
</div>
<div t-if="bundle.host_id">
<span class="badge text-bg-info" t-out="bundle.host_id.name" />
</div>
</div>
<div class="o_runbot_bundle_batch_row col">
<div class="row g-2">
<t t-foreach="enumerate(bundle.last_batchs)" t-as="e_batch">
<t t-set="index" t-value="e_batch[0]" />
<div t-att-class="'col-12 col-sm-6 col-xl-3' if index &lt; 2 else 'col-xl-3 d-none d-xl-block'">
<t t-call="runbot.batch_card">
<t t-set="batch" t-value="e_batch[1]" />
</t>
</div>
</t>
</template>
</data>
</div>
</div>
</div>
</t>
</template>
<!-- Bundle "form" view -->
<template id="runbot.bundle_toolbar_middle_section">
<div class="btn-group btn-group-sm" role="group">
<a groups="runbot.group_runbot_advanced_user"
t-attf-href="/web/#id={{bundle.id}}&amp;view_type=form&amp;model=runbot.bundle&amp;menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}"
class="btn btn-default" target="new" title="View in Backend">
<i class="fa fa-list" />
</a>
<a groups="runbot.group_runbot_advanced_user" class="btn btn-default" t-attf-href="/runbot/bundle/{{bundle.id}}/force" title="Force A New Batch">
<i class="fa fa-refresh" />
</a>
<a t-if="bundle.env.user.has_group('runbot.group_runbot_advanced_user') or (bundle.env.user.has_group('runbot.group_user') and ':' in bundle.name)"
class="btn btn-default" t-attf-href="/runbot/bundle/{{bundle.id}}/force/1" title="Force A New Batch with automatic rebase">
<i class="fa fa-fast-forward" />
</a>
<t t-call="runbot.branch_copy_button">
<t t-set="btn_size" t-value="'btn'" />
</t>
<t t-call="runbot.bundle_stats_dropdown" />
</div>
</template>
<template id="runbot.bundle">
<t t-call="runbot.layout">
<div class="o_runbot_bundle row p-2">
<div class="col-12">
<div class="container-lg p-0 card o_runbot_bundle_meta">
<div class="card-header fw-medium">
<t t-out="bundle.name" />
<i t-if="bundle.sticky" class="fa fa-star" style="color: #f0ad4e" />
</div>
<div class="o_runbot_grid o_runbot_grid_2">
<t groups="base.group_user" t-if="not bundle.sticky and not bundle.is_base">
<t t-if="bundle.no_build">
<div class="text-danger">Build disabled</div>
<div>
<a class="btn btn-primary btn-ssm"
t-attf-href="/runbot/bundle/toggle_no_build/{{bundle.id}}/0">
Enable builds
</a>
</div>
</t>
<t t-else="">
<div>Build enabled</div>
<div>
<a class="btn btn-secondary btn-ssm"
t-attf-href="/runbot/bundle/toggle_no_build/{{bundle.id}}/1">
Disable builds
</a>
</div>
</t>
</t>
<div>Version</div>
<div t-out="bundle.version_id.name" />
<div>Branches</div>
<div class="row gx-0 gy-1">
<t t-foreach="bundle._branch_groups().items()" t-as="group">
<div t-foreach="group[1]" t-as="branch" class="btn-group btn-group-ssm" role="group">
<a t-att-href="branch.branch_url" class="o_runbot_slot_btn_small btn btn-default text-start" title="View Branch on Github">
<i class="fa fa-github" />
</a>
<a groups="runbot.group_runbot_admin" class="o_runbot_slot_btn_small btn btn-default fa fa-list text-start"
t-attf-href="/web/#id={{branch.id}}&amp;view_type=form&amp;model=runbot.branch" target="new" title="View Branch in Backend" />
<a href="#" t-out="branch.remote_id.short_name" class="btn btn-default disabled text-start flex-grow-1" />
<a t-attf-href="/runbot/branch/{{branch.id}}" class="btn btn-default text-start flex-grow-0" title="View Branch Details">
<span t-att-class="'' if branch.alive else 'line-through'" t-out="branch.name" />
<i t-if="not branch.alive" title="deleted/closed" class="fa fa-ban text-danger" />
</a>
<t t-if="not any (b.is_pr and b.alive for b in group[1]) and not branch.is_pr">
<a
t-attf-href="https://{{group[0].main_remote_id.base_url}}/compare/{{bundle.version_id.name}}...{{branch.remote_id.owner}}:{{branch.name}}?expand=1"
class="btn btn-default text-start flex-grow-0" title="Create pr"><i class="fa fa-code-fork" /> Create pr</a>
</t>
</div>
</t>
</div>
<t t-if="more">
<div>Project</div>
<div t-out="bundle.project_id.name" />
<div>New build enabled</div>
<div t-attf-class="fa fa-{{'times' if bundle.no_build else 'check'}}" />
<div>Modules</div>
<div t-out="bundle.modules or '/'" />
</t>
</div>
</div>
</div>
<div t-foreach="bundle._consistency_warning()" t-as="warning" t-out="warning[1]" t-attf-class="col-12 alert alert-{{warning[0]}}" />
<div class="col-12" t-foreach="batchs" t-as="batch">
<t t-call="runbot.batch_card" />
</div>
</div>
</t>
</template>
</odoo>

View File

@ -1,133 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<template id="runbot.commit_status_state_td">
<!-- Must be called with a `state` variable !-->
<td t-if="state=='pending'">
<i class="fa fa-circle text-warning"/>
&amp;nbsp;
<t t-out="state"/>
</td>
<td t-if="state=='success'">
<i class="fa fa-check text-success"/>
&amp;nbsp;
<t t-out="state"/>
</td>
<td t-if="state in ('failure', 'error')">
<i class="fa fa-times text-danger"/>
&amp;nbsp;
<t t-out="state"/>
</td>
</template>
<template id="runbot.commit_status_state_cell">
<div>
<t t-if="state == 'pending'">
<i class="fa fa-circle text-warning"/> <t t-out="state"/>
</t>
<t t-if="state == 'success'">
<i class="fa fa-check text-success"/> <t t-out="state"/>
</t>
<t t-if="state in ('failure', 'error')">
<i class="fa fa-times text-danger"/> <t t-out="state"/>
</t>
</div>
</template>
<template id="runbot.commit">
<t t-call='runbot.layout'>
<div class="row g-0">
<!-- Commit base informations -->
<div class="col-md-6">
<table class="table table-stripped">
<tr>
<td>Name</td>
<td>
<t t-out="commit.name"/>
<div class="btn-group" role="group">
<a t-att-href="'' if not reflogs else 'https://%s/commit/%s' % (reflogs[0].branch_id.remote_id.base_url, commit.name)" class="btn btn-sm text-start" title="View Commit on Github"><i class="fa fa-github"/></a>
<a groups="runbot.group_runbot_admin" class="btn btn-sm fa fa-list text-start"
t-attf-href="/odoo/runbot.commit/{{commit.id}}?menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}" target="new" title="View Commit in Backend"/>
</div>
</td>
</tr>
<tr>
<td>Tree hash</td>
<td>
<t t-out="commit.tree_hash"/>
</td>
</tr>
<tr>
<td>Repo</td>
<td t-out="commit.repo_id.name"/>
</tr>
<tr>
<td>Subject</td>
<td t-out="commit.subject"/>
</tr>
<tr>
<td>Date</td>
<td t-out="commit.date"/>
</tr>
<tr>
<td>Author</td>
<td>
<t t-out="commit.author"/>
<small t-out="commit.author_email"/>
</td>
</tr>
<tr t-if="commit.author != commit.committer">
<td>Commiter</td>
<td>
<template id="runbot.commit">
<t t-call="runbot.layout">
<div class="o_runbot_commit row g-2">
<!-- Commit base information -->
<div class="col-lg-6">
<div class="card">
<div class="card-header px-2 py-1">Commit</div>
<div class="o_runbot_grid o_runbot_grid_2 o_runbot_grid_striped">
<div>Name</div>
<div>
<t t-out="commit.name"/>
<div class="btn-group" role="group">
<a t-att-href="'' if not reflogs else 'https://%s/commit/%s' % (reflogs[0].branch_id.remote_id.base_url, commit.name)" class="btn btn-sm text-start" title="View Commit on Github"><i class="fa fa-github"/></a>
<a groups="runbot.group_runbot_admin" class="btn btn-sm fa fa-list text-start" t-attf-href="/web/#id={{commit.id}}&amp;view_type=form&amp;model=runbot.commit" target="new" title="View Commit in Backend"/>
</div>
</div>
<div>Tree hash</div>
<div t-out="commit.tree_hash"/>
<div>Repo</div>
<div t-out="commit.repo_id.name"/>
<div>Subject</div>
<div t-out="commit.subject"/>
<div>Date</div>
<div t-out="commit.date"/>
<div>Author</div>
<div>
<t t-out="commit.author"/>
<small t-out="commit.author_email"/>
</div>
<t t-if="commit.author != commit.committer">
<div>Committer</div>
<div>
<t t-out="commit.committer"/>
<small t-out="commit.committer_email"/>
</td>
</tr>
</table>
</div>
<!-- Status -->
<div class="col-md-4">
<h3>Last Status</h3>
<table class="table table-sm table-borderless">
<tr t-foreach='last_status_by_context' t-as='context'>
<t t-set="status" t-value="last_status_by_context[context]"/>
<td t-out="status.sent_date and status.sent_date.strftime('%Y-%m-%d %H:%M:%S') or '—'"/>
<td t-out="context"/>
<t t-call="runbot.commit_status_state_td">
<t t-set="state" t-value="status.state"/>
</t>
<td>
<a t-att-href="status.target_url">
build
<t t-if="status.target_url" t-out="status.target_url.split('/')[-1]" />
</a>
</td>
<td groups="base.group_user">
<a t-attf-href="/runbot/commit/resend/{{status.id}}" title="Resend github status">
<i class="fa fa-repeat"/>
</a>
</td>
</tr>
</table>
</div>
</t>
</div>
</div>
</div>
<div class="row g-0">
<div class="col-md-6">
<h3>Branch presence history</h3>
<table class="table table-stripped">
<tr t-foreach='reflogs' t-as='reflog'>
<td t-out="reflog.date"/>
<td t-out="reflog.branch_id.remote_id.short_name"/>
<td><a t-attf-href="/runbot/branch/{{reflog.branch_id.id}}" t-out="reflog.branch_id.name" title="View Branch Details"/></td>
</tr>
</table>
</div>
<div class="col-md-6">
<h3>Status history</h3>
<table class="table table-stripped">
<tr t-foreach='status_list' t-as='status'>
<td t-out="status.sent_date and status.sent_date.strftime('%Y-%m-%d %H:%M:%S') or '—'"/>
<td t-out="status.context"/>
<t t-call="runbot.commit_status_state_td">
<!-- Status -->
<div class="col-lg-6">
<div class="card">
<div class="card-header px-2 py-1">Last Status</div>
<div class="o_runbot_grid o_runbot_grid_5 o_runbot_commit_status_grid">
<t t-foreach="last_status_by_context" t-as="context">
<t t-set="status" t-value="last_status_by_context[context]"/>
<div t-out="status.sent_date and status.sent_date.strftime('%Y-%m-%d %H:%M:%S') or '—'"/>
<div t-out="context"/>
<t t-call="runbot.commit_status_state_cell">
<t t-set="state" t-value="status.state"/>
</t>
<td>
<div>
<a t-att-href="status.target_url">
Build <t t-if="status.target_url" t-out="status.target_url.split('/')[-1]"/>
</a>
</div>
<div>
<div class="btn-group btn-group-ssm" role="group">
<a groups="base.group_user" class="btn btn-default" t-attf-href="/runbot/commit/resent/{{status.id}}" title="Resend github status">
<i class="fa fa-repeat"/>
</a>
</div>
</div>
</t>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card">
<div class="card-header px-2 py-1">Branch presence history</div>
<div class="o_runbot_grid o_runbot_grid_3 o_runbot_grid_auto">
<t t-foreach="reflogs" t-as="reflog">
<div t-out="reflog.date"/>
<div t-out="reflog.branch_id.remote_id.short_name"/>
<div>
<a t-attf-href="/runbot/branch/{{reflog.branch_id.id}}" t-out="reflog.branch_id.name" title="View Branch Details"/>
</div>
</t>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card">
<div class="card-header px-2 py-1">Status history</div>
<div class="o_runbot_grid o_runbot_grid_4 o_runbot_commit_status_history_grid">
<t t-foreach="status_list" t-as="status">
<div t-out="status.sent_date and status.sent_date.strftime('%Y-%m-%d %H:%M:%S') or '—'"/>
<div t-out="status.context"/>
<t t-call="runbot.commit_status_state_cell">
<t t-set="state" t-value="status.state"/>
</t>
<div>
<a t-attf-href="/runbot/build/{{status.build_id.id}}">
build
<t t-out="status.build_id.id" />
</a>
</td>
</tr>
</table>
</div>
</t>
</div>
</div>
</div>
</t>
</template>
</data>
</div>
</t>
</template>
</odoo>

View File

@ -38,8 +38,8 @@
</template>
<template id="frontend_no_nav" inherit_id="runbot.layout" primary="True">
<xpath expr="//header" position="replace">
</xpath>
<!-- <xpath expr="//header" position="replace">
</xpath> -->
</template>
<template id="runbot.config_monitoring">

View File

@ -1,132 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="runbot.bundles">
<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 ps-0">
<input class="form-control my-0 py-1" type="text" placeholder="Search" aria-label="Search" name="search" t-att-value="search"/>
<a t-if="has_pr" class="btn btn-primary active input-group-text" title="All" t-att-href="qu(has_pr=None)">
<i class="fa fa-github text-grey"/>
</a>
<a t-else="" class="btn input-group-text" title="Open pull requests" t-att-href="qu(has_pr=1)">
<i class="fa fa-github text-grey"/>
</a>
<button type='submit' class="input-group-text">
<i class="fa fa-search text-grey"/>
</button>
</div>
</form>
</t>
<div class="container-fluid frontend">
<div class="row">
<div class='col-md-12'>
<span class="pull-right" t-call="runbot.slots_infos"/>
</div>
<div class='col-md-12'>
<div t-if="message" class="alert alert-warning" role="alert">
<t t-out="message" />
</div>
<div t-if="not project" class="mb32">
<h3>No project</h3>
</div>
<div t-else="">
<div t-foreach="bundles" t-as="bundle" class="row bundle_row">
<div class="col-md-3 col-lg-2 cell">
<div class="one_line">
<i t-if="bundle.sticky" class="fa fa-star" style="color: #f0ad4e" />
<a t-attf-href="/runbot/bundle/#{bundle.id}" t-attf-title="View Bundle #{bundle.name}">
<b t-out="bundle.name"/>
</a>
</div>
<div class="btn-toolbar" role="toolbar" aria-label="Toolbar with button groups">
<div class="btn-group" role="group">
<t t-foreach="categories" t-as="category">
<t t-if="active_category_id != category.id">
<t t-set="last_category_batch" t-value="bundle.with_context(category_id=category.id).last_done_batch"/>
<t t-if="last_category_batch">
<t t-if="category.view_id" t-call="{{category.view_id.sudo().key}}"/>
<a t-else=""
t-attf-title="View last {{category.name}} batch"
t-attf-href="/runbot/batch/{{last_category_batch.id}}"
t-attf-class="fa fa-{{category.icon}}"
/>
</t>
</t>
</t>
</div>
<div class="btn-group" role="group">
<t t-if="not bundle.sticky" t-call="runbot.branch_copy_button"/>
<t t-call="runbot.branch_github_menu"/>
</div>
</div>
<div t-if="bundle.host_id">
<span class="badge text-bg-info" t-out="bundle.host_id.name"></span>
</div>
</div>
<div class="col-md-9 col-lg-10">
<div class="row gx-0">
<div t-foreach="bundle.last_batchs" t-as="batch" t-attf-class="col-md-6 col-xl-3 {{'d-none d-xl-block' if batch_index > 1 else ''}}">
<t t-call="runbot.batch_tile"/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</template>
<template id="runbot.batch_tile">
<t t-set="klass">info</t>
<t t-if="batch.state=='skipped'" t-set="klass">killed</t>
<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 if more">
<div t-attf-class="card bg-{{klass}}-subtle">
<a t-attf-href="/runbot/batch/#{batch.id}" title="View Batch">
<div class="batch_header">
<span t-attf-class="badge text-bg-{{'warning' if batch.has_warning else 'default'}}">
<t t-out="batch._get_formated_age()"/>
<i class="fa fa-exclamation-triangle" t-if="batch.has_warning"/>
</span>
<span class="float-end 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>
<div class="batch_slots">
<t t-foreach="batch.slot_ids" t-as="slot">
<t t-if="slot.build_id">
<div t-if="((not slot.trigger_id.hide and trigger_display is None) or (trigger_display and slot.trigger_id.id in trigger_display)) or slot.build_id.global_result != 'ok'"
t-call="runbot.slot_button" class="slot_container"/>
</t>
</t>
<div class="slot_filler" t-foreach="range(10)" t-as="x"/>
</div>
<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">
<t t-set="match_class" t-value="'info' if commit_link.match_type == 'new' else 'secondary'"/>
<a t-attf-href="/runbot/commit/#{commit_link.commit_id.id}" t-attf-class="badge text-bg-{{match_class}}">
<i class="fa fa-fw fa-hashtag" t-if="commit_link.match_type == 'new'" title="This commit is a new head"/>
<i class="fa fa-fw fa-link" t-if="commit_link.match_type == 'head'" title="This commit is an existing head from bundle branches"/>
<i class="fa fa-fw fa-code-fork" t-if="commit_link.match_type == 'base_match'" title="This commit is matched from a base batch with matching merge_base"/>
<i class="fa fa-fw fa-clock-o" t-if="commit_link.match_type == 'base_head'" title="This commit is the head of a base branch"/>
<t t-out="commit_link.commit_id.dname"/>
</a>
<a t-att-href="'https://%s/commit/%s' % (commit_link.branch_id.remote_id.base_url, commit_link.commit_id.name)" t-attf-class="badge text-bg-{{match_class}}" title="View Commit on Github"><i class="fa fa-github"/></a>
<span t-out="commit_link.commit_id.subject"/>
</div>
</div>
</div>
</div>
</template>
</data>
</odoo>

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Empty page layout, only contains assets -->
<template id="runbot.base_layout">
<html>
<title t-out="title or 'Runbot'" />
<meta name="runbot-project-id" t-att-content="project.id" />
<t t-call-assets="runbot.assets_frontend" />
<t t-if="refresh">
<!-- TODO: replace this with javascript code -->
<meta http-equiv="refresh" t-att-content="refresh" />
</t>
<t t-if="not page_info_state or page_info_state == 'ok' or page_info_state not in ('warn', 'ko', 'skipped', 'killed', 'manually_killed')">
<link rel="icon" type="image/png" href="/runbot/static/src/img/icon_ok.png" />
<link rel="icon" type="image/svg+xml" href="/runbot/static/src/img/icon_ok.svg" />
</t>
<t t-elif="page_info_state == 'ko'">
<link rel="icon" type="image/png" href="/runbot/static/src/img/icon_ko.png" />
<link rel="icon" type="image/svg+xml" href="/runbot/static/src/img/icon_ko.svg" />
</t>
<t t-elif="page_info_state == 'warn'">
<link rel="icon" type="image/png" href="/runbot/static/src/img/icon_warn.png" />
<link rel="icon" type="image/svg+xml" href="/runbot/static/src/img/icon_warn.svg" />
</t>
<t t-elif="page_info_state == 'skipped'">
<link rel="icon" type="image/png" href="/runbot/static/src/img/icon_skipped.png" />
<link rel="icon" type="image/svg+xml" href="/runbot/static/src/img/icon_skipped.svg" />
</t>
<t t-elif="page_info_state == 'killed' or page_info_state == 'manually_killed'">
<link rel="icon" type="image/png" href="/runbot/static/src/img/icon_killed.png" />
<link rel="icon" type="image/svg+xml" href="/runbot/static/src/img/icon_killed.svg" />
</t>
</html>
<body>
<t t-out="0" />
</body>
</template>
<!-- Layout with navbar, container potential toolbar -->
<template id="runbot.layout">
<t t-call="runbot.base_layout">
<t t-call="{{layout_nav_template or 'runbot.navbar'}}" />
<div class="container-fluid o_runbot_main_container d-flex flex-column">
<t t-call="{{layout_toolbar_template or 'runbot.toolbar'}}" />
<t t-out="0" />
</div>
</t>
</template>
</odoo>

172
runbot/templates/navbar.xml Normal file
View File

@ -0,0 +1,172 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- TODO: figure out proper keepquery logic -->
<template id="runbot.navbar">
<!--
Context used;
class Navbar(TypedDict):
project: self.env['runbot.project']
projects: Optional[self.env['runbot.project']]
qu: QueryURL object
nb_assigned_errors: int
nb_team_errors: int
nb_build_errors: int
has_pr: Optional[bool]
-->
<nav class="navbar navbar-expand-sm sticky-top shadow-sm bg-body-tertiary">
<div class="container-fluid">
<div class="navbar-brand">
<a class="text-reset text-decoration-none text-initial" t-out="project.name"
t-att-href="qu('/runbot/%s' % slug(project))" />
<span t-if="projects and len(projects) > 1"
class="dropdown-toggle dropdown-toggle-caret d-none d-sm-inline" role="button"
data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false" />
<ul t-if="projects and len(projects) > 1" class="dropdown-menu">
<li>
<h6 class="dropdown-header">Change Project</h6>
</li>
<li>
<a class="dropdown-item active" t-out="project.name"
t-att-href="qu('/runbot/%s' % slug(project))" />
</li>
<li>
<hr class="dropdown-divider" />
</li>
<t t-foreach="projects" t-as="_project">
<li t-if="project.id != _project.id">
<a class="dropdown-item" t-att-href="qu('/runbot/%s' % slug(_project))"
t-out="_project.name" />
</li>
</t>
</ul>
</div>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#navbarOffCanvas"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="offcanvas offcanvas-end" tabindex="-1" id="navbarOffCanvas">
<div class="offcanvas-header">
<h5 class="offcanvas-title" t-out="project.name" />
<button
type="button"
class="btn-close"
data-bs-dismiss="offcanvas"
aria-label="Close"
/>
</div>
<div class="offcanvas-body">
<ul class="navbar-nav justify-content-end flex-grow-1 pe-3">
<li class="nav-item d-sm-none">
<a class="nav-link active" aria-current="page"
t-att-href="qu('/runbot/%s' % slug(project))" t-out="project.name" />
</li>
<t t-foreach="projects" t-as="_project">
<li t-if="project.id != _project.id" class="nav-item d-sm-none">
<a class="nav-link" t-att-href="qu('/runbot/%s' % slug(_project))"
t-out="_project.name" />
</li>
</t>
<!-- TODO: js code to open preferences -->
<li class="nav-item divider" />
<li class="nav-item dropdown o_runbot_preferences">
<a class="nav-link" href="#" data-bs-toggle="dropdown" data-bs-auto-close="outside">
<i class="fa fa-gear" />
</a>
<div class="dropdown-menu" role="menu">
<div class="form-check form-switch m-2"
title="Displays more information (requires reload)">
<label class="form-check-label" for="navbarMoreInfo">More info</label>
<input class="form-check-input o_runbot_more_info" id="navbarMoreInfo"
t-att-checked="more" type="checkbox" />
</div>
<div class="dropdown-divider" />
<div class="form-floating m-2">
<select class="form-select o_runbot_theme_switcher" id="navbarThemeSelector"
aria-label="Change Theme">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="legacy">Legacy</option>
<option value="red404">Red404</option>
</select>
<label for="navbarThemeSelector">Theme</label>
</div>
<div class="dropdown-divider" groups="base.group_user" />
<a class="dropdown-item o_runbot_manage_filters" role="menuitem" href="#"
groups="base.group_user">
Manage filters
</a>
</div>
</li>
<li class="nav-item divider" />
<t t-if="not user_id._is_public()">
<li class="nav-item" t-if="nb_assigned_errors">
<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-out="nb_assigned_errors" />
<span class="text-warning" t-if="nb_team_errors"
t-out="'+ ' + str(nb_team_errors)" />
</a>
</li>
<li class="nav-item" t-elif="nb_team_errors">
<a href="/runbot/errors" class="nav-link text-warning"
t-attf-title="Your team has {{nb_team_errors}} random bug assigned">
<i class="fa fa-bug" />
<t t-out="nb_team_errors" />
</a>
</li>
<li class="nav-item" t-elif="nb_build_errors">
<a href="/runbot/errors" class="nav-link" title="Random Bugs">
<i class="fa fa-bug" />
</a>
</li>
<li class="nav-item dropdown" t-ignore="true">
<a href="#" class="o_runbot_navbar_username nav-link fw-bold text-truncate"
data-bs-toggle="dropdown" t-out="user_id.name" />
<div class="dropdown-menu dropdown-menu-end" 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-out="team.name.capitalize()" />
</a>
</div>
</li>
</t>
<li class="nav-item" t-else="" t-ignore="true">
<a class="nav-link fw-bold"
t-attf-href="/web/login?redirect={{request.httprequest.path}}">Login</a>
</li>
</ul>
<form class="mt-3 mb-0 mt-sm-0 d-flex" t-attf-action="/runbot/{{slug(project)}}">
<div class="input-group me-2 flex-grow-1">
<input class="form-control" type="search" name="search" placeholder="Search"
aria-label="Search" t-att-value="search"/>
</div>
<div class="btn-group">
<div class="input-group-text o_runbot_btn_haspr btn btn-outline-primary p-0"
type="button" tabindex="0">
<input type="checkbox" tabindex="0" id="searchHasPr" name="has_pr" class="d-none"
t-att-checked="bool(request.params.get('has_pr'))"/>
<label for="searchHasPr" class="py-2 px-3 w-100 h-100 align-content-center"
role="button">
<i class="fa fa-github"></i>
</label>
</div>
<button class="btn btn-outline-success" type="submit">Search</button>
</div>
</form>
</div>
</div>
</div>
</nav>
</template>
</odoo>

23
runbot/templates/slot.xml Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="runbot.slot_btn_group">
<t t-set="bu" t-value="slot.build_id" />
<t t-set="color" t-value="bu._get_color_class()" />
<div class="o_runbot_slot_btn_group btn-group btn-group-ssm">
<button t-attf-class="o_runbot_slot_btn_small btn btn-{{color}} disabled fa fa-{{slot._fa_link_type()}}" t-att-title="slot.link_type" />
<a t-att-href="'/runbot/batch/%s/build/%s' % (slot.batch_id.id, bu.id) if bu else None"
t-attf-class="btn btn-default text-start text-nowrap text-truncate flex-grow-1{{' disabled' if not bu else ''}}"
t-out="slot.trigger_id.name"
/>
<a t-if="slot.trigger_id.report_view_id" title="View last trigger report" t-attf-href="/runbot/trigger/report/{{slot.trigger_id.id}}"
class="o_runbot_slot_btn_small fa fa-eye btn btn-info" />
<a t-if="bu.local_state == 'running' and bu.database_ids" t-attf-href="/runbot/run/{{bu.id}}" class="o_runbot_slot_btn_small fa fa-sign-in btn btn-info" />
<a t-if="bu.static_run" t-att-href="bu.static_run" class="o_runbot_slot_btn_small fa fa-sign-in btn btn-info" />
<t t-if="bu" t-call="runbot.build_menu" />
<a t-if="not bu" groups="base.group_user" class="o_runbot_slot_btn_small btn btn-default" title="Create build"
t-attf-href="/runbot/batch/slot/{{slot.id}}/build">
<i class="fa fa-play fa-fw" />
</a>
</div>
</template>
</odoo>

View File

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!--
The toolbar is inserted in the default layout if the 'toolbar' context value is set.
(see frontend.py for default implementation.)
The toolbar is a 3 part row similar to odoo's toolbar, the middle section wraps first
when the window becomes too small.
The default toolbar template renders the following:
1) Breadcrumbs
2) Message (if there is an alert)
3) Runbot status
-->
<template id="runbot.toolbar">
<div t-if="toolbar" t-att-class="'o_runbot_toolbar row bg-body' + (' position-sticky z-1' if toolbar.get('sticky', True) else '')">
<div class="d-flex flex-wrap flex-md-nowrap justify-content-between align-items-center gap-md-3">
<t t-call="{{toolbar.get('start_template', 'runbot.toolbar_start_section')}}" />
<t t-call="{{toolbar.get('middle_template', 'runbot.toolbar_middle_section')}}" />
<t t-call="{{toolbar.get('end_template', 'runbot.toolbar_end_section')}}" />
</div>
</div>
</template>
<!-- Toolbar item layout and content are separated such that everything can be customised -->
<template id="runbot.toolbar_section_empty">
</template>
<template id="runbot.toolbar_start">
<div class="d-flex d-empty-none order-0" style="flex: 1;">
<t t-out="0"/>
</div>
</template>
<template id="runbot.toolbar_start_section">
<t t-call="runbot.toolbar_start">
<div t-if="toolbar.get('breadcrumbs') and len(toolbar.get('breadcrumbs')) > 1" class="o_runbot_toolbar_breadcrumbs d-flex">
<t t-set="breadcrumbs" t-value="toolbar.get('breadcrumbs')" />
<ol class="breadcrumb flex-nowrap text-nowrap lh-sm">
<li
t-foreach="breadcrumbs" t-as="breadcrumb"
t-attf-class="breadcrumb-item d-inline-flex{{' active' if breadcrumb == breadcrumbs[-1] else None}}"
t-att-aria-current="'page' if breadcrumb == breadcrumbs[-1] else None"
>
<a t-if="breadcrumb != breadcrumbs[-1]" t-att-href="breadcrumb.url"
t-out="breadcrumb.name" />
<t t-else="" t-out="breadcrumb.name" />
</li>
</ol>
</div>
</t>
</template>
<template id="runbot.toolbar_middle">
<div class="d-flex d-empty-none justify-content-start justify-content-md-around order-2 order-md-1 w-100 w-md-auto mw-100">
<t t-out="0"/>
</div>
</template>
<template id="runbot.toolbar_middle_section">
<!-- TODO: remove t-call and use whole row instead -->
<t t-call="runbot.toolbar_middle">
<div t-if="toolbar.get('message')" class="alert alert-warning m-0" role="alert"
t-out="toolbar.get('message')" />
</t>
</template>
<template id="runbot.toolbar_end">
<div class="d-flex d-empty-none justify-content-end order-1 order-md-2 flex-grow-1 gap-1" style="flex: 1;">
<t t-out="0"/>
</div>
</template>
<template id="runbot.toolbar_end_section">
<t t-call="runbot.toolbar_end">
<a t-if="toolbar.get('pending_count')" href="/runbot/load_info" class="text-decoration-none">
<span t-attf-class="badge text-bg-{{toolbar.get('pending_level')}}"> Pending: <t
t-out="toolbar.get('pending_count')" />
<span t-if="toolbar.get('pending_assigned_count')"
title="Assigned build (reserved host)"> (<t t-out="toolbar.get('pending_assigned_count')" />
)
</span>
</span>
</a>
<a href="/runbot/load_info" t-if="toolbar.get('hosts_data') is not None"
class="slots_infos text-decoration-none">
<t t-set="testing" t-value="toolbar.get('hosts_data')._total_testing()" />
<t t-set="workers" t-value="toolbar.get('hosts_data')._total_workers()" />
<t t-set="klass">success</t>
<t t-if="not workers" t-set="klass">danger</t>
<t t-else="">
<t t-if="int(testing)/workers > 0" t-set="klass">info</t>
<t t-if="int(testing)/workers > 0.75" t-set="klass">warning</t>
<t t-if="int(testing)/workers >= 1" t-set="klass">danger</t>
</t>
<span t-attf-class="badge text-bg-{{klass}}"> Testing: <t t-out="testing" /> / <t
t-out="workers" />
</span>
</a>
</t>
</template>
</odoo>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="runbot.trigger_report">
<t t-call="runbot.layout">
<t t-call="{{trigger.report_view_id.id}}">
</t>
</t>
</template>
</odoo>

View File

@ -1,419 +1,200 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- base layout -->
<template id="runbot.base_page">
<html t-att-data-bs-theme="theme">
<head>
<title t-out="title or 'Runbot'"/>
<link rel="stylesheet" type="text/css" href="/runbot/static/src/libs/bootstrap/css/bootstrap.css"/>
<link rel="stylesheet" type="text/css" href="/runbot/static/src/libs/fontawesome/css/font-awesome.css"/>
<link rel="stylesheet" type="text/css" href="/runbot/static/src/css/runbot.css"/>
<script src="/runbot/static/src/libs/jquery/jquery.js" type="text/javascript"/>
<script type="text/javascript" src="/runbot/static/src/libs/popper/popper.js"/>
<script type="text/javascript" src="/runbot/static/src/libs/bootstrap/js/bootstrap.bundle.js"/>
<script type="text/javascript" src="/runbot/static/src/js/runbot.js"/>
<!-- The default pager but with small buttons -->
<template id="runbot.pager">
<t t-call="portal.pager">
<t t-set="classname" t-value="'pagination-sm'"/>
</t>
</template>
<t t-if="refresh">
<meta http-equiv="refresh" t-att-content="refresh"/>
</t>
<t t-if="not page_info_state or page_info_state == 'ok' or page_info_state not in ('warn', 'ko', 'skipped', 'killed', 'manually_killed')">
<link rel="icon" type="image/png" href="/runbot/static/src/img/icon_ok.png"/>
<link rel="icon" type="image/svg+xml" href="/runbot/static/src/img/icon_ok.svg"/>
</t>
<t t-elif="page_info_state == 'ko'">
<link rel="icon" type="image/png" href="/runbot/static/src/img/icon_ko.png"/>
<link rel="icon" type="image/svg+xml" href="/runbot/static/src/img/icon_ko.svg"/>
</t>
<t t-elif="page_info_state == 'warn'">
<link rel="icon" type="image/png" href="/runbot/static/src/img/icon_warn.png"/>
<link rel="icon" type="image/svg+xml" href="/runbot/static/src/img/icon_warn.svg"/>
</t>
<t t-elif="page_info_state == 'skipped'">
<link rel="icon" type="image/png" href="/runbot/static/src/img/icon_skipped.png"/>
<link rel="icon" type="image/svg+xml" href="/runbot/static/src/img/icon_skipped.svg"/>
</t>
<t t-elif="page_info_state == 'killed' or page_info_state == 'manually_killed'">
<link rel="icon" type="image/png" href="/runbot/static/src/img/icon_killed.png"/>
<link rel="icon" type="image/svg+xml" href="/runbot/static/src/img/icon_killed.svg"/>
</t>
</head>
<body>
<t t-out="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 bg-body-tertiary">
<a t-if="project" t-att-href="qu(search=search)">
<b class="active_project">
<t t-out="project.name"/>
</b>
</a>
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#top_menu_collapse">
<span class="navbar-toggler-icon"/>
</button>
<div class="collapse navbar-collapse" id="top_menu_collapse">
<ul class="nav navbar-nav ms-auto text-end" 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-out="l_project.name"/>
</a>
</li>
</t>
</t>
<li class="nav-item divider"/>
<li class="nav-item dropdown">
<a data-bs-toggle="collapse" href="#collapsePreference" role="button" class="nav-link">
<i class="fa fa-gear"/>
</a>
</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-bs-toggle="dropdown">
<b>
<span t-out="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 dropdown-menu-end 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-out="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={{request.httprequest.path}}">Login</a>
</b>
</li>
</t>
</ul>
<t t-out="nav_form or ''">
</t>
</div>
</nav>
</header>
<div id="collapsePreference" class="collapse">
<form class="px-4 py-3" method="post" action="/runbot/submit" id="preferences_form">
<input type="hidden" name="redirect" t-att-value="current_path"/>
<hr class="separator"/>
<div class="form-check form-switch">
<input onclick="document.getElementById('preferences_form').submit()" class="form-check-input" type="checkbox" role="switch" id="more" name="more" t-att-checked="more"/>
<label class="form-check-label" for="flexSwitchCheckDefault" >More info</label>
</div>
<hr class="separator"/>
<div class="text-nowrap btn-group btn-group-sm" role="group">
<button onclick="document.cookie = 'theme=legacy; expires=Thu, 1 Dec 2942 12:00:00 UTC; path=/'; location.reload();" type="button" t-attf-class="btn btn-{{'primary' if theme=='legacy' else 'secondary'}}">Legacy</button>
<button onclick="document.cookie = 'theme=dark; expires=Thu, 1 Dec 2942 12:00:00 UTC; path=/'; location.reload();" type="button" t-attf-class="btn btn-{{'primary' if theme=='dark' else 'secondary'}}">Dark</button>
<button onclick="document.cookie = 'theme=light; expires=Thu, 1 Dec 2942 12:00:00 UTC; path=/'; location.reload();" type="button" t-attf-class="btn btn-{{'primary' if theme=='light' else 'secondary'}}">Light</button>
<button onclick="document.cookie = 'theme=red404; expires=Thu, 1 Dec 2942 12:00:00 UTC; path=/'; location.reload();" type="button" t-attf-class="btn btn-{{'primary' if theme=='red404' else 'secondary'}}">Red404</button>
</div>
<hr class="separator"/>
<div class="text-nowrap btn-group btn-group-sm" role="group">
<button onclick="document.cookie = 'filter_mode=all; expires=Thu, 1 Dec 2942 12:00:00 UTC; path=/'; location.reload();" type="button" t-attf-class="btn btn-{{'primary' if filter_mode=='all' else 'secondary'}}">All</button>
<button onclick="document.cookie = 'filter_mode=sticky; expires=Thu, 1 Dec 2942 12:00:00 UTC; path=/'; location.reload();" type="button" t-attf-class="btn btn-{{'primary' if filter_mode=='sticky' else 'secondary'}}">Sticky only</button>
<button onclick="document.cookie = 'filter_mode=nosticky; expires=Thu, 1 Dec 2942 12:00:00 UTC; path=/'; location.reload();" type="button" t-attf-class="btn btn-{{'primary' if filter_mode=='nosticky' else 'secondary'}}">Dev only</button>
</div>
<div class="text-nowrap btn-group btn-group-sm" role="group">
<t t-foreach="categories" t-as="category">
<button t-attf-onclick="document.cookie = 'category={{category.id}}; expires=Thu, 1 Dec 2942 12:00:00 UTC; path=/'; location.reload();" type="button" t-attf-class="btn btn-{{'primary' if category.id == active_category_id else 'secondary'}}" t-out="category.name"/>
</t>
</div>
<hr class="separator"/>
<div t-if="triggers">
<input type="hidden" name="update_triggers" t-att-value="project.id"/>
<t t-foreach="categories" t-as="category">
<t t-set="category_triggers" t-value="triggers.filtered(lambda t: not t.manual and t.category_id == category)"/>
<t t-if="category_triggers">
<h3 t-out="category.name"/>
<div class="row">
<t t-foreach="category_triggers" t-as="trigger">
<div class="col-md-3 text-nowrap">
<input t-attf-class="trigger_selection {{'trigger_selection_hide' if trigger.hide else 'trigger_selection_show'}}" 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-out="trigger.name"/>
</div>
</t>
</div>
</t>
</t>
</div>
<button
onclick="Array.from(document.getElementsByClassName('trigger_selection_show')).forEach((element) => element.checked = true); Array.from(document.getElementsByClassName('trigger_selection_hide')).forEach((element) => element.checked = false); event.preventDefault();"
class="btn btn-secondary">Reset to default</button>
<button
onclick="Array.from(document.getElementsByClassName('trigger_selection')).forEach((element) => element.checked = true); event.preventDefault();"
class="btn btn-secondary">All</button>
<button
onclick="Array.from(document.getElementsByClassName('trigger_selection')).forEach((element) => element.checked = false); event.preventDefault();"
class="btn btn-secondary">None</button>
<button type="submit" class="btn btn-primary">Save</button>
</form>
</div>
<t t-out="0"/>
</body>
</xpath>
</template>
<template id="runbot.build_errors_link">
<t t-if="nb_assigned_errors">
<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-out="nb_assigned_errors"/>
<span class="text-warning" t-if="nb_team_errors">+<t t-out="nb_team_errors"/></span>
</a>
</li>
</t>
<t t-elif="nb_team_errors">
<li class="nav-item divider"/>
<li class="nav-item">
<a href="/runbot/errors" class="nav-link text-warning" t-attf-title="Your team has {{nb_team_errors}} random bug assigned">
<i class="fa fa-bug"/>
<t t-out="nb_team_errors"/>
</a>
</li>
</t>
<t t-elif="nb_build_errors">
<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">
<a href="/runbot/load_info" class="slots_infos">
<span t-attf-class="badge text-bg-{{pending_level}}">
Pending:
<t t-out="pending_count"/><span title="Assigned build (reserved host)" t-if="pending_assigned_count">(<t t-out="pending_assigned_count"/>)</span>
</span>
<t t-set="testing" t-value="hosts_data._total_testing()"/>
<t t-set="workers" t-value="hosts_data._total_workers()"/>
<t t-set="klass">success</t>
<t t-if="not workers" t-set="klass">danger</t>
<t t-else="">
<t t-if="int(testing)/workers > 0" t-set="klass">info</t>
<t t-if="int(testing)/workers > 0.75" t-set="klass">warning</t>
<t t-if="int(testing)/workers >= 1" t-set="klass">danger</t>
</t>
<span t-attf-class="badge text-bg-{{klass}}">
Testing:
<t t-out="testing"/>
/
<t t-out="workers"/>
</span>
<!-- Delete from here on -->
<template id="runbot.slot_button">
<t t-set="bu" t-value="slot.build_id"/>
<t t-set="color" t-value="bu._get_color_class()"/>
<div t-attf-class="btn-group btn-group-ssm slot_button_group">
<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/batch/{{slot.batch_id.id}}/build/#{bu.id}" t-attf-class="btn btn-default slot_name">
<span t-out="slot.trigger_id.name"/>
</a>
</template>
<span t-else="" t-attf-class="btn btn-default disabled slot_name">
<span t-out="slot.trigger_id.name"/>
</span>
<a t-if="slot.trigger_id.report_view_id" title="View last trigger report" t-attf-href="/runbot/trigger/report/{{slot.trigger_id.id}}" class="fa fa-eye btn btn-info"/>
<a t-if="bu.local_state == 'running' and bu.database_ids" t-attf-href="/runbot/run/{{bu.id}}" class="fa fa-sign-in btn btn-info"/>
<a t-if="bu.static_run" t-att-href="bu.static_run" class="fa fa-sign-in btn btn-info"/>
<t t-if="bu" t-call="runbot.build_menu"/>
<a t-if="not bu" groups="base.group_user" class="btn btn-default" title="Create build" t-attf-href="/runbot/batch/slot/{{slot.id}}/build">
<i class="fa fa-play fa-fw"/>
</a>
</div>
</template>
<template id="runbot.slot_button">
<t t-set="bu" t-value="slot.build_id"/>
<t t-set="color" t-value="bu._get_color_class()"/>
<div t-attf-class="btn-group btn-group-ssm slot_button_group">
<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/batch/{{slot.batch_id.id}}/build/#{bu.id}" t-attf-class="btn btn-default slot_name">
<span t-out="slot.trigger_id.name"/>
<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="/runbot/run/{{bu.id}}" class="btn btn-info" title="Sign in on this build" aria-label="Sign in on this build">
<i class="fa fa-sign-in"/>
</a>
<span t-else="" t-attf-class="btn btn-default disabled slot_name">
<span t-out="slot.trigger_id.name"/>
</span>
<a t-if="slot.trigger_id.report_view_id" title="View last trigger report" t-attf-href="/runbot/trigger/report/{{slot.trigger_id.id}}" class="fa fa-eye btn btn-info"/>
<a t-if="bu.local_state == 'running' and bu.database_ids" t-attf-href="/runbot/run/{{bu.id}}" class="fa fa-sign-in btn btn-info"/>
<a t-if="bu.static_run" t-att-href="bu.static_run" class="fa fa-sign-in btn btn-info"/>
<t t-if="bu" t-call="runbot.build_menu"/>
<a t-if="not bu" groups="base.group_user" class="btn btn-default" title="Create build" t-attf-href="/runbot/batch/slot/{{slot.id}}/build">
<i class="fa fa-play fa-fw"/>
<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 groups="base.group_user" t-if="bu.local_state=='done' and (not bu.parent_id.database_ids or user_id.has_group('runbot.group_runbot_advanced_user')) 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">
<i class="fa fa-coffee"/>
</a>
<a t-attf-href="/runbot/build/{{bu['id']}}" class="btn btn-default" title="Build details" aria-label="Build details">
<i class="fa fa-file-text-o"/>
</a>
<!--<a t-if="show_commit_button" t-attf-href="https://#{repo.base_url}/commit/#{bu['name']}" class="btn btn-default" title="Open commit on GitHub" aria-label="Open commit on GitHub"><i class="fa fa-github"/></a>-->
<t t-call="runbot.build_menu"/>
</div>
</template>
<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="/runbot/run/{{bu.id}}" 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-info" title="View result" aria-label="View result">
<i class="fa fa-sign-in"/>
</a>
<a groups="base.group_user" t-if="bu.local_state=='done' and (not bu.parent_id.database_ids or user_id.has_group('runbot.group_runbot_advanced_user')) 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">
<i class="fa fa-coffee"/>
</a>
<a t-attf-href="/runbot/build/{{bu['id']}}" class="btn btn-default" title="Build details" aria-label="Build details">
<i class="fa fa-file-text-o"/>
</a>
<!--<a t-if="show_commit_button" t-attf-href="https://#{repo.base_url}/commit/#{bu['name']}" class="btn btn-default" title="Open commit on GitHub" aria-label="Open commit on GitHub"><i class="fa fa-github"/></a>-->
<t t-call="runbot.build_menu"/>
</div>
</div>
</template>
<!-- Event / Logs page -->
<template id="runbot.build_class">
<t t-set="rowclass">info</t>
<t t-if="build.global_state in ['running','done']">
<t t-if="build.global_result == 'ok'">
<t t-set="rowclass">success</t>
</t>
<t t-if="build.global_result == 'skipped'">
<t t-set="rowclass">default</t>
</t>
<t t-if="build.global_result in ['killed', 'manually_killed']">
<t t-set="rowclass">killed</t>
</t>
</div>
</template>
<!-- Event / Logs page -->
<template id="runbot.build_class">
<t t-set="rowclass">info</t>
<t t-if="build.global_state in ['running','done']">
<t t-if="build.global_result == 'ok'">
<t t-set="rowclass">success</t>
</t>
<t t-if="build.global_result == 'ko'">
<t t-set="rowclass">danger</t>
<t t-if="build.global_result == 'skipped'">
<t t-set="rowclass">default</t>
</t>
<t t-if="build.global_result == 'warn'">
<t t-set="rowclass">warning</t>
<t t-if="build.global_result in ['killed', 'manually_killed']">
<t t-set="rowclass">killed</t>
</t>
<t t-out="rowclass"/>
</template>
</t>
<t t-if="build.global_result == 'ko'">
<t t-set="rowclass">danger</t>
</t>
<t t-if="build.global_result == 'warn'">
<t t-set="rowclass">warning</t>
</t>
<t t-out="rowclass"/>
</template>
<template id="runbot.build_menu">
<button t-attf-class="btn btn-default dropdown-toggle" data-bs-toggle="dropdown" title="Build options" aria-label="Build options" aria-expanded="false">
<i t-attf-class="fa {{'fa-spinner' if bu.global_state == 'pending' else 'fa-cog'}} {{'' if bu.global_state in ('done', 'running') else 'fa-spin'}} fa-fw"/>
<span class="caret"/>
</button>
<div class="dropdown-menu dropdown-menu-end" role="menu">
<a t-if="bu.global_result=='skipped'" groups="runbot.group_runbot_admin" class="dropdown-item" href="#" data-runbot="rebuild" t-att-data-runbot-build="bu['id']">
<i class="fa fa-level-up"/>
Force Build
</a>
<t t-if="bu.local_state=='running'">
<t t-foreach="bu.database_ids[1:].sorted('name')" t-as="db">
<a class="dropdown-item" t-attf-href="/runbot/run/{{bu.id}}/{{db.db_suffix}}">
<i class="fa fa-sign-in"/>
Connect <t t-out="db.db_suffix"></t>
</a>
</t>
<a class="dropdown-item" t-attf-href="http://{{bu.domain}}/web/database/selector">
<i class="fa fa-sign-in"/>
Database selector
</a>
</t>
<a class="dropdown-item" t-if="bu.global_state in ['done','running'] or bu.requested_action == 'deathrow'" groups="base.group_user" href="#" data-runbot="rebuild" t-att-data-runbot-build="bu['id']" title="Retry this build, usefull for false positive">
<i class="fa fa-refresh"/>
Rebuild
</a>
<t t-if="bu.global_state != 'done'">
<t t-if="bu.requested_action != 'deathrow'">
<a groups="base.group_user" href="#" data-runbot="kill" class="dropdown-item" t-att-data-runbot-build="bu['id']">
<i class="fa fa-crosshairs"/>
Kill
</a>
</t>
<t t-else="">
<a groups="base.group_user" class="dropdown-item disabled">
<i class="fa fa-spinner fa-spin"/>
Killing
<i class="fa fa-crosshairs"/>
</a>
</t>
</t>
<t t-if="bu.global_state == 'done'">
<t t-if="bu.requested_action != 'wake_up' and bu.database_ids">
<a groups="base.group_user" class="dropdown-item" href="#" data-runbot="wakeup" t-att-data-runbot-build="bu['id']">
<i class="fa fa-coffee"/>
Wake up
</a>
</t>
<t t-if="bu.requested_action == 'wake_up'">
<a groups="base.group_user" class="dropdown-item disabled">
<i class="fa fa-spinner fa-spin"/>
Waking up
<i class="fa fa-crosshairs"/>
</a>
</t>
</t>
<div t-if="bu.global_state not in ('testing', 'waiting', 'pending')" groups="base.group_user" class="dropdown-divider"/>
<t t-set="log_url" t-value="'http://%s' % bu.host if bu.host != fqdn else ''"/>
<t t-if="bu.host" t-foreach="bu.log_list.split(',') if bu.log_list else []" t-as="log_name">
<a class="dropdown-item" t-attf-href="{{log_url}}/runbot/static/build/#{bu.dest}/logs/#{log_name}.txt">
<i class="fa fa-file-text-o"/>
Full
<t t-out="log_name"/>
logs
</a>
</t>
<t groups="runbot.group_runbot_admin">
<div class="dropdown-divider"/>
<a class="dropdown-item" t-attf-href="/runbot/build/search?config_id={{bu.params_id.config_id.id}}&amp;trigger_id={{bu.params_id.trigger_id.id}}&amp;version_id={{bu.params_id.version_id.id}}&amp;create_batch_id.bundle_id={{bu.params_id.create_batch_id.bundle_id.id}}&amp;description={{bu.description or ''}}">
<i class="fa fa-search"/>
Find similar builds
</a>
<a class="dropdown-item"
t-attf-href="/odoo/runbot.build/{{bu['id']}}?menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}" target="new">
<i class="fa fa-list"/>
View in backend
</a>
</t>
</div>
</template>
<template id="runbot.branch_github_menu">
<button t-attf-class="btn btn-default btn-ssm" data-bs-toggle="dropdown" title="Github links" aria-label="Github links" aria-expanded="false">
<i t-attf-class="fa fa-github {{'text-primary' if any(branch_id.is_pr and branch_id.alive for branch_id in bundle.branch_ids) else 'text-secondary' if all(not branch_id.alive for branch_id in bundle.branch_ids) else ''}}"/>
<template id="runbot.build_menu">
<button t-attf-class="btn btn-default dropdown-toggle" data-bs-toggle="dropdown" title="Build options" aria-label="Build options" aria-expanded="false">
<i t-attf-class="fa {{'fa-spinner' if bu.global_state == 'pending' else 'fa-cog'}} {{'' if bu.global_state in ('done', 'running') else 'fa-spin'}} fa-fw"/>
<span class="caret"/>
</button>
<div class="dropdown-menu" role="menu">
<t t-foreach="bundle.branch_ids.sorted(key=lambda b: (not b.alive, b.remote_id.repo_id.sequence, b.remote_id.repo_id.id, b.is_pr, b.id))" t-as="branch">
<t t-set="link_title" t-value="'View %s %s on Github' % ('PR' if branch.is_pr else 'Branch', branch.name)"/>
<a t-att-href="branch.branch_url" class="dropdown-item" t-att-title="link_title">
<span class="fst-italic text-muted" t-out="branch.remote_id.short_name"/> <span t-att-class="'' if branch.alive else 'line-through'" t-out="branch.name"/> <i t-if="not branch.alive" title="deleted/closed" class="fa fa-ban text-danger"/>
</a>
</t>
</div>
</template>
<template id="runbot.branch_copy_button">
<button t-attf-class="btn btn-default {{btn_size or 'btn-ssm'}}" title="Copy Bundle name" aria-label="Copy Bundle name" t-attf-onclick="copyToClipboard('{{ bundle.name.split(':')[-1] }}')">
<i t-attf-class="fa fa-clipboard"/>
</button>
</template>
<template id="runbot.bundle_stats_dropdown">
<button t-attf-class="btn btn-default dropdown-toggle" data-bs-toggle="dropdown" title="Bundle Stats" aria-label="Bundle Stats" aria-expanded="false">
<i t-attf-class="fa fa-bar-chart"/>
<span class="caret"/>
</button>
<div class="dropdown-menu dropdown-menu-start" role="menu">
<t t-foreach="project.trigger_ids.sorted(lambda t: (t.category_id.id, t.sequence, t.id))" t-as="trigger">
<t t-if="trigger.has_stats and not trigger.manual" >
<t t-if="current_category != trigger.category_id">
<span class="dropdown-item"><b><t t-out="trigger.category_id.name"/></b></span>
<t t-set="current_category" t-value="trigger.category_id"/>
</t>
<a class="dropdown-item" t-attf-href="/runbot/stats/{{bundle.id}}/{{trigger.id}}">
<t t-out="trigger.name" />
</a>
</t>
</button>
<div class="dropdown-menu dropdown-menu-end" role="menu">
<a t-if="bu.global_result=='skipped'" groups="runbot.group_runbot_admin" class="dropdown-item" href="#" data-runbot="rebuild" t-att-data-runbot-build="bu['id']">
<i class="fa fa-level-up"/>
Force Build
</a>
<t t-if="bu.local_state=='running'">
<t t-foreach="bu.database_ids[1:].sorted('name')" t-as="db">
<a class="dropdown-item" t-attf-href="/runbot/run/{{bu.id}}/{{db.db_suffix}}">
<i class="fa fa-sign-in"/>
Connect <t t-out="db.db_suffix"></t>
</a>
</t>
</div>
</template>
</data>
<a class="dropdown-item" t-attf-href="http://{{bu.domain}}/web/database/selector">
<i class="fa fa-sign-in"/>
Database selector
</a>
</t>
<a class="dropdown-item" t-if="bu.global_state in ['done','running'] or bu.requested_action == 'deathrow'" groups="base.group_user" href="#" data-runbot="rebuild" t-att-data-runbot-build="bu['id']" title="Retry this build, usefull for false positive">
<i class="fa fa-refresh"/>
Rebuild
</a>
<t t-if="bu.global_state != 'done'">
<t t-if="bu.requested_action != 'deathrow'">
<a groups="base.group_user" href="#" data-runbot="kill" class="dropdown-item" t-att-data-runbot-build="bu['id']">
<i class="fa fa-crosshairs"/>
Kill
</a>
</t>
<t t-else="">
<a groups="base.group_user" class="dropdown-item disabled">
<i class="fa fa-spinner fa-spin"/>
Killing
<i class="fa fa-crosshairs"/>
</a>
</t>
</t>
<t t-if="bu.global_state == 'done'">
<t t-if="bu.requested_action != 'wake_up' and bu.database_ids">
<a groups="base.group_user" class="dropdown-item" href="#" data-runbot="wakeup" t-att-data-runbot-build="bu['id']">
<i class="fa fa-coffee"/>
Wake up
</a>
</t>
<t t-if="bu.requested_action == 'wake_up'">
<a groups="base.group_user" class="dropdown-item disabled">
<i class="fa fa-spinner fa-spin"/>
Waking up
<i class="fa fa-crosshairs"/>
</a>
</t>
</t>
<div t-if="bu.global_state not in ('testing', 'waiting', 'pending')" groups="base.group_user" class="dropdown-divider"/>
<t t-set="log_url" t-value="'http://%s' % bu.host if bu.host != fqdn else ''"/>
<t t-if="bu.host" t-foreach="bu.log_list.split(',') if bu.log_list else []" t-as="log_name">
<a class="dropdown-item" t-attf-href="{{log_url}}/runbot/static/build/#{bu.dest}/logs/#{log_name}.txt">
<i class="fa fa-file-text-o"/>
Full
<t t-out="log_name"/>
logs
</a>
</t>
<t groups="runbot.group_runbot_admin">
<div class="dropdown-divider"/>
<a class="dropdown-item" t-attf-href="/runbot/build/search?config_id={{bu.params_id.config_id.id}}&amp;trigger_id={{bu.params_id.trigger_id.id}}&amp;version_id={{bu.params_id.version_id.id}}&amp;create_batch_id.bundle_id={{bu.params_id.create_batch_id.bundle_id.id}}&amp;description={{bu.description or ''}}">
<i class="fa fa-search"/>
Find similar builds
</a>
<a class="dropdown-item"
t-attf-href="/odoo/runbot.build/{{bu['id']}}?menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}" target="new">
<i class="fa fa-list"/>
View in backend
</a>
</t>
</div>
</template>
<template id="runbot.branch_github_menu">
<button t-attf-class="btn btn-default btn-ssm" data-bs-toggle="dropdown" title="Github links" aria-label="Github links" aria-expanded="false">
<i t-attf-class="fa fa-github {{'text-primary' if any(branch_id.is_pr and branch_id.alive for branch_id in bundle.branch_ids) else 'text-secondary' if all(not branch_id.alive for branch_id in bundle.branch_ids) else ''}}"/>
<span class="caret"/>
</button>
<div class="dropdown-menu" role="menu">
<t t-foreach="bundle.branch_ids.sorted(key=lambda b: (not b.alive, b.remote_id.repo_id.sequence, b.remote_id.repo_id.id, b.is_pr, b.id))" t-as="branch">
<t t-set="link_title" t-value="'View %s %s on Github' % ('PR' if branch.is_pr else 'Branch', branch.name)"/>
<a t-att-href="branch.branch_url" class="dropdown-item" t-att-title="link_title">
<span class="fst-italic text-muted" t-out="branch.remote_id.short_name"/> <span t-att-class="'' if branch.alive else 'line-through'" t-out="branch.name"/> <i t-if="not branch.alive" title="deleted/closed" class="fa fa-ban text-danger"/>
</a>
</t>
</div>
</template>
<template id="runbot.branch_copy_button">
<button t-attf-class="btn btn-default {{btn_size or 'btn-ssm'}}" title="Copy Bundle name" aria-label="Copy Bundle name" t-attf-onclick="copyToClipboard('{{ bundle.name.split(':')[-1] }}')">
<i t-attf-class="fa fa-clipboard"/>
</button>
</template>
<template id="runbot.bundle_stats_dropdown">
<button t-attf-class="btn btn-default dropdown-toggle" data-bs-toggle="dropdown" title="Bundle Stats" aria-label="Bundle Stats" aria-expanded="false">
<i t-attf-class="fa fa-bar-chart"/>
<span class="caret"/>
</button>
<div class="dropdown-menu dropdown-menu-start" role="menu">
<t t-foreach="project.trigger_ids.sorted(lambda t: (t.category_id.id, t.sequence, t.id))" t-as="trigger">
<t t-if="trigger.has_stats and not trigger.manual" >
<t t-if="current_category != trigger.category_id">
<span class="dropdown-item"><b><t t-out="trigger.category_id.name"/></b></span>
<t t-set="current_category" t-value="trigger.category_id"/>
</t>
<a class="dropdown-item" t-attf-href="/runbot/stats/{{bundle.id}}/{{trigger.id}}">
<t t-out="trigger.name" />
</a>
</t>
</t>
</div>
</template>
</odoo>