diff --git a/forwardport/data/views.xml b/forwardport/data/views.xml index 36d427f7..b098d7e5 100644 --- a/forwardport/data/views.xml +++ b/forwardport/data/views.xml @@ -15,7 +15,7 @@ ])"/>
- outstanding forward-ports (>1 week) + outstanding forward-ports (>1 week)
diff --git a/runbot/__manifest__.py b/runbot/__manifest__.py index eab898c6..a445264a 100644 --- a/runbot/__manifest__.py +++ b/runbot/__manifest__.py @@ -36,6 +36,11 @@ 'templates/git.xml', 'templates/nginx.xml', 'templates/build_error.xml', + 'templates/new/page.xml', + 'templates/new/bundle.xml', + 'templates/new/batch.xml', + 'templates/new/build.xml', + 'templates/new/slot.xml', 'views/branch_views.xml', 'views/build_error_views.xml', @@ -65,6 +70,16 @@ 'runbot/static/src/libs/diff_match_patch/diff_match_patch.js', 'runbot/static/src/js/fields/*', ], + 'runbot.assets_frontend_new': [ + # TODO: compile bootstrap scss from /web without reviews and copy custom_utilities.scss + '/runbot/static/src/libs/bootstrap/css/bootstrap.css', + '/runbot/static/src/libs/fontawesome/css/font-awesome.css', + '/runbot/static/src/css/runbot.scss',# TODO: Check what is necessary from here + '/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', + ], 'runbot.assets_frontend': [ '/web/static/lib/bootstrap/dist/css/bootstrap.css', '/web/static/src/libs/fontawesome/css/font-awesome.css', diff --git a/runbot/controllers/frontend.py b/runbot/controllers/frontend.py index af0ae038..3047051b 100644 --- a/runbot/controllers/frontend.py +++ b/runbot/controllers/frontend.py @@ -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 @@ -19,6 +20,32 @@ 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) @@ -73,6 +100,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)) @@ -108,22 +151,17 @@ class Runbot(Controller): '/runbot', '/runbot/', '/runbot//search/'], website=True, auth='public', type='http') - def bundles(self, project=None, search='', projects=False, refresh=False, for_next_freeze=False, limit=40, has_pr=None, **kwargs): + def bundles(self, project=None, search='', projects=False, refresh=False, for_next_freeze=False, limit=40, has_pr=None, old=None, **kwargs): search = search if len(search) < 60 else search[:60] env = request.env categories = env['runbot.category'].search([]) 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: @@ -186,28 +224,32 @@ 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) + return request.render(f'runbot.bundles{"_new" if not old else ""}', context) @route([ '/runbot/bundle/', '/runbot/bundle//page/', '/runbot/bundle/', ], website=True, auth='public', type='http', sitemap=False) - def bundle(self, bundle=None, page=1, limit=50, **kwargs): + def bundle(self, bundle=None, page=1, limit=50, old=None, **kwargs): if isinstance(bundle, str): bundle = request.env['runbot.bundle'].search([('name', '=', bundle)], limit=1, order='id') if not bundle: raise NotFound 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,9 +260,16 @@ 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' % 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='website.pager', + ) } - - return request.render('runbot.bundle', context) + return request.render(f'runbot.bundle{"_new" if not old else ""}', context) @o_route([ '/runbot/bundle//force', @@ -237,15 +286,25 @@ class Runbot(Controller): return werkzeug.utils.redirect('/runbot/batch/%s' % batch.id) @route(['/runbot/batch/'], website=True, auth='public', type='http', sitemap=False) - def batch(self, batch_id=None, **kwargs): + def batch(self, batch_id=None, old=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' % 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) + return request.render(f'runbot.batch{"_new" if not old else ""}', context) @o_route(['/runbot/batch/slot//build'], auth='user', type='http') def slot_create_build(self, slot=None, **kwargs): @@ -312,7 +371,7 @@ class Runbot(Controller): '/runbot/build/', '/runbot/batch//build/' ], type='http', auth="public", website=True, sitemap=False) - def build(self, build_id, search=None, from_batch=None, **post): + def build(self, build_id, search=None, from_batch=None, old=None, **post): """Events/Logs""" if from_batch: @@ -328,10 +387,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' % 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, @@ -340,8 +423,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(f'runbot.build{"_new" if not old else ""}', context) @route([ '/runbot/build/search', diff --git a/runbot/models/batch.py b/runbot/models/batch.py index e617cc0f..7f3aeb35 100644 --- a/runbot/models/batch.py +++ b/runbot/models/batch.py @@ -34,6 +34,10 @@ class Batch(models.Model): column2='referenced_batch_id', ) + def _compute_display_name(self): + for batch in self: + batch.display_name = f'Batch #{batch.id}' + @api.depends('slot_ids.build_id') def _compute_all_build_ids(self): all_builds = self.env['runbot.build'].search([('id', 'child_of', self.slot_ids.build_id.ids)]) diff --git a/runbot/models/build.py b/runbot/models/build.py index bf787b00..7dd1e388 100644 --- a/runbot/models/build.py +++ b/runbot/models/build.py @@ -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' diff --git a/runbot/static/src/css/runbot.css b/runbot/static/src/css/runbot.scss similarity index 60% rename from runbot/static/src/css/runbot.css rename to runbot/static/src/css/runbot.scss index 0ae3ab62..882fdb52 100644 --- a/runbot/static/src/css/runbot.css +++ b/runbot/static/src/css/runbot.scss @@ -12,18 +12,51 @@ --bs-warning-bg-subtle: #fff9e6; --bs-info-bg-subtle: #d9edf7; --bs-info-rgb: 23, 162, 184; + // TODO: move to batch section + .o_runbot_batch_card > .card-header { + --bs-success-rgb: 223, 240, 216; + --bs-info-rgb: 217, 237, 247; + --bs-danger-rgb: 242, 222, 222; + --bs-warning-rgb: 255, 249, 230; + &.text-bg-info, &.text-bg-danger { + color: #000 !important; + } + .badge { + --bs-warning-rgb: 255, 193, 7; + } + } } -:root[data-bs-theme=red404] { +:root[data-bs-theme=red404] { --bs-success-bg-subtle: #cdffb9; --bs-danger-bg-subtle: #e67ecf; --bs-warning-bg-subtle: #fae9b1; --bs-info-bg-subtle: #b6e2f8; --bs-info-rgb: 23, 162, 184; + --bs-success-bg: #cdffb9; + // TODO: move to batch section + .o_runbot_batch_card > .card-header { + --bs-success-rgb: 205, 255, 185; + --bs-info-rgb: 182, 226, 248; + --bs-danger-rgb: 230, 126, 207; + --bs-warning-rgb: 250, 233, 177; + &.text-bg-danger { + color: #fff !important; + } + &.text-bg-success { + color: #000 !important; + } + .badge { + --bs-warning-rgb: 255, 193, 7; + } + } } [data-bs-theme=legacy] .text-bg-info { color: #fff !important; /* black by default, changes from previous version, color forced to fit with --bs-info-rgb*/ } +[data-bs-theme=legacy] .text-bg-success { + color: #000 !important; +} [data-bs-theme=legacy] .btn-success { --bs-btn-color: #fff; @@ -85,7 +118,7 @@ --bs-btn-focus-shadow-rgb: 60, 153, 110; --bs-btn-active-color: var(--btn-default-color); --bs-btn-active-bg: var(--bs-body-bg); - --bs-btn-active-border-color: var(--bs-body-bg); + --bs-btn-active-border-color: var(--btn-default-border); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-disabled-color: var(--btn-default-color); --bs-btn-disabled-bg: var(--bs-body-bg); @@ -221,6 +254,9 @@ body, .table { .dropdown-toggle:after { content: none; } +.dropdown-toggle-caret:after { + content: ' '; +} .one_line { white-space: nowrap; @@ -249,12 +285,13 @@ body, .table { margin-left: auto; } -.bg-killed { - background-color: #aaa; +.bg-killed, .bg-killed-subtle, .bg-skipped, .bg-skipped-subtle { + background-color: var(--bs-tertiary-bg); } .text-bg-killed { - background-color: #aaa; + color: var(--bs-body-color) !important; + background-color: var(--bs-tertiary-bg); } .table-condensed td { @@ -425,4 +462,178 @@ body, .table { code { white-space: pre-wrap; -} \ No newline at end of file +} + + +/* NEW STUFF */ + +.o_runbot_sticky_star { + color: #f0ad4e; +} + +/* Layout */ +.o_runbot_main_container > .row { + padding: .5rem 0; +} + +.o_runbot_main_container > .row:not(:last-child) { + border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; +} + +/* Navbar */ +.o_runbot_navbar_username { + max-width: 16rem; +} + +.o_runbot_btn_haspr { + color: var(--bs-btn-color) !important; + background-color: var(--bs-btn-bg) !important; + border-color: var(--bs-btn-border-color) !important; +} + +.o_runbot_btn_haspr:has(input:checked) { + color: var(--bs-btn-active-color) !important; + background-color: var(--bs-btn-active-bg) !important; + border-color: var(--bs-btn-active-border-color) !important; +} + +/* Bundles */ +@media (min-width: 768px) { + .o_runbot_bundle_info { + width: 16rem; + max-width: 16rem; + } +} + +.o_runbot_bundle_row .o_runbot_batch_card > .o_runbot_batch_card_content > .o_runbot_batch_card_slots > .o_runbot_batch_card_slot { + min-width: 50%; + flex-grow: 1; +} + +@media (max-width: 576px) { + .o_runbot_bundle .o_runbot_batch_card > .o_runbot_batch_card_content > .o_runbot_batch_card_slots > .o_runbot_batch_card_slot { + min-width: 50%; + flex-grow: 1; + } +} +@media (min-width: 576px) { + .o_runbot_bundle .o_runbot_batch_card > .o_runbot_batch_card_content > .o_runbot_batch_card_slots > .o_runbot_batch_card_slot { + flex-basis: 200px; + flex-grow: 1; + max-width: 25%; + } +} + +.o_runbot_bundle > .col-12 { + --bs-gutter-x: 0.5rem; + --bs-gutter-y: 0.5rem; +} + +/* Batch */ +:root[data-bs-theme="legacy"] .o_runbot_batch_card { + /* In legacy theme we want to keep the card's border color */ + border: var(--bs-card-border-width) solid var(--bs-card-border-color) !important; +} + +.o_runbot_batch_card > .o_runbot_batch_card_content > .o_runbot_batch_card_slots > .o_runbot_batch_card_slot > .o_runbot_slot_btn_group { + width: 100%; +} + +.o_runbot_batch_card > .o_runbot_batch_card_content > .o_runbot_batch_card_commits { + font-size: 80%; +} + +.o_runbot_batch > .o_runbot_batch_slots .o_runbot_slot_btn_group { + width: 100%; +} + +/* Slot */ +// .btn-group-ssm > * { +// align-items: center; +// } +.btn-group-ssm { + > * { + align-content: center; + } + .o_runbot_slot_btn_small, .o_runbot_build_menu { + flex: 0 0 25px; + } +} +.o_runbot_slot_btn_group .o_runbot_slot_btn_small, .o_runbot_slot_btn_group > .o_runbot_build_menu { + flex: 0 0 25px; +} + +.o_runbot_log_grid { + display: grid; + grid-template-columns: repeat(3, fit-content(200px)) 1fr; + line-height: normal; + min-width: 0; + + &> *:not(.separator) { + // Same as .border-bottom; + border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; + } + + &> .separator { + grid-column: span 4; + } + + &> .o_runbot_log_grid_header { + font-weight: bold; + padding: 0.5rem 0; + } + + &> .o_runbot_log_grid_date { + padding-left: 0.5rem; + } + + &> *:not(.o_runbot_log_grid_message) { + padding-right: 0.5rem; + } + + &> .o_runbot_log_grid_message { + overflow: hidden; + + &> * { + overflow: auto; + text-wrap: nowrap; + } + + .o_runbot_log_message_toolbox { + opacity: 0.8; + position: absolute; + right: 12px; + + &:hover { + opacity: 1; + + // .btn.disabled has a different opacity + &> * { + --bs-btn-disabled-opacity: 1; + } + } + } +} +} + +/* TODO: remove if not used */ +.bg-danger-subtle-hover:hover { + background-color: var(--bs-danger) !important; + color: #fff !important; +} +.bg-success-subtle-hover:hover { + background-color: var(--bs-success) !important; + color: #fff !important; +} +.bg-warning-subtle-hover:hover { + background-color: var(--bs-warning) !important; + color: #000 !important; +} +.bg-info-subtle-hover:hover { + background-color: var(--bs-info) !important; + color: #000 !important; +} +.bg-killed-subtle-hover:hover { + background-color: var(--body-secondary-bg); + color: var(--body-secondary-color); +} diff --git a/runbot/static/src/js/fields/fields.js b/runbot/static/src/js/fields/fields.js index 5fa95487..6f029641 100644 --- a/runbot/static/src/js/fields/fields.js +++ b/runbot/static/src/js/fields/fields.js @@ -23,7 +23,7 @@ function stringify(obj) { export class JsonField extends TextField { static template = xml` - +
@@ -64,7 +64,7 @@ registry.category("fields").add("runbotjsonb", { export class FrontendUrl extends Component { static template = xml` -
+
`; static components = { Many2OneField }; diff --git a/runbot/static/src/js/fields/tracking_value.xml b/runbot/static/src/js/fields/tracking_value.xml index a3829af8..9aaca233 100644 --- a/runbot/static/src/js/fields/tracking_value.xml +++ b/runbot/static/src/js/fields/tracking_value.xml @@ -9,24 +9,24 @@
-
()
+
()
-
- - + + +
- + - - () + + () diff --git a/runbot/static/src/js/runbot.js b/runbot/static/src/js/runbot.js index 4775c9e7..3f570847 100644 --- a/runbot/static/src/js/runbot.js +++ b/runbot/static/src/js/runbot.js @@ -37,3 +37,40 @@ function copyToClipboard(text) { } navigator.clipboard.writeText(text); } + +const copyHashToClipboard = (hash) => { + if (!navigator.clipboard) { + return + } + navigator.clipboard.writeText(location.origin + location.pathname + `#${hash}`); +} + +const switchTheme = (theme) => { + document.documentElement.dataset.bsTheme = theme; +} + +// setInterval(() => { +// if (document.documentElement.dataset.bsTheme === 'dark') { +// switchTheme('light'); +// } else { +// switchTheme('dark'); +// } +// }, 2000) + +const dark = switchTheme.bind(null, 'dark'); +const legacy = switchTheme.bind(null, 'legacy'); +const light = switchTheme.bind(null, 'light'); +const red404 = switchTheme.bind(null, 'red404'); + +setTimeout(() => { + const navbarElem = document.querySelector('nav.navbar'); + const toolbarElem = document.querySelector('.o_runbot_toolbar.position-sticky'); + + if (navbarElem && toolbarElem) { + toolbarElem.style.top = navbarElem.getBoundingClientRect().height; + new ResizeObserver(() => { + console.log('resize') + toolbarElem.style.top = navbarElem.getBoundingClientRect().height; + }).observe(navbarElem); + } +}, 150); diff --git a/runbot/templates/badge.xml b/runbot/templates/badge.xml index 98f3f289..fdfc7d8d 100644 --- a/runbot/templates/badge.xml +++ b/runbot/templates/badge.xml @@ -15,10 +15,10 @@ - - - - + + + + @@ -36,10 +36,10 @@ - - - - + + + + diff --git a/runbot/templates/batch.xml b/runbot/templates/batch.xml index 8840104d..b54f8c3b 100644 --- a/runbot/templates/batch.xml +++ b/runbot/templates/batch.xml @@ -10,7 +10,7 @@ Bundle - + &emsp; @@ -19,28 +19,28 @@ Category - + Version - + Create date - + Last update - - + + Version reference batches (for upgrade) - + @@ -54,63 +54,63 @@ - + from base: - +
found in branch - + - - + + ( - + ) ( - + , - + )
Base head: - +
Merge base: - +
- Automatic rebase on
+ Automatic rebase on
Subject: - +
Author: - + ( - + )
Committer: - + ( - + )
Commit date: - +

@@ -155,7 +155,7 @@
- + --
diff --git a/runbot/templates/branch.xml b/runbot/templates/branch.xml index d95f6a96..7999a29f 100644 --- a/runbot/templates/branch.xml +++ b/runbot/templates/branch.xml @@ -8,7 +8,7 @@