summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/widgets/debug_manager.js
diff options
context:
space:
mode:
authorAaron Bohy <aab@odoo.com>2016-04-08 14:53:34 +0200
committerGéry Debongnie <ged@odoo.com>2016-04-21 15:21:39 +0200
commit977db823c7c84e9edc7c8cf8384eb9cf47275b3a (patch)
treec3312f745baf11d51f976a60d6e285480f557d4c /addons/web/static/src/js/widgets/debug_manager.js
parente1bfd3fbda97ac7a64a585b07be161d5be606cf9 (diff)
[REF] web: stop duplicating web
Avoid duplicating web addon in enterprise by extracting a common basis. Enterprise features stay in enterprise, but use that common basis. Mainly: - JS refactoring and linting - Conversion of .sass into .less split into multiple files - Templates cleaning and DOM simplification - Re-generation of web.pot, and update of .po files
Diffstat (limited to 'addons/web/static/src/js/widgets/debug_manager.js')
-rw-r--r--addons/web/static/src/js/widgets/debug_manager.js845
1 files changed, 664 insertions, 181 deletions
diff --git a/addons/web/static/src/js/widgets/debug_manager.js b/addons/web/static/src/js/widgets/debug_manager.js
index 74a534ec40e..d95509bb465 100644
--- a/addons/web/static/src/js/widgets/debug_manager.js
+++ b/addons/web/static/src/js/widgets/debug_manager.js
@@ -1,6 +1,8 @@
odoo.define('web.DebugManager', function (require) {
"use strict";
+var ActionManager = require('web.ActionManager');
+var common = require('web.form_common');
var core = require('web.core');
var Dialog = require('web.Dialog');
var formats = require('web.formats');
@@ -8,210 +10,691 @@ var framework = require('web.framework');
var Model = require('web.Model');
var session = require('web.session');
var SystrayMenu = require('web.SystrayMenu');
+var Tour = require('web.Tour');
var utils = require('web.utils');
var ViewManager = require('web.ViewManager');
+var WebClient = require('web.WebClient');
var Widget = require('web.Widget');
var QWeb = core.qweb;
var _t = core._t;
-if (core.debug) {
- var DebugManager = Widget.extend({
- template: "WebClient.DebugManager",
- events: {
- "click .oe_debug_button": "render_dropdown",
- "click .js_debug_dropdown li": "on_debug_click",
- },
- start: function() {
- this._super();
- this.$dropdown = this.$(".js_debug_dropdown");
- },
- /**
- * Updates its attributes according to the inner_widget of the ActionManager
- */
- _update: function() {
- this.view_manager = odoo.__DEBUG__.services['web.web_client'].action_manager.get_inner_widget();
- if (!this.view_manager instanceof ViewManager) { return; }
- this.dataset = this.view_manager.dataset;
- this.active_view = this.view_manager.active_view;
- if (!this.active_view) { return; }
- this.view = this.active_view.controller;
- return true;
+var ADD = function (id) { return [4, id, false]; },
+ REMOVE = function (id) { return [3, id, false]; };
+/**
+ * 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(.open)": function(e) {
+ // Open other dropdowns on mouseover
+ var $opened = this.$('.o_debug_dropdowns > li.open');
+ if($opened.length) {
+ $opened.removeClass('open');
+ $(e.currentTarget).addClass('open').find('> a').focus();
+ }
},
- /**
- * Renders the DebugManager dropdown
- */
- render_dropdown: function() {
- var self = this;
+ },
+ 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('action_pushed', this, this.update);
+ 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')));
+ }
- // Empty the previously rendered dropdown
- this.$dropdown.empty();
+ 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 = false;
+ return $.when(
+ new Model('res.users').call('check_access_rights', {operation: 'write', raise_exception: false}),
+ this.session.user_has_group('base.group_no_one'),
+ new Model('ir.model.data').call('xmlid_to_res_id', {xmlid: 'base.group_no_one'}),
+ this.session.user_has_group('base.group_system'),
+ this._super()
+ ).then(function (can_write_user, has_group_no_one, group_no_one_id, is_admin) {
+ this._features_group = can_write_user && group_no_one_id;
+ this._has_features = has_group_no_one;
+ this._is_admin = is_admin;
+ return this.update();
+ }.bind(this));
+ },
+ leave_debug_mode: function () {
+ var qs = $.deparam.querystring();
+ delete 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;
- // Attempt to retrieve the inner_widget of the ActionManager
- if (!this._update()) {
- // Disable the button when not available
- console.warn("DebugManager is not available");
- return;
+ 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);
+ },
- this.session.user_has_group('base.group_system').then(function(is_admin) {
- // Render the dropdown and append it
- var dropdown_content = QWeb.render('WebClient.DebugDropdown', {
- widget: self,
- active_view: self.active_view,
- view: self.view,
- action: self.view_manager.action,
- searchview: self.view_manager.searchview,
- is_admin: is_admin,
- });
- $(dropdown_content).appendTo(self.$dropdown);
- });
- },
- /**
- * Calls the appropriate callback when clicking on a Debug option
- */
- on_debug_click: 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 debug handler for ", callback);
+ /**
+ * Update the debug manager: reinserts all "universal" controls
+ */
+ update: function () {
+ this.$dropdown
+ .empty()
+ .append(QWeb.render('WebClient.DebugManager.Global', {
+ manager: this,
+ }));
+ return $.when();
+ },
+ start_tour: function() {
+ var dialog = new Dialog(this, {
+ title: 'Tours',
+ $content: QWeb.render('WebClient.DebugManager.ToursDialog', {
+ tours: Tour.tours
+ }),
+ }).open();
+
+ dialog.$('.o_start_tour').on('click', function(e) {
+ e.preventDefault();
+ odoo.__DEBUG__.services['web.Tour'].run($(e.target).data('id'));
+ });
+ },
+ select_view: function () {
+ var self = this;
+ new common.SelectCreateDialog(this, {
+ res_model: 'ir.ui.view',
+ title: _t('Select a view'),
+ disable_multiple_selection: true,
+ on_selected: function (element_ids) {
+ new Model('ir.ui.view')
+ .query(['name', 'model', 'type'])
+ .filter([['id', '=', element_ids[0]]])
+ .first()
+ .then(function (view) {
+ self.do_action({
+ type: 'ir.actions.act_window',
+ name: view.name,
+ res_model: view.model,
+ views: [[view.id, view.type]]
+ });
+ });
}
- },
- get_metadata: function() {
- var ds = this.dataset;
- ds.call('get_metadata', [this.view.get_selected_ids()]).done(function(result) {
- new Dialog(this, {
- title: _.str.sprintf(_t("Metadata (%s)"), ds.model),
- size: 'medium',
- $content: QWeb.render('WebClient.DebugViewLog', {
- perm : result[0],
- format : formats.format_value
- })
- }).open();
- });
- },
- toggle_layout_outline: function() {
- this.view.rendering_engine.toggle_layout_debugging();
- },
- set_defaults: function() {
- this.view.open_defaults_dialog();
- },
- perform_js_tests: function() {
- this.do_action({
- name: _t("JS Tests"),
- target: 'new',
- type : 'ir.actions.act_url',
- url: '/web/tests?mod=*'
+ }).open();
+ },
+ perform_js_tests: function () {
+ this.do_action({
+ name: _t("JS Tests"),
+ target: 'new',
+ type: 'ir.actions.act_url',
+ url: '/web/tests?mod=*'
+ });
+ },
+ toggle_technical_features: function () {
+ if (!this._features_group) { return; }
+ var command = this._has_features ? REMOVE(this._features_group) : ADD(this._features_group);
+ new Model('res.users').call('write', [session.uid, {
+ groups_id: [command]
+ }]).then(function () {
+ window.location.reload();
+ });
+ },
+ split_assets: function() {
+ window.location = $.param.querystring(window.location.href, 'debug=assets');
+ },
+});
+
+/**
+ * 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 self = this;
+ var model = this._action.res_model;
+ new Model(model).call('fields_get', {
+ attributes: ['string', 'searchable', 'required', 'readonly', 'type', 'store', 'sortable', 'relation', 'help']
+ }).done(function (fields) {
+ new Dialog(self, {
+ title: _.str.sprintf(_t("Fields of %s"), model),
+ $content: $(QWeb.render('WebClient.DebugManager.Action.Fields', {
+ fields: fields
+ }))
+ }).open();
+ });
+ },
+ 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
+ }
+ });
+ },
+ edit_workflow: function () {
+ return this.do_action({
+ res_model: 'workflow',
+ name: _t('Edit Workflow'),
+ domain: [['osv', '=', this._action.res_model]],
+ views: [[false, 'list'], [false, 'form'], [false, 'diagram']],
+ type: 'ir.actions.act_window',
+ view_type: 'list',
+ view_mode: 'list'
+ });
+ },
+ translate: function() {
+ var model = this._action.res_model;
+ new Model("ir.translation")
+ .call('get_technical_translations', [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 $.when(
+ this._super(),
+ new Model('ir.ui.view').call(
+ 'check_access_rights', {operation: 'write', raise_exception: false}
+ ).then(function (ar) {
+ this._can_edit_views = ar;
+ }.bind(this))
+ );
+ },
+ update: function (tag, descriptor, widget) {
+ switch (tag) {
+ case 'action':
+ if (!(widget instanceof ViewManager)) {
+ this._active_view = null;
+ this._view_manager = null;
+ break;
+ }
+ this._view_manager = widget;
+ widget.on('switch_mode', this, function () {
+ this.update('view', null, widget);
});
- },
- get_view_fields: function() {
- var self = this;
- self.dataset.call('fields_get', [false, {}]).done(function (fields) {
- var $root = $('<dl>');
- _(fields).each(function (attributes, name) {
- $root.append($('<dt>').append($('<h4>').text(name)));
- var $attrs = $('<dl>').appendTo($('<dd>').appendTo($root));
- _(attributes).each(function (def, name) {
- if (def instanceof Object) {
- def = JSON.stringify(def);
- }
- $attrs
- .append($('<dt>').text(name))
- .append($('<dd style="white-space: pre-wrap;">').text(def));
- });
+ case 'view':
+ this._active_view = widget.active_view;
+ }
+
+ return this._super(tag, descriptor).then(function () {
+ this.$dropdown.find(".o_debug_leave_section").before(QWeb.render('WebClient.DebugManager.View', {
+ manager: this,
+ action: this._action,
+ view: this._active_view,
+ can_edit: this._can_edit_views,
+ searchview: this._view_manager && this._view_manager.searchview,
+ }));
+ }.bind(this));
+ },
+
+ get_metadata: function() {
+ var ds = this._view_manager.dataset;
+ ds.call('get_metadata', [this._active_view.controller.get_selected_ids()]).done(function(result) {
+ new Dialog(this, {
+ title: _.str.sprintf(_t("Metadata (%s)"), ds.model),
+ size: 'medium',
+ $content: QWeb.render('WebClient.DebugViewLog', {
+ perm : result[0],
+ format : formats.format_value
+ })
+ }).open();
+ });
+ },
+ set_defaults: function() {
+ this._active_view.controller.open_defaults_dialog();
+ },
+ fvg: function() {
+ var dialog = new Dialog(this, { title: _t("Fields View Get") }).open();
+ $('<pre>').text(utils.json_node_to_xml(
+ this._active_view.controller.fields_view.arch, true)
+ ).appendTo(dialog.$el);
+ },
+ print_workflow: function() {
+ var ids = this._active_view.controller.get_selected_ids();
+ framework.blockUI();
+ var action = {
+ context: { active_ids: ids },
+ report_name: "workflow.instance.graph",
+ datas: {
+ model: this._view_manager.dataset.model,
+ id: ids[0],
+ nested: true,
+ }
+ };
+ this.session.get_file({
+ url: '/web/report',
+ data: {action: JSON.stringify(action)},
+ complete: framework.unblockUI
+ });
+ },
+});
+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 = ['<p>', text, ' (', (end - start), 'ms)', '</p>'].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++
});
- new Dialog(self, {
- title: _.str.sprintf(_t("Model %s fields"), self.dataset.model),
- $content: $root
- }).open();
- });
- },
- fvg: function() {
- var dialog = new Dialog(this, { title: _t("Fields View Get") }).open();
- $('<pre>').text(utils.json_node_to_xml(this.view.fields_view.arch, true)).appendTo(dialog.$el);
- },
- 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.dataset.model
- }
- });
- },
- translate: function() {
- new Model("ir.translation")
- .call('get_technical_translations', [this.dataset.model])
- .then(this.do_action);
+ } 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();
},
- edit: function(params, evt) {
- this.do_action({
- res_model : params.model,
- res_id : params.id,
- name: evt.target.text,
- type : 'ir.actions.act_window',
- view_type : 'form',
- view_mode : 'form',
- views : [[false, 'form']],
- target : 'new',
- flags : {
- action_buttons : true,
- headless: true,
+ '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 (core.debug) {
+ SystrayMenu.Items.push(DebugManager);
+
+ WebClient.include({
+ start: function() {
+ var self = this;
+ return this._super().then(function () {
+ // Override push_action so that it triggers an event each time a new action is pushed
+ // The DebugManager listens to this event to keep itself up-to-date
+ var push_action = self.action_manager.push_action;
+ self.action_manager.push_action = function(widget, descr) {
+ return push_action.apply(self.action_manager, arguments).then(function () {
+ core.bus.trigger('action_pushed', 'action', descr, widget);
+ });
+ };
});
},
- edit_workflow: function() {
- return this.do_action({
- res_model : 'workflow',
- name: _t('Edit Workflow'),
- domain : [['osv', '=', this.dataset.model]],
- views: [[false, 'list'], [false, 'form'], [false, 'diagram']],
- type : 'ir.actions.act_window',
- view_type : 'list',
- view_mode : 'list'
- });
- },
- print_workflow: function() {
- if (this.view.get_selected_ids && this.view.get_selected_ids().length == 1) {
- framework.blockUI();
- var action = {
- context: { active_ids: this.view.get_selected_ids() },
- report_name: "workflow.instance.graph",
- datas: {
- model: this.dataset.model,
- id: this.view.get_selected_ids()[0],
- nested: true,
- }
- };
- this.session.get_file({
- url: '/web/report',
- data: {action: JSON.stringify(action)},
- complete: framework.unblockUI
+ });
+
+ Dialog.include({
+ open: function() {
+ var self = this;
+
+ var parent = self.getParent();
+ if (parent instanceof ActionManager && parent.dialog_widget) {
+ // Instantiate the DebugManager and insert it into the DOM once dialog is opened
+ this.opened(function() {
+ self.debug_manager = new DebugManager(self);
+ var $header = self.$modal.find('.modal-header');
+ return self.debug_manager.prependTo($header).then(function() {
+ self.debug_manager.update('action', parent.dialog_widget.action, parent.dialog_widget);
+ });
});
- } else {
- this.do_warn("Warning", "No record selected.");
}
- },
- split_assets: function() {
- window.location = $.param.querystring(window.location.href, 'debug=assets');
- },
- leave_debug: function() {
- window.location.search="?";
+
+ return this._super.apply(this, arguments);
},
});
-
- SystrayMenu.Items.push(DebugManager);
-
- return DebugManager;
}
+return DebugManager;
+
});