Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[REF] runbot: rewrite stats page using owl #1035

Open
wants to merge 8 commits into
base: 18.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions runbot/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,20 @@
'runbot/static/src/libs/diff_match_patch/diff_match_patch.js',
'runbot/static/src/js/fields/*',
],
'runbot.assets_stats': [
# Required for module loading
('include', 'web.assets_frontend_minimal'),
# Required for separate js and xml files
'/web/static/src/core/template_inheritance.js',
'/web/static/src/core/templates.js', # ^
# Owl
'web/static/lib/owl/owl.js',
'web/static/lib/owl/odoo_module.js',
# Runbot
'/runbot/static/src/utils.js',
'/runbot/static/src/chartjs_module.js',
'/runbot/static/src/stats/**/*',
],
'runbot.assets_frontend': [
'/web/static/lib/bootstrap/dist/css/bootstrap.css',
'/web/static/src/libs/fontawesome/css/font-awesome.css',
Expand Down
143 changes: 97 additions & 46 deletions runbot/controllers/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,48 @@ def submit(self, more=False, redirect='/', keep_search=False, category=False, fi
response.set_cookie(key, '-'.join(enabled_triggers))
return response

def _get_bundles(self, /, project, search='', has_pr=None, for_next_freeze=False, limit=40, **_):
domain = [('last_batch', '!=', False), ('project_id', '=', project.id)]
if not search:
domain.append(('no_build', '=', False))

if has_pr is not None:
domain.append(('has_pr', '=', bool(has_pr)))

filter_mode = request.httprequest.cookies.get('filter_mode', False)
if filter_mode == 'sticky':
domain.append(('sticky', '=', True))
elif filter_mode == 'nosticky':
domain.append(('sticky', '=', False))

if for_next_freeze:
domain.append(('for_next_freeze', '=', True))

if search:
search_domains = []
pr_numbers = []
for search_elem in search.split("|"):
if search_elem.isnumeric():
pr_numbers.append(int(search_elem))
operator = '=ilike' if '%' in search_elem else 'ilike'
search_domains.append([('name', operator, search_elem)])
if pr_numbers:
res = request.env['runbot.branch'].search([('name', 'in', pr_numbers)])
if res:
search_domains.append([('id', 'in', res.mapped('bundle_id').ids)])
search_domain = expression.OR(search_domains)
domain = expression.AND([domain, search_domain])

e = expression.expression(domain, request.env['runbot.bundle'])
query = e.query
query.order = """
(case when "runbot_bundle".sticky then 1 when "runbot_bundle".sticky is null then 2 else 2 end),
case when "runbot_bundle".sticky then "runbot_bundle".version_number end collate "C" desc,
"runbot_bundle".last_batch desc
"""
query.limit = min(int(limit), 200)
return request.env['runbot.bundle'].browse(query)

@route(['/',
'/runbot',
'/runbot/<model("runbot.project"):project>',
Expand All @@ -126,46 +168,7 @@ def bundles(self, project=None, search='', projects=False, refresh=False, for_ne
'hosts_data': request.env['runbot.host'].search([('assigned_only', '=', False)]),
}
if project:
domain = [('last_batch', '!=', False), ('project_id', '=', project.id)]
if not search:
domain.append(('no_build', '=', False))

if has_pr is not None:
domain.append(('has_pr', '=', bool(has_pr)))

filter_mode = request.httprequest.cookies.get('filter_mode', False)
if filter_mode == 'sticky':
domain.append(('sticky', '=', True))
elif filter_mode == 'nosticky':
domain.append(('sticky', '=', False))

if for_next_freeze:
domain.append(('for_next_freeze', '=', True))

if search:
search_domains = []
pr_numbers = []
for search_elem in search.split("|"):
if search_elem.isnumeric():
pr_numbers.append(int(search_elem))
operator = '=ilike' if '%' in search_elem else 'ilike'
search_domains.append([('name', operator, search_elem)])
if pr_numbers:
res = request.env['runbot.branch'].search([('name', 'in', pr_numbers)])
if res:
search_domains.append([('id', 'in', res.mapped('bundle_id').ids)])
search_domain = expression.OR(search_domains)
domain = expression.AND([domain, search_domain])

e = expression.expression(domain, request.env['runbot.bundle'])
query = e.query
query.order = """
(case when "runbot_bundle".sticky then 1 when "runbot_bundle".sticky is null then 2 else 2 end),
case when "runbot_bundle".sticky then "runbot_bundle".version_number end collate "C" desc,
"runbot_bundle".last_batch desc
"""
query.limit = min(int(limit), 200)
bundles = env['runbot.bundle'].browse(query)
bundles = self._get_bundles(project=project, search=search, has_pr=has_pr, for_next_freeze=for_next_freeze, limit=limit)

category_id = int(request.httprequest.cookies.get('category') or 0) or request.env['ir.model.data']._xmlid_to_res_id('runbot.default_category')

Expand Down Expand Up @@ -557,9 +560,18 @@ def build_stats(self, build_id, search=None, **post):
}
return request.render("runbot.build_stats", context)

@route([
'/runbot/bundles_json',
'/runbot/bundles_json/<model("runbot.project"):project>',
'/runbot/bundles_json/<model("runbot.project"):project>/search/<search>'], type='http', auth='public', website=False, sitemap=False)
def bundles_json(self, project=None, projects=False, **kwargs):
if not project and projects:
project = projects[0]
bundles = self._get_bundles(project=project, **kwargs)
return request.make_json_response(bundles.read(['id', 'name']))

@route(['/runbot/stats/'], type='json', auth="public", website=False, sitemap=False)
def stats_json(self, bundle_id=False, trigger_id=False, key_category='', center_build_id=False, ok_only=False, limit=100, search=None, **post):
def stats_json(self, bundle_id=False, trigger_id=False, key_category='', center_build_id=False, ok_only=False, limit=100, search=None, add_bundles='', **post):
""" Json stats """
trigger_id = trigger_id and int(trigger_id)
bundle_id = bundle_id and int(bundle_id)
Expand All @@ -571,17 +583,23 @@ def stats_json(self, bundle_id=False, trigger_id=False, key_category='', center_
if not trigger_id or not bundle_id or not trigger.exists() or not bundle.exists():
return request.not_found()

bundle_ids = [bundle_id]
for bundle_id in add_bundles.split(','):
if not bundle_id.isdigit():
continue
bundle_ids.append(int(bundle_id))

builds_domain = [
('global_state', 'in', ('running', 'done')),
('slot_ids.batch_id.bundle_id', '=', bundle_id),
('slot_ids.batch_id.bundle_id', 'in', bundle_ids),
('params_id.trigger_id', '=', trigger.id),
]
if ok_only:
builds_domain += ('global_result', '=', 'ok')
builds = request.env['runbot.build'].with_context(active_test=False)
if center_build_id:
builds = builds.search(
expression.AND([builds_domain, [('id', '>=', center_build_id)]]),
expression.AND([builds_domain, [('id', '>', center_build_id)]]),
order='id', limit=limit/2)
builds_domain = expression.AND([builds_domain, [('id', '<=', center_build_id)]])
limit -= len(builds)
Expand All @@ -593,13 +611,28 @@ def stats_json(self, bundle_id=False, trigger_id=False, key_category='', center_
builds = builds.search([('id', 'child_of', builds.ids)])

parents = {b.id: b.top_parent.id for b in builds.with_context(prefetch_fields=False)}
# Prefetch bundle name, we need to be able to bind a
builds.with_context(prefetch_fields=False).fetch([
'create_date', 'slot_ids',
])
res_arr = [
{
'id': b.id,
'values': {},
'create_date': b.create_date.isoformat(),
'bundle_id': b.slot_ids.batch_id.bundle_id.id,
}
for b in builds.with_context(prefetch_fields=False).sorted('id')
]
res_dict = {r['id']: r['values'] for r in res_arr}
request.env.cr.execute("SELECT build_id, values FROM runbot_build_stat WHERE build_id IN %s AND category = %s", [tuple(builds.ids), key_category]) # read manually is way faster than using orm
res = {}
for (build_id, values) in request.env.cr.fetchall():
if values:
res.setdefault(parents[build_id], {}).update(values)
res_dict[parents[build_id]].update(values)
# we need to update here to manage the post install case: we want to combine stats from all post_install childrens.
return res
# Filter out results without values
res_arr = [r for r in res_arr if r['values']]
return res_arr

@route(['/runbot/stats/<model("runbot.bundle"):bundle>/<model("runbot.trigger"):trigger>'], type='http', auth="public", website=True, sitemap=False)
def modules_stats(self, bundle, trigger, search=None, **post):
Expand All @@ -620,10 +653,28 @@ def list_config_categories(config):

categories = sorted(categories)

triggers = bundle.project_id.trigger_ids.filtered(
lambda t: t.has_stats and not t.manual
).sorted(
lambda t: (t.category_id.id, t.sequence, t.id)
)
triggers_by_category = defaultdict(list)
slug = request.env['ir.http']._slug
for trig in triggers:
triggers_by_category[trig.category_id.name].append(
{
'id': trig.id,
'slug': slug(trig),
'name': trig.name,
},
)
context = {
'stats_categories': categories,
'bundle': bundle,
'trigger': trigger,
'project': bundle.project_id,
# Category name -> List of trigger name + id
'triggers_by_category': triggers_by_category
}

return request.render("runbot.modules_stats", context)
Expand Down
6 changes: 3 additions & 3 deletions runbot/models/build_stat.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ class BuildStat(models.Model):

_sql_constraints = [
(
"build_config_key_unique",
"unique (build_id, config_step_id, category)",
"category_build_config_step_unique_key",
"unique (category, build_id, config_step_id)",
"Build stats must be unique for the same build step",
)
]
Expand All @@ -23,5 +23,5 @@ class BuildStat(models.Model):
config_step_id = fields.Many2one(
"runbot.build.config.step", "Step", ondelete="cascade"
)
category = fields.Char("Category", index=True)
category = fields.Char("Category")
values = JsonDictField("Value")
5 changes: 5 additions & 0 deletions runbot/static/src/chartjs_module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
odoo.define("@runbot/chartjs", [], function () {
"use strict";

return Chart;
});
29 changes: 0 additions & 29 deletions runbot/static/src/css/runbot.css
Original file line number Diff line number Diff line change
Expand Up @@ -352,35 +352,6 @@ body, .table {
margin-left: -1px;
}*/

.chart-legend {
max-height: calc(100vh - 160px);
overflow-y: scroll;
overflow-x: hidden;
cursor: pointer;
padding: 5px;
}

.chart-legend .label {
margin-left: 5px;
font-weight: bold;
}

.chart-legend .disabled .color {
visibility: hidden;
}

.chart-legend .disabled .label {
font-weight: normal;
text-decoration: line-through;
margin-left: 5px;
}

.chart-legend ul {
list-style-type: none;
margin: 0;
padding: 0;
}

.limited-height {
max-height: 180px;
overflow: scroll;
Expand Down
Loading