From fbe5b64eb3d962e1c828e77d848f85a8edfcdde8 Mon Sep 17 00:00:00 2001 From: Romain Derie Date: Mon, 3 Jun 2019 14:26:14 +0000 Subject: [IMP] web: rename file to prepare changes on next commit As this file will be split in 2 parts, this will minimize diff. task-1934445 Comes with odoo/enterprise#4281 Closes #33213 --- addons/web/static/src/js/tools/debug_manager.js | 910 --------------------- .../static/src/js/tools/debug_manager_backend.js | 910 +++++++++++++++++++++ 2 files changed, 910 insertions(+), 910 deletions(-) delete mode 100644 addons/web/static/src/js/tools/debug_manager.js create mode 100644 addons/web/static/src/js/tools/debug_manager_backend.js (limited to 'addons/web/static/src/js') diff --git a/addons/web/static/src/js/tools/debug_manager.js b/addons/web/static/src/js/tools/debug_manager.js deleted file mode 100644 index 29fe1123e1f..00000000000 --- a/addons/web/static/src/js/tools/debug_manager.js +++ /dev/null @@ -1,910 +0,0 @@ -odoo.define('web.DebugManager', function (require) { -"use strict"; - -var ActionManager = require('web.ActionManager'); -var dialogs = require('web.view_dialogs'); -var startClickEverywhere = require('web.clickEverywhere'); -var config = require('web.config'); -var core = require('web.core'); -var Dialog = require('web.Dialog'); -var field_utils = require('web.field_utils'); -var session = require('web.session'); -var SystrayMenu = require('web.SystrayMenu'); -var utils = require('web.utils'); -var WebClient = require('web.WebClient'); -var Widget = require('web.Widget'); - -var QWeb = core.qweb; -var _t = core._t; - -/** - * DebugManager base + general features (applicable to any context) - */ -var DebugManager = Widget.extend({ - template: "WebClient.DebugManager", - events: { - "click a[data-action]": "perform_callback", - "mouseover .o_debug_dropdowns > li:not(.show)": function(e) { - // Open other dropdowns on mouseover - var $opened = this.$('.o_debug_dropdowns > li.show'); - if($opened.length) { - $opened.removeClass('show'); - $(e.currentTarget).addClass('show').find('> a').focus(); - } - }, - }, - init: function () { - this._super.apply(this, arguments); - // 15 fps, only actually call after sequences of queries - this._update_stats = _.throttle( - this._update_stats.bind(this), - 1000/15, {leading: false}); - this._events = null; - if (document.querySelector('meta[name=debug]')) { - this._events = []; - } - }, - start: function () { - core.bus.on('rpc:result', this, function (req, resp) { - this._debug_events(resp.debug); - }); - this.on('update-stats', this, this._update_stats); - var init; - if ((init = document.querySelector('meta[name=debug]'))) { - this._debug_events(JSON.parse(init.getAttribute('value'))); - } - - this.$dropdown = this.$(".o_debug_dropdown"); - // falsy if can't write to user or couldn't find technical features - // group, otherwise features group id - this._features_group = null; - // whether group is currently enabled for current user - this._has_features = false; - // whether the current user is an administrator - this._is_admin = session.is_system; - return Promise.resolve( - this._rpc({ - model: 'res.users', - method: 'check_access_rights', - kwargs: {operation: 'write', raise_exception: false}, - }), - session.user_has_group('base.group_no_one'), - this._rpc({ - model: 'ir.model.data', - method: 'xmlid_to_res_id', - kwargs: {xmlid: 'base.group_no_one'}, - }), - this._super() - ).then(function (can_write_user, has_group_no_one, group_no_one_id) { - this._features_group = can_write_user && group_no_one_id; - this._has_features = has_group_no_one; - return this.update(); - }.bind(this)); - }, - leave_debug_mode: function () { - var qs = $.deparam.querystring(); - qs.debug = ''; - window.location.search = '?' + $.param(qs); - }, /** - * Calls the appropriate callback when clicking on a Debug option - */ - perform_callback: function (evt) { - evt.preventDefault(); - var params = $(evt.target).data(); - var callback = params.action; - - if (callback && this[callback]) { - // Perform the callback corresponding to the option - this[callback](params, evt); - } else { - console.warn("No handler for ", callback); - } - }, - - _debug_events: function (events) { - if (!this._events) { return; } - if (events && events.length) { - this._events.push(events); - } - this.trigger('update-stats', this._events); - }, - requests_clear: function () { - if (!this._events) { return; } - this._events = []; - this.trigger('update-stats', this._events); - }, - _update_stats: function (rqs) { - var requests = 0, rtime = 0, queries = 0, qtime = 0; - for(var r = 0; r < rqs.length; ++r) { - for (var i = 0; i < rqs[r].length; i++) { - var event = rqs[r][i]; - var query_start, request_start; - switch (event[0]) { - case 'request-start': - request_start = event[3] * 1e3; - break; - case 'request-end': - ++requests; - rtime += (event[3] * 1e3 - request_start) | 0; - break; - case 'sql-start': - query_start = event[3] * 1e3; - break; - case 'sql-end': - ++queries; - qtime += (event[3] * 1e3 - query_start) | 0; - break; - } - } - } - this.$('#debugmanager_requests_stats').text( - _.str.sprintf(_t("%d requests (%d ms) %d queries (%d ms)"), - requests, rtime, queries, qtime)); - }, - show_timelines: function () { - if (this._overlay) { - this._overlay.destroy(); - this._overlay = null; - return; - } - this._overlay = new RequestsOverlay(this); - this._overlay.appendTo(document.body); - }, - - /** - * Update the debug manager: reinserts all "universal" controls - */ - update: function () { - this.$dropdown - .empty() - .append(QWeb.render('WebClient.DebugManager.Global', { - manager: this, - })); - return Promise.resolve(); - }, - select_view: function () { - var self = this; - new dialogs.SelectCreateDialog(this, { - res_model: 'ir.ui.view', - title: _t('Select a view'), - disable_multiple_selection: true, - domain: [['type', '!=', 'qweb'], ['type', '!=', 'search']], - on_selected: function (records) { - self._rpc({ - model: 'ir.ui.view', - method: 'search_read', - domain: [['id', '=', records[0].id]], - fields: ['name', 'model', 'type'], - limit: 1, - }) - .then(function (views) { - var view = views[0]; - view.type = view.type === 'tree' ? 'list' : view.type; // ignore tree view - self.do_action({ - type: 'ir.actions.act_window', - name: view.name, - res_model: view.model, - views: [[view.id, view.type]] - }); - }); - } - }).open(); - }, - /** - * Runs the JS (desktop) tests - */ - perform_js_tests: function () { - this.do_action({ - name: _t("JS Tests"), - target: 'new', - type: 'ir.actions.act_url', - url: '/web/tests?mod=*' - }); - }, - /** - * Runs the JS mobile tests - */ - perform_js_mobile_tests: function () { - this.do_action({ - name: _t("JS Mobile Tests"), - target: 'new', - type: 'ir.actions.act_url', - url: '/web/tests/mobile?mod=*' - }); - }, - perform_click_everywhere_test: function () { - var $homeMenu = $("nav.o_main_navbar > a.o_menu_toggle.fa-th"); - $homeMenu.click(); - startClickEverywhere(); - }, - split_assets: function() { - window.location = $.param.querystring(window.location.href, 'debug=assets'); - }, - tests_assets: function () { - window.location = $.param.querystring(window.location.href, 'debug=tests'); - }, - /** - * Delete assets bundles to force their regeneration - * - * @returns {void} - */ - regenerateAssets: function () { - var self = this; - var domain = [ - ['res_model', '=', 'ir.ui.view'], - ['name', 'like', 'assets_'] - ]; - this._rpc({ - model: 'ir.attachment', - method: 'search', - args: [domain], - }).then(function (ids) { - self._rpc({ - model: 'ir.attachment', - method: 'unlink', - args: [ids], - }).then(self.do_action('reload')); - }); - } -}); - -/** - * DebugManager features depending on having an action, and possibly a model - * (window action) - */ -DebugManager.include({ - /** - * Updates current action (action descriptor) on tag = action, - */ - update: function (tag, descriptor) { - if (tag === 'action') { - this._action = descriptor; - } - return this._super().then(function () { - this.$dropdown.find(".o_debug_leave_section").before(QWeb.render('WebClient.DebugManager.Action', { - manager: this, - action: this._action - })); - }.bind(this)); - }, - edit: function (params, evt) { - this.do_action({ - res_model: params.model, - res_id: params.id, - name: evt.target.text, - type: 'ir.actions.act_window', - views: [[false, 'form']], - view_mode: 'form', - target: 'new', - flags: {action_buttons: true, headless: true} - }); - }, - get_view_fields: function () { - var model = this._action.res_model, - self = this; - this._rpc({ - model: 'ir.model', - method: 'search', - args: [[['model', '=', model]]] - }).then(function (ids) { - self.do_action({ - res_model: 'ir.model.fields', - name: _t('View Fields'), - views: [[false, 'list'], [false, 'form']], - domain: [['model_id', '=', model]], - type: 'ir.actions.act_window', - context: { - 'default_model_id': ids[0] - } - }); - }); - }, - manage_filters: function () { - this.do_action({ - res_model: 'ir.filters', - name: _t('Manage Filters'), - views: [[false, 'list'], [false, 'form']], - type: 'ir.actions.act_window', - context: { - search_default_my_filters: true, - search_default_model_id: this._action.res_model - } - }); - }, - translate: function() { - this._rpc({ - model: 'ir.translation', - method: 'get_technical_translations', - args: [this._action.res_model], - }) - .then(this.do_action); - } -}); - -/** - * DebugManager features depending on having a form view or single record. - * These could theoretically be split, but for now they'll be considered one - * and the same. - */ -DebugManager.include({ - start: function () { - this._can_edit_views = false; - return Promise.all([ - this._super(), - this._rpc({ - model: 'ir.ui.view', - method: 'check_access_rights', - kwargs: {operation: 'write', raise_exception: false}, - }) - .then(function (ar) { - this._can_edit_views = ar; - }.bind(this)) - ] - ); - }, - update: function (tag, descriptor, widget) { - if (tag === 'action' || tag === 'view') { - this._controller = widget; - } - return this._super(tag, descriptor).then(function () { - this.$dropdown.find(".o_debug_leave_section").before(QWeb.render('WebClient.DebugManager.View', { - action: this._action, - can_edit: this._can_edit_views, - controller: this._controller, - controlPanelView: this._controller && this._controller._controlPanel, - manager: this, - view: this._controller && _.findWhere(this._action.views, { - type: this._controller.viewType, - }), - })); - }.bind(this)); - }, - get_attachments: function() { - var selectedIDs = this._controller.getSelectedIds(); - if (!selectedIDs.length) { - console.warn(_t("No attachment available")); - return; - } - this.do_action({ - res_model: 'ir.attachment', - name: _t('Manage Attachments'), - views: [[false, 'list'], [false, 'form']], - type: 'ir.actions.act_window', - domain: [['res_model', '=', this._action.res_model], ['res_id', '=', selectedIDs[0]]], - context: { - default_res_model: this._action.res_model, - default_res_id: selectedIDs[0], - }, - }); - }, - get_metadata: function() { - var self = this; - var selectedIDs = this._controller.getSelectedIds(); - if (!selectedIDs.length) { - console.warn(_t("No metadata available")); - return; - } - this._rpc({ - model: this._action.res_model, - method: 'get_metadata', - args: [selectedIDs], - }).then(function(result) { - var metadata = result[0]; - metadata.creator = field_utils.format.many2one(metadata.create_uid); - metadata.lastModifiedBy = field_utils.format.many2one(metadata.write_uid); - var createDate = field_utils.parse.datetime(metadata.create_date); - metadata.create_date = field_utils.format.datetime(createDate); - var modificationDate = field_utils.parse.datetime(metadata.write_date); - metadata.write_date = field_utils.format.datetime(modificationDate); - var dialog = new Dialog(this, { - title: _.str.sprintf(_t("Metadata (%s)"), self._action.res_model), - size: 'medium', - $content: QWeb.render('WebClient.DebugViewLog', { - perm : metadata, - }) - }); - dialog.open().opened(function () { - dialog.$el.on('click', 'a[data-action="toggle_noupdate"]', function (ev) { - ev.preventDefault(); - self._rpc({ - model: 'ir.model.data', - method: 'toggle_noupdate', - args: [self._action.res_model, metadata.id] - }).then(function (res) { - dialog.close(); - self.get_metadata(); - }) - }); - }) - }); - }, - set_defaults: function() { - var self = this; - - var display = function (fieldInfo, value) { - var displayed = value; - if (value && fieldInfo.type === 'many2one') { - displayed = value.data.display_name; - value = value.data.id; - } else if (value && fieldInfo.type === 'selection') { - displayed = _.find(fieldInfo.selection, function (option) { - return option[0] === value; - })[1]; - } - return [value, displayed]; - }; - - var renderer = this._controller.renderer; - var state = renderer.state; - var fields = state.fields; - var fieldsInfo = state.fieldsInfo.form; - var fieldNamesInView = state.getFieldNames(); - var fieldNamesOnlyOnView = ['message_attachment_count']; - var fieldsValues = state.data; - var modifierDatas = {}; - _.each(fieldNamesInView, function (fieldName) { - modifierDatas[fieldName] = _.find(renderer.allModifiersData, function (modifierdata) { - return modifierdata.node.attrs.name === fieldName; - }); - }); - this.fields = _.chain(fieldNamesInView) - .difference(fieldNamesOnlyOnView) - .map(function (fieldName) { - var modifierData = modifierDatas[fieldName]; - var invisibleOrReadOnly; - if (modifierData) { - var evaluatedModifiers = modifierData.evaluatedModifiers[state.id]; - invisibleOrReadOnly = evaluatedModifiers.invisible || evaluatedModifiers.readonly; - } - var fieldInfo = fields[fieldName]; - var valueDisplayed = display(fieldInfo, fieldsValues[fieldName]); - var value = valueDisplayed[0]; - var displayed = valueDisplayed[1]; - // ignore fields which are empty, invisible, readonly, o2m - // or m2m - if (!value || invisibleOrReadOnly || fieldInfo.type === 'one2many' || - fieldInfo.type === 'many2many' || fieldInfo.type === 'binary' || - fieldsInfo[fieldName].options.isPassword || !_.isEmpty(fieldInfo.depends)) { - return false; - } - return { - name: fieldName, - string: fieldInfo.string, - value: value, - displayed: displayed, - }; - }) - .compact() - .sortBy(function (field) { return field.string; }) - .value(); - - var conditions = _.chain(fieldNamesInView) - .filter(function (fieldName) { - var fieldInfo = fields[fieldName]; - return fieldInfo.change_default; - }) - .map(function (fieldName) { - var fieldInfo = fields[fieldName]; - var valueDisplayed = display(fieldInfo, fieldsValues[fieldName]); - var value = valueDisplayed[0]; - var displayed = valueDisplayed[1]; - return { - name: fieldName, - string: fieldInfo.string, - value: value, - displayed: displayed, - }; - }) - .value(); - var d = new Dialog(this, { - title: _t("Set Default"), - buttons: [ - {text: _t("Close"), close: true}, - {text: _t("Save default"), click: function () { - var $defaults = d.$el.find('#formview_default_fields'); - var fieldToSet = $defaults.val(); - if (!fieldToSet) { - $defaults.parent().addClass('o_form_invalid'); - return; - } - var selfUser = d.$el.find('#formview_default_self').is(':checked'); - var condition = d.$el.find('#formview_default_conditions').val(); - var value = _.find(self.fields, function (field) { - return field.name === fieldToSet; - }).value; - self._rpc({ - model: 'ir.default', - method: 'set', - args: [ - self._action.res_model, - fieldToSet, - value, - selfUser, - true, - condition || false, - ], - }).then(function () { d.close(); }); - }} - ] - }); - d.args = { - fields: this.fields, - conditions: conditions, - }; - d.template = 'FormView.set_default'; - d.open(); - }, - fvg: function() { - var self = this; - var dialog = new Dialog(this, { title: _t("Fields View Get") }); - dialog.opened().then(function () { - $('
').text(utils.json_node_to_xml(
-                self._controller.renderer.arch, true)
-            ).appendTo(dialog.$el);
-        });
-        dialog.open();
-    },
-});
-function make_context(width, height, fn) {
-    var canvas = document.createElement('canvas');
-    canvas.width = width;
-    canvas.height = height;
-    // make e.layerX/e.layerY imitate e.offsetX/e.offsetY.
-    canvas.style.position = 'relative';
-    var ctx = canvas.getContext('2d');
-    ctx.imageSmoothingEnabled = false;
-    ctx.mozImageSmoothingEnabled = false;
-    ctx.oImageSmoothingEnabled = false;
-    ctx.webkitImageSmoothingEnabled = false;
-    fn && fn(ctx);
-    return ctx;
-}
-var RequestsOverlay = Widget.extend({
-    template: 'WebClient.DebugManager.RequestsOverlay',
-    TRACKS: 8,
-    TRACK_WIDTH: 9,
-    events: {
-        mousemove: function (e) {
-            this.$tooltip.hide();
-        }
-    },
-    init: function () {
-        this._super.apply(this, arguments);
-        this._render = _.throttle(
-            this._render.bind(this),
-            1000/15, {leading: false}
-        );
-    },
-    start: function () {
-        var _super = this._super();
-        this.$tooltip = this.$('div.o_debug_tooltip');
-        this.getParent().on('update-stats', this, this._render);
-        this._render();
-        return _super;
-    },
-    tooltip: function (text, start, end, x, y) {
-        // x and y are hit point with respect to the viewport. To know where
-        // this hit point is with respect to the overlay, subtract the offset
-        // between viewport and overlay, then add scroll factor of overlay
-        // (which isn't taken in account by the viewport).
-        //
-        // Normally the viewport overlay should sum offsets of all
-        // offsetParents until we reach `null` but in this case the overlay
-        // should have been added directly to the body, which should have an
-        // offset of 0.
-
-        var top = y - this.el.offsetTop + this.el.scrollTop + 1;
-        var left = x - this.el.offsetLeft + this.el.scrollLeft + 1;
-        this.$tooltip.css({top: top, left: left}).show()[0].innerHTML = ['

', text, ' (', (end - start), 'ms)', '

'].join(''); - }, - - _render: function () { - var $summary = this.$('header'), - w = $summary[0].clientWidth, - $requests = this.$('.o_debug_requests'); - $summary.find('canvas').attr('width', w); - var tracks = document.getElementById('o_debug_requests_summary'); - - _.invoke(this.getChildren(), 'destroy'); - - var requests = this.getParent()._events; - var bounds = this._get_bounds(requests); - // horizontal scaling factor for summary - var scale = w / (bounds.high - bounds.low); - - // store end-time of "current" requests, to find out which track a - // request should go in, just look for the first track whose end-time - // is smaller than the new request's start time. - var track_ends = _(this.TRACKS).times(_.constant(-Infinity)); - - var ctx = tracks.getContext('2d'); - ctx.lineWidth = this.TRACK_WIDTH; - for (var i = 0; i < requests.length; i++) { - var request = requests[i]; - // FIXME: is it certain that events in the request are sorted by timestamp? - var rstart = Math.floor(request[0][3] * 1e3); - var rend = Math.ceil(request[request.length - 1][3] * 1e3); - // find free track for current request - for(var track=0; track < track_ends.length; ++track) { - if (track_ends[track] < rstart) { break; } - } - // FIXME: display error message of some sort? Re-render with larger area? Something? - if (track >= track_ends.length) { - console.warn("could not find an empty summary track"); - continue; - } - // set new track end - track_ends[track] = rend; - ctx.save(); - ctx.translate(Math.floor((rstart - bounds.low) * scale), track * (this.TRACK_WIDTH + 1)); - this._draw_request(request, ctx, 0, scale); - ctx.restore(); - new RequestDetails(this, request, scale).appendTo($requests); - } - }, - _draw_request: function (request, to_context, step, hscale, handle_event) { - // have one draw surface for each event type: - // * no need to alter context from one event to the next, each surface - // gets its own color for all its lifetime - // * surfaces can be blended in a specified order, which means events - // can be drawn in any order, no need to care about z-index while - // serializing events to the surfaces - var surfaces = { - request: make_context(to_context.canvas.width, to_context.canvas.height, function (ctx) { - ctx.strokeStyle = 'blue'; - ctx.fillStyle = '#88f'; - ctx.lineJoin = 'round'; - ctx.lineWidth = 1; - }), - //func: make_context(to_context.canvas.width, to_context.canvas.height, function (ctx) { - // ctx.strokeStyle = 'gray'; - // ctx.lineWidth = to_context.lineWidth; - // ctx.translate(0, initial_offset); - //}), - sql: make_context(to_context.canvas.width, to_context.canvas.height, function (ctx) { - ctx.strokeStyle = 'red'; - ctx.fillStyle = '#f88'; - ctx.lineJoin = 'round'; - ctx.lineWidth = 1; - }), - template: make_context(to_context.canvas.width, to_context.canvas.height, function (ctx) { - ctx.strokeStyle = 'green'; - ctx.fillStyle = '#8f8'; - ctx.lineJoin = 'round'; - ctx.lineWidth = 1; - }) - }; - // apply scaling manually so zooming in improves display precision - var stacks = {}, start = Math.floor(request[0][3] * 1e3 * hscale); - var event_idx = 0; - - var rect_width = to_context.lineWidth; - for (var i = 0; i < request.length; i++) { - var type, m, event = request[i]; - var tag = event[0], timestamp = Math.floor(event[3] * 1e3 * hscale) - start; - - if (m = /(\w+)-start/.exec(tag)) { - type = m[1]; - if (!(type in stacks)) { stacks[type] = []; } - handle_event && handle_event(event_idx, timestamp, event); - stacks[type].push({ - timestamp: timestamp, - idx: event_idx++ - }); - } else if (m = /(\w+)-end/.exec(tag)) { - type = m[1]; - var stack = stacks[type]; - var estart = stack.pop(), duration = Math.ceil(timestamp - estart.timestamp); - handle_event && handle_event(estart.idx, timestamp, event); - - var surface = surfaces[type]; - if (!surface) { continue; } // FIXME: support for unknown event types - - var y = step * estart.idx; - // path rectangle for the current event on the relevant surface - surface.rect(estart.timestamp + 0.5, y + 0.5, duration || 1, rect_width); - } - } - // add each layer to the main canvas - var keys = ['request', /*'func', */'template', 'sql']; - for (var j = 0; j < keys.length; ++j) { - // stroke and fill all rectangles for the relevant surface/context - var ctx = surfaces[keys[j]]; - ctx.fill(); - ctx.stroke(); - to_context.drawImage(ctx.canvas, 0, 0); - } - }, - /** - * Returns first and last events in milliseconds - * - * @param requests - * @returns {{low: number, high: number}} - * @private - */ - _get_bounds: function (requests) { - var low = +Infinity; - var high =-+Infinity; - - for (var i = 0; i < requests.length; i++) { - var request = requests[i]; - for (var j = 0; j < request.length; j++) { - var event = request[j]; - var timestamp = event[3]; - low = Math.min(low, timestamp); - high = Math.max(high, timestamp); - } - } - return {low: Math.floor(low * 1e3), high: Math.ceil(high * 1e3)}; - } -}); -var RequestDetails = Widget.extend({ - events: { - click: function () { - this._open = !this._open; - this.render(); - }, - 'mousemove canvas': function (e) { - e.stopPropagation(); - var y = e.y || e.offsetY || e.layerY; - if (!y) { return; } - var event = this._payloads[Math.floor(y / this._REQ_HEIGHT)]; - if (!event) { return; } - - this.getParent().tooltip(event.payload, event.start, event.stop, e.clientX, e.clientY); - } - }, - init: function (parent, request, scale) { - this._super.apply(this, arguments); - this._request = request; - this._open = false; - this._scale = scale; - this._REQ_HEIGHT = 20; - }, - start: function () { - this.el.style.borderBottom = '1px solid black'; - this.render(); - return this._super(); - }, - render: function () { - var request_cell_height = this._REQ_HEIGHT, TITLE_WIDTH = 200; - var request = this._request; - var req_start = request[0][3] * 1e3; - var req_duration = request[request.length - 1][3] * 1e3 - req_start; - var height = request_cell_height * (this._open ? request.length / 2 : 1); - var cell_center = request_cell_height / 2; - var ctx = make_context(210 + Math.ceil(req_duration * this._scale), height, function (ctx) { - ctx.lineWidth = cell_center; - }); - this.$el.empty().append(ctx.canvas); - var payloads = this._payloads = []; - - // lazy version: if the render is single-line (!this._open), the extra - // content will be discarded when the text canvas gets pasted onto the - // main canvas. An improvement would be to not do text rendering - // beyond the first event for "closed" requests events… then again - // that makes for more regular rendering profile? - var text_ctx = make_context(TITLE_WIDTH, height, function (ctx) { - ctx.font = '12px sans-serif'; - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.translate(0, cell_center); - }); - - ctx.save(); - ctx.translate(TITLE_WIDTH + 10, ((request_cell_height/4)|0)); - - this.getParent()._draw_request(request, ctx, this._open ? request_cell_height : 0, this._scale, function (idx, timestamp, event) { - if (/-start$/g.test(event[0])) { - payloads.push({ - payload: event[2], - start: timestamp, - stop: null - }); - - // we want ~200px wide, assume the average character is at - // least 4px wide => there can be *at most* 49 characters - var title = event[2]; - title = title.replace(/\s+$/, ''); - title = title.length <= 50 ? title : ('…' + title.slice(-49)); - while (text_ctx.measureText(title).width > 200) { - title = '…' + title.slice(2); - } - text_ctx.fillText(title, TITLE_WIDTH, request_cell_height * idx); - } else if (/-end$/g.test(event[0])) { - payloads[idx].stop = timestamp; - } - }); - ctx.restore(); - // add the text layer to the main canvas - ctx.drawImage(text_ctx.canvas, 0, 0); - } -}); - -if (config.debug) { - SystrayMenu.Items.push(DebugManager); - - WebClient.include({ - //---------------------------------------------------------------------- - // Public - //---------------------------------------------------------------------- - - /** - * @override - */ - current_action_updated: function (action, controller) { - this._super.apply(this, arguments); - var debugManager = _.find(this.menu.systray_menu.widgets, function(item) { - return item instanceof DebugManager; - }); - debugManager.update('action', action, controller && controller.widget); - }, - }); - - ActionManager.include({ - //---------------------------------------------------------------------- - // Public - //---------------------------------------------------------------------- - - /** - * Returns the action of the controller currently opened in a dialog, - * i.e. a target='new' action, if any. - * - * @returns {Object|null} - */ - getCurrentActionInDialog: function () { - if (this.currentDialogController) { - return this.actions[this.currentDialogController.actionID]; - } - return null; - }, - /** - * Returns the controller currently opened in a dialog, if any. - * - * @returns {Object|null} - */ - getCurrentControllerInDialog: function () { - return this.currentDialogController; - }, - }); - - Dialog.include({ - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - */ - open: function() { - var self = this; - // if the dialog is opened by the ActionManager, instantiate a - // DebugManager and insert it into the DOM once the dialog is opened - // (delay this with a setTimeout(0) to ensure that the internal - // state, i.e. the current action and controller, of the - // ActionManager is set to properly update the DebugManager) - this.opened(function() { - setTimeout(function () { - var parent = self.getParent(); - if (parent instanceof ActionManager) { - var action = parent.getCurrentActionInDialog(); - if (action) { - var controller = parent.getCurrentControllerInDialog(); - self.debugManager = new DebugManager(self); - var $header = self.$modal.find('.modal-header:first'); - return self.debugManager.prependTo($header).then(function () { - self.debugManager.update('action', action, controller.widget); - }); - } - } - }, 0); - }); - - return this._super.apply(this, arguments); - }, - }); -} - -return DebugManager; - -}); diff --git a/addons/web/static/src/js/tools/debug_manager_backend.js b/addons/web/static/src/js/tools/debug_manager_backend.js new file mode 100644 index 00000000000..29fe1123e1f --- /dev/null +++ b/addons/web/static/src/js/tools/debug_manager_backend.js @@ -0,0 +1,910 @@ +odoo.define('web.DebugManager', function (require) { +"use strict"; + +var ActionManager = require('web.ActionManager'); +var dialogs = require('web.view_dialogs'); +var startClickEverywhere = require('web.clickEverywhere'); +var config = require('web.config'); +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var field_utils = require('web.field_utils'); +var session = require('web.session'); +var SystrayMenu = require('web.SystrayMenu'); +var utils = require('web.utils'); +var WebClient = require('web.WebClient'); +var Widget = require('web.Widget'); + +var QWeb = core.qweb; +var _t = core._t; + +/** + * DebugManager base + general features (applicable to any context) + */ +var DebugManager = Widget.extend({ + template: "WebClient.DebugManager", + events: { + "click a[data-action]": "perform_callback", + "mouseover .o_debug_dropdowns > li:not(.show)": function(e) { + // Open other dropdowns on mouseover + var $opened = this.$('.o_debug_dropdowns > li.show'); + if($opened.length) { + $opened.removeClass('show'); + $(e.currentTarget).addClass('show').find('> a').focus(); + } + }, + }, + init: function () { + this._super.apply(this, arguments); + // 15 fps, only actually call after sequences of queries + this._update_stats = _.throttle( + this._update_stats.bind(this), + 1000/15, {leading: false}); + this._events = null; + if (document.querySelector('meta[name=debug]')) { + this._events = []; + } + }, + start: function () { + core.bus.on('rpc:result', this, function (req, resp) { + this._debug_events(resp.debug); + }); + this.on('update-stats', this, this._update_stats); + var init; + if ((init = document.querySelector('meta[name=debug]'))) { + this._debug_events(JSON.parse(init.getAttribute('value'))); + } + + this.$dropdown = this.$(".o_debug_dropdown"); + // falsy if can't write to user or couldn't find technical features + // group, otherwise features group id + this._features_group = null; + // whether group is currently enabled for current user + this._has_features = false; + // whether the current user is an administrator + this._is_admin = session.is_system; + return Promise.resolve( + this._rpc({ + model: 'res.users', + method: 'check_access_rights', + kwargs: {operation: 'write', raise_exception: false}, + }), + session.user_has_group('base.group_no_one'), + this._rpc({ + model: 'ir.model.data', + method: 'xmlid_to_res_id', + kwargs: {xmlid: 'base.group_no_one'}, + }), + this._super() + ).then(function (can_write_user, has_group_no_one, group_no_one_id) { + this._features_group = can_write_user && group_no_one_id; + this._has_features = has_group_no_one; + return this.update(); + }.bind(this)); + }, + leave_debug_mode: function () { + var qs = $.deparam.querystring(); + qs.debug = ''; + window.location.search = '?' + $.param(qs); + }, /** + * Calls the appropriate callback when clicking on a Debug option + */ + perform_callback: function (evt) { + evt.preventDefault(); + var params = $(evt.target).data(); + var callback = params.action; + + if (callback && this[callback]) { + // Perform the callback corresponding to the option + this[callback](params, evt); + } else { + console.warn("No handler for ", callback); + } + }, + + _debug_events: function (events) { + if (!this._events) { return; } + if (events && events.length) { + this._events.push(events); + } + this.trigger('update-stats', this._events); + }, + requests_clear: function () { + if (!this._events) { return; } + this._events = []; + this.trigger('update-stats', this._events); + }, + _update_stats: function (rqs) { + var requests = 0, rtime = 0, queries = 0, qtime = 0; + for(var r = 0; r < rqs.length; ++r) { + for (var i = 0; i < rqs[r].length; i++) { + var event = rqs[r][i]; + var query_start, request_start; + switch (event[0]) { + case 'request-start': + request_start = event[3] * 1e3; + break; + case 'request-end': + ++requests; + rtime += (event[3] * 1e3 - request_start) | 0; + break; + case 'sql-start': + query_start = event[3] * 1e3; + break; + case 'sql-end': + ++queries; + qtime += (event[3] * 1e3 - query_start) | 0; + break; + } + } + } + this.$('#debugmanager_requests_stats').text( + _.str.sprintf(_t("%d requests (%d ms) %d queries (%d ms)"), + requests, rtime, queries, qtime)); + }, + show_timelines: function () { + if (this._overlay) { + this._overlay.destroy(); + this._overlay = null; + return; + } + this._overlay = new RequestsOverlay(this); + this._overlay.appendTo(document.body); + }, + + /** + * Update the debug manager: reinserts all "universal" controls + */ + update: function () { + this.$dropdown + .empty() + .append(QWeb.render('WebClient.DebugManager.Global', { + manager: this, + })); + return Promise.resolve(); + }, + select_view: function () { + var self = this; + new dialogs.SelectCreateDialog(this, { + res_model: 'ir.ui.view', + title: _t('Select a view'), + disable_multiple_selection: true, + domain: [['type', '!=', 'qweb'], ['type', '!=', 'search']], + on_selected: function (records) { + self._rpc({ + model: 'ir.ui.view', + method: 'search_read', + domain: [['id', '=', records[0].id]], + fields: ['name', 'model', 'type'], + limit: 1, + }) + .then(function (views) { + var view = views[0]; + view.type = view.type === 'tree' ? 'list' : view.type; // ignore tree view + self.do_action({ + type: 'ir.actions.act_window', + name: view.name, + res_model: view.model, + views: [[view.id, view.type]] + }); + }); + } + }).open(); + }, + /** + * Runs the JS (desktop) tests + */ + perform_js_tests: function () { + this.do_action({ + name: _t("JS Tests"), + target: 'new', + type: 'ir.actions.act_url', + url: '/web/tests?mod=*' + }); + }, + /** + * Runs the JS mobile tests + */ + perform_js_mobile_tests: function () { + this.do_action({ + name: _t("JS Mobile Tests"), + target: 'new', + type: 'ir.actions.act_url', + url: '/web/tests/mobile?mod=*' + }); + }, + perform_click_everywhere_test: function () { + var $homeMenu = $("nav.o_main_navbar > a.o_menu_toggle.fa-th"); + $homeMenu.click(); + startClickEverywhere(); + }, + split_assets: function() { + window.location = $.param.querystring(window.location.href, 'debug=assets'); + }, + tests_assets: function () { + window.location = $.param.querystring(window.location.href, 'debug=tests'); + }, + /** + * Delete assets bundles to force their regeneration + * + * @returns {void} + */ + regenerateAssets: function () { + var self = this; + var domain = [ + ['res_model', '=', 'ir.ui.view'], + ['name', 'like', 'assets_'] + ]; + this._rpc({ + model: 'ir.attachment', + method: 'search', + args: [domain], + }).then(function (ids) { + self._rpc({ + model: 'ir.attachment', + method: 'unlink', + args: [ids], + }).then(self.do_action('reload')); + }); + } +}); + +/** + * DebugManager features depending on having an action, and possibly a model + * (window action) + */ +DebugManager.include({ + /** + * Updates current action (action descriptor) on tag = action, + */ + update: function (tag, descriptor) { + if (tag === 'action') { + this._action = descriptor; + } + return this._super().then(function () { + this.$dropdown.find(".o_debug_leave_section").before(QWeb.render('WebClient.DebugManager.Action', { + manager: this, + action: this._action + })); + }.bind(this)); + }, + edit: function (params, evt) { + this.do_action({ + res_model: params.model, + res_id: params.id, + name: evt.target.text, + type: 'ir.actions.act_window', + views: [[false, 'form']], + view_mode: 'form', + target: 'new', + flags: {action_buttons: true, headless: true} + }); + }, + get_view_fields: function () { + var model = this._action.res_model, + self = this; + this._rpc({ + model: 'ir.model', + method: 'search', + args: [[['model', '=', model]]] + }).then(function (ids) { + self.do_action({ + res_model: 'ir.model.fields', + name: _t('View Fields'), + views: [[false, 'list'], [false, 'form']], + domain: [['model_id', '=', model]], + type: 'ir.actions.act_window', + context: { + 'default_model_id': ids[0] + } + }); + }); + }, + manage_filters: function () { + this.do_action({ + res_model: 'ir.filters', + name: _t('Manage Filters'), + views: [[false, 'list'], [false, 'form']], + type: 'ir.actions.act_window', + context: { + search_default_my_filters: true, + search_default_model_id: this._action.res_model + } + }); + }, + translate: function() { + this._rpc({ + model: 'ir.translation', + method: 'get_technical_translations', + args: [this._action.res_model], + }) + .then(this.do_action); + } +}); + +/** + * DebugManager features depending on having a form view or single record. + * These could theoretically be split, but for now they'll be considered one + * and the same. + */ +DebugManager.include({ + start: function () { + this._can_edit_views = false; + return Promise.all([ + this._super(), + this._rpc({ + model: 'ir.ui.view', + method: 'check_access_rights', + kwargs: {operation: 'write', raise_exception: false}, + }) + .then(function (ar) { + this._can_edit_views = ar; + }.bind(this)) + ] + ); + }, + update: function (tag, descriptor, widget) { + if (tag === 'action' || tag === 'view') { + this._controller = widget; + } + return this._super(tag, descriptor).then(function () { + this.$dropdown.find(".o_debug_leave_section").before(QWeb.render('WebClient.DebugManager.View', { + action: this._action, + can_edit: this._can_edit_views, + controller: this._controller, + controlPanelView: this._controller && this._controller._controlPanel, + manager: this, + view: this._controller && _.findWhere(this._action.views, { + type: this._controller.viewType, + }), + })); + }.bind(this)); + }, + get_attachments: function() { + var selectedIDs = this._controller.getSelectedIds(); + if (!selectedIDs.length) { + console.warn(_t("No attachment available")); + return; + } + this.do_action({ + res_model: 'ir.attachment', + name: _t('Manage Attachments'), + views: [[false, 'list'], [false, 'form']], + type: 'ir.actions.act_window', + domain: [['res_model', '=', this._action.res_model], ['res_id', '=', selectedIDs[0]]], + context: { + default_res_model: this._action.res_model, + default_res_id: selectedIDs[0], + }, + }); + }, + get_metadata: function() { + var self = this; + var selectedIDs = this._controller.getSelectedIds(); + if (!selectedIDs.length) { + console.warn(_t("No metadata available")); + return; + } + this._rpc({ + model: this._action.res_model, + method: 'get_metadata', + args: [selectedIDs], + }).then(function(result) { + var metadata = result[0]; + metadata.creator = field_utils.format.many2one(metadata.create_uid); + metadata.lastModifiedBy = field_utils.format.many2one(metadata.write_uid); + var createDate = field_utils.parse.datetime(metadata.create_date); + metadata.create_date = field_utils.format.datetime(createDate); + var modificationDate = field_utils.parse.datetime(metadata.write_date); + metadata.write_date = field_utils.format.datetime(modificationDate); + var dialog = new Dialog(this, { + title: _.str.sprintf(_t("Metadata (%s)"), self._action.res_model), + size: 'medium', + $content: QWeb.render('WebClient.DebugViewLog', { + perm : metadata, + }) + }); + dialog.open().opened(function () { + dialog.$el.on('click', 'a[data-action="toggle_noupdate"]', function (ev) { + ev.preventDefault(); + self._rpc({ + model: 'ir.model.data', + method: 'toggle_noupdate', + args: [self._action.res_model, metadata.id] + }).then(function (res) { + dialog.close(); + self.get_metadata(); + }) + }); + }) + }); + }, + set_defaults: function() { + var self = this; + + var display = function (fieldInfo, value) { + var displayed = value; + if (value && fieldInfo.type === 'many2one') { + displayed = value.data.display_name; + value = value.data.id; + } else if (value && fieldInfo.type === 'selection') { + displayed = _.find(fieldInfo.selection, function (option) { + return option[0] === value; + })[1]; + } + return [value, displayed]; + }; + + var renderer = this._controller.renderer; + var state = renderer.state; + var fields = state.fields; + var fieldsInfo = state.fieldsInfo.form; + var fieldNamesInView = state.getFieldNames(); + var fieldNamesOnlyOnView = ['message_attachment_count']; + var fieldsValues = state.data; + var modifierDatas = {}; + _.each(fieldNamesInView, function (fieldName) { + modifierDatas[fieldName] = _.find(renderer.allModifiersData, function (modifierdata) { + return modifierdata.node.attrs.name === fieldName; + }); + }); + this.fields = _.chain(fieldNamesInView) + .difference(fieldNamesOnlyOnView) + .map(function (fieldName) { + var modifierData = modifierDatas[fieldName]; + var invisibleOrReadOnly; + if (modifierData) { + var evaluatedModifiers = modifierData.evaluatedModifiers[state.id]; + invisibleOrReadOnly = evaluatedModifiers.invisible || evaluatedModifiers.readonly; + } + var fieldInfo = fields[fieldName]; + var valueDisplayed = display(fieldInfo, fieldsValues[fieldName]); + var value = valueDisplayed[0]; + var displayed = valueDisplayed[1]; + // ignore fields which are empty, invisible, readonly, o2m + // or m2m + if (!value || invisibleOrReadOnly || fieldInfo.type === 'one2many' || + fieldInfo.type === 'many2many' || fieldInfo.type === 'binary' || + fieldsInfo[fieldName].options.isPassword || !_.isEmpty(fieldInfo.depends)) { + return false; + } + return { + name: fieldName, + string: fieldInfo.string, + value: value, + displayed: displayed, + }; + }) + .compact() + .sortBy(function (field) { return field.string; }) + .value(); + + var conditions = _.chain(fieldNamesInView) + .filter(function (fieldName) { + var fieldInfo = fields[fieldName]; + return fieldInfo.change_default; + }) + .map(function (fieldName) { + var fieldInfo = fields[fieldName]; + var valueDisplayed = display(fieldInfo, fieldsValues[fieldName]); + var value = valueDisplayed[0]; + var displayed = valueDisplayed[1]; + return { + name: fieldName, + string: fieldInfo.string, + value: value, + displayed: displayed, + }; + }) + .value(); + var d = new Dialog(this, { + title: _t("Set Default"), + buttons: [ + {text: _t("Close"), close: true}, + {text: _t("Save default"), click: function () { + var $defaults = d.$el.find('#formview_default_fields'); + var fieldToSet = $defaults.val(); + if (!fieldToSet) { + $defaults.parent().addClass('o_form_invalid'); + return; + } + var selfUser = d.$el.find('#formview_default_self').is(':checked'); + var condition = d.$el.find('#formview_default_conditions').val(); + var value = _.find(self.fields, function (field) { + return field.name === fieldToSet; + }).value; + self._rpc({ + model: 'ir.default', + method: 'set', + args: [ + self._action.res_model, + fieldToSet, + value, + selfUser, + true, + condition || false, + ], + }).then(function () { d.close(); }); + }} + ] + }); + d.args = { + fields: this.fields, + conditions: conditions, + }; + d.template = 'FormView.set_default'; + d.open(); + }, + fvg: function() { + var self = this; + var dialog = new Dialog(this, { title: _t("Fields View Get") }); + dialog.opened().then(function () { + $('
').text(utils.json_node_to_xml(
+                self._controller.renderer.arch, true)
+            ).appendTo(dialog.$el);
+        });
+        dialog.open();
+    },
+});
+function make_context(width, height, fn) {
+    var canvas = document.createElement('canvas');
+    canvas.width = width;
+    canvas.height = height;
+    // make e.layerX/e.layerY imitate e.offsetX/e.offsetY.
+    canvas.style.position = 'relative';
+    var ctx = canvas.getContext('2d');
+    ctx.imageSmoothingEnabled = false;
+    ctx.mozImageSmoothingEnabled = false;
+    ctx.oImageSmoothingEnabled = false;
+    ctx.webkitImageSmoothingEnabled = false;
+    fn && fn(ctx);
+    return ctx;
+}
+var RequestsOverlay = Widget.extend({
+    template: 'WebClient.DebugManager.RequestsOverlay',
+    TRACKS: 8,
+    TRACK_WIDTH: 9,
+    events: {
+        mousemove: function (e) {
+            this.$tooltip.hide();
+        }
+    },
+    init: function () {
+        this._super.apply(this, arguments);
+        this._render = _.throttle(
+            this._render.bind(this),
+            1000/15, {leading: false}
+        );
+    },
+    start: function () {
+        var _super = this._super();
+        this.$tooltip = this.$('div.o_debug_tooltip');
+        this.getParent().on('update-stats', this, this._render);
+        this._render();
+        return _super;
+    },
+    tooltip: function (text, start, end, x, y) {
+        // x and y are hit point with respect to the viewport. To know where
+        // this hit point is with respect to the overlay, subtract the offset
+        // between viewport and overlay, then add scroll factor of overlay
+        // (which isn't taken in account by the viewport).
+        //
+        // Normally the viewport overlay should sum offsets of all
+        // offsetParents until we reach `null` but in this case the overlay
+        // should have been added directly to the body, which should have an
+        // offset of 0.
+
+        var top = y - this.el.offsetTop + this.el.scrollTop + 1;
+        var left = x - this.el.offsetLeft + this.el.scrollLeft + 1;
+        this.$tooltip.css({top: top, left: left}).show()[0].innerHTML = ['

', text, ' (', (end - start), 'ms)', '

'].join(''); + }, + + _render: function () { + var $summary = this.$('header'), + w = $summary[0].clientWidth, + $requests = this.$('.o_debug_requests'); + $summary.find('canvas').attr('width', w); + var tracks = document.getElementById('o_debug_requests_summary'); + + _.invoke(this.getChildren(), 'destroy'); + + var requests = this.getParent()._events; + var bounds = this._get_bounds(requests); + // horizontal scaling factor for summary + var scale = w / (bounds.high - bounds.low); + + // store end-time of "current" requests, to find out which track a + // request should go in, just look for the first track whose end-time + // is smaller than the new request's start time. + var track_ends = _(this.TRACKS).times(_.constant(-Infinity)); + + var ctx = tracks.getContext('2d'); + ctx.lineWidth = this.TRACK_WIDTH; + for (var i = 0; i < requests.length; i++) { + var request = requests[i]; + // FIXME: is it certain that events in the request are sorted by timestamp? + var rstart = Math.floor(request[0][3] * 1e3); + var rend = Math.ceil(request[request.length - 1][3] * 1e3); + // find free track for current request + for(var track=0; track < track_ends.length; ++track) { + if (track_ends[track] < rstart) { break; } + } + // FIXME: display error message of some sort? Re-render with larger area? Something? + if (track >= track_ends.length) { + console.warn("could not find an empty summary track"); + continue; + } + // set new track end + track_ends[track] = rend; + ctx.save(); + ctx.translate(Math.floor((rstart - bounds.low) * scale), track * (this.TRACK_WIDTH + 1)); + this._draw_request(request, ctx, 0, scale); + ctx.restore(); + new RequestDetails(this, request, scale).appendTo($requests); + } + }, + _draw_request: function (request, to_context, step, hscale, handle_event) { + // have one draw surface for each event type: + // * no need to alter context from one event to the next, each surface + // gets its own color for all its lifetime + // * surfaces can be blended in a specified order, which means events + // can be drawn in any order, no need to care about z-index while + // serializing events to the surfaces + var surfaces = { + request: make_context(to_context.canvas.width, to_context.canvas.height, function (ctx) { + ctx.strokeStyle = 'blue'; + ctx.fillStyle = '#88f'; + ctx.lineJoin = 'round'; + ctx.lineWidth = 1; + }), + //func: make_context(to_context.canvas.width, to_context.canvas.height, function (ctx) { + // ctx.strokeStyle = 'gray'; + // ctx.lineWidth = to_context.lineWidth; + // ctx.translate(0, initial_offset); + //}), + sql: make_context(to_context.canvas.width, to_context.canvas.height, function (ctx) { + ctx.strokeStyle = 'red'; + ctx.fillStyle = '#f88'; + ctx.lineJoin = 'round'; + ctx.lineWidth = 1; + }), + template: make_context(to_context.canvas.width, to_context.canvas.height, function (ctx) { + ctx.strokeStyle = 'green'; + ctx.fillStyle = '#8f8'; + ctx.lineJoin = 'round'; + ctx.lineWidth = 1; + }) + }; + // apply scaling manually so zooming in improves display precision + var stacks = {}, start = Math.floor(request[0][3] * 1e3 * hscale); + var event_idx = 0; + + var rect_width = to_context.lineWidth; + for (var i = 0; i < request.length; i++) { + var type, m, event = request[i]; + var tag = event[0], timestamp = Math.floor(event[3] * 1e3 * hscale) - start; + + if (m = /(\w+)-start/.exec(tag)) { + type = m[1]; + if (!(type in stacks)) { stacks[type] = []; } + handle_event && handle_event(event_idx, timestamp, event); + stacks[type].push({ + timestamp: timestamp, + idx: event_idx++ + }); + } else if (m = /(\w+)-end/.exec(tag)) { + type = m[1]; + var stack = stacks[type]; + var estart = stack.pop(), duration = Math.ceil(timestamp - estart.timestamp); + handle_event && handle_event(estart.idx, timestamp, event); + + var surface = surfaces[type]; + if (!surface) { continue; } // FIXME: support for unknown event types + + var y = step * estart.idx; + // path rectangle for the current event on the relevant surface + surface.rect(estart.timestamp + 0.5, y + 0.5, duration || 1, rect_width); + } + } + // add each layer to the main canvas + var keys = ['request', /*'func', */'template', 'sql']; + for (var j = 0; j < keys.length; ++j) { + // stroke and fill all rectangles for the relevant surface/context + var ctx = surfaces[keys[j]]; + ctx.fill(); + ctx.stroke(); + to_context.drawImage(ctx.canvas, 0, 0); + } + }, + /** + * Returns first and last events in milliseconds + * + * @param requests + * @returns {{low: number, high: number}} + * @private + */ + _get_bounds: function (requests) { + var low = +Infinity; + var high =-+Infinity; + + for (var i = 0; i < requests.length; i++) { + var request = requests[i]; + for (var j = 0; j < request.length; j++) { + var event = request[j]; + var timestamp = event[3]; + low = Math.min(low, timestamp); + high = Math.max(high, timestamp); + } + } + return {low: Math.floor(low * 1e3), high: Math.ceil(high * 1e3)}; + } +}); +var RequestDetails = Widget.extend({ + events: { + click: function () { + this._open = !this._open; + this.render(); + }, + 'mousemove canvas': function (e) { + e.stopPropagation(); + var y = e.y || e.offsetY || e.layerY; + if (!y) { return; } + var event = this._payloads[Math.floor(y / this._REQ_HEIGHT)]; + if (!event) { return; } + + this.getParent().tooltip(event.payload, event.start, event.stop, e.clientX, e.clientY); + } + }, + init: function (parent, request, scale) { + this._super.apply(this, arguments); + this._request = request; + this._open = false; + this._scale = scale; + this._REQ_HEIGHT = 20; + }, + start: function () { + this.el.style.borderBottom = '1px solid black'; + this.render(); + return this._super(); + }, + render: function () { + var request_cell_height = this._REQ_HEIGHT, TITLE_WIDTH = 200; + var request = this._request; + var req_start = request[0][3] * 1e3; + var req_duration = request[request.length - 1][3] * 1e3 - req_start; + var height = request_cell_height * (this._open ? request.length / 2 : 1); + var cell_center = request_cell_height / 2; + var ctx = make_context(210 + Math.ceil(req_duration * this._scale), height, function (ctx) { + ctx.lineWidth = cell_center; + }); + this.$el.empty().append(ctx.canvas); + var payloads = this._payloads = []; + + // lazy version: if the render is single-line (!this._open), the extra + // content will be discarded when the text canvas gets pasted onto the + // main canvas. An improvement would be to not do text rendering + // beyond the first event for "closed" requests events… then again + // that makes for more regular rendering profile? + var text_ctx = make_context(TITLE_WIDTH, height, function (ctx) { + ctx.font = '12px sans-serif'; + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.translate(0, cell_center); + }); + + ctx.save(); + ctx.translate(TITLE_WIDTH + 10, ((request_cell_height/4)|0)); + + this.getParent()._draw_request(request, ctx, this._open ? request_cell_height : 0, this._scale, function (idx, timestamp, event) { + if (/-start$/g.test(event[0])) { + payloads.push({ + payload: event[2], + start: timestamp, + stop: null + }); + + // we want ~200px wide, assume the average character is at + // least 4px wide => there can be *at most* 49 characters + var title = event[2]; + title = title.replace(/\s+$/, ''); + title = title.length <= 50 ? title : ('…' + title.slice(-49)); + while (text_ctx.measureText(title).width > 200) { + title = '…' + title.slice(2); + } + text_ctx.fillText(title, TITLE_WIDTH, request_cell_height * idx); + } else if (/-end$/g.test(event[0])) { + payloads[idx].stop = timestamp; + } + }); + ctx.restore(); + // add the text layer to the main canvas + ctx.drawImage(text_ctx.canvas, 0, 0); + } +}); + +if (config.debug) { + SystrayMenu.Items.push(DebugManager); + + WebClient.include({ + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * @override + */ + current_action_updated: function (action, controller) { + this._super.apply(this, arguments); + var debugManager = _.find(this.menu.systray_menu.widgets, function(item) { + return item instanceof DebugManager; + }); + debugManager.update('action', action, controller && controller.widget); + }, + }); + + ActionManager.include({ + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * Returns the action of the controller currently opened in a dialog, + * i.e. a target='new' action, if any. + * + * @returns {Object|null} + */ + getCurrentActionInDialog: function () { + if (this.currentDialogController) { + return this.actions[this.currentDialogController.actionID]; + } + return null; + }, + /** + * Returns the controller currently opened in a dialog, if any. + * + * @returns {Object|null} + */ + getCurrentControllerInDialog: function () { + return this.currentDialogController; + }, + }); + + Dialog.include({ + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + open: function() { + var self = this; + // if the dialog is opened by the ActionManager, instantiate a + // DebugManager and insert it into the DOM once the dialog is opened + // (delay this with a setTimeout(0) to ensure that the internal + // state, i.e. the current action and controller, of the + // ActionManager is set to properly update the DebugManager) + this.opened(function() { + setTimeout(function () { + var parent = self.getParent(); + if (parent instanceof ActionManager) { + var action = parent.getCurrentActionInDialog(); + if (action) { + var controller = parent.getCurrentControllerInDialog(); + self.debugManager = new DebugManager(self); + var $header = self.$modal.find('.modal-header:first'); + return self.debugManager.prependTo($header).then(function () { + self.debugManager.update('action', action, controller.widget); + }); + } + } + }, 0); + }); + + return this._super.apply(this, arguments); + }, + }); +} + +return DebugManager; + +}); -- cgit v1.2.3