From cb243bebd04ca92fc5c8d64af2ec9ad17fe95caa Mon Sep 17 00:00:00 2001
From: Philip ABBET <philip.abbet@idiap.ch>
Date: Tue, 26 Sep 2017 08:56:31 +0200
Subject: [PATCH] [experiments] Refactoring of the 'Experiment results' page

---
 beat/web/experiments/serializers.py           |  35 +-
 .../static/experiments/js/panels.js           | 293 +++++++++++
 .../static/experiments/js/utils.js            | 111 ----
 .../templates/experiments/view.html           | 487 ++++++++++--------
 .../toolchains/static/toolchains/js/models.js |  18 +
 buildout.cfg                                  |   1 +
 6 files changed, 622 insertions(+), 323 deletions(-)
 mode change 100644 => 100755 beat/web/experiments/serializers.py

diff --git a/beat/web/experiments/serializers.py b/beat/web/experiments/serializers.py
old mode 100644
new mode 100755
index bab80c88b..4dadd9e09
--- a/beat/web/experiments/serializers.py
+++ b/beat/web/experiments/serializers.py
@@ -26,6 +26,7 @@
 ###############################################################################
 
 from rest_framework import serializers
+from django.contrib.humanize.templatetags.humanize import naturaltime
 
 from ..common.serializers import ShareableSerializer
 from ..common.fields import JSONSerializerField
@@ -198,18 +199,22 @@ class ExperimentResultsSerializer(ShareableSerializer):
     attestation = serializers.IntegerField(source='attestation.number')
     blocks_status = serializers.SerializerMethodField()
     execution_info = serializers.SerializerMethodField()
+    execution_order = serializers.SerializerMethodField()
+    status = serializers.CharField(source="get_status_display")
     results = serializers.SerializerMethodField()
     errors = serializers.SerializerMethodField()
     html_description = serializers.SerializerMethodField()
     description = serializers.SerializerMethodField()
     declaration = JSONSerializerField()
+    display_start_date = serializers.SerializerMethodField()
+    display_end_date = serializers.SerializerMethodField()
 
     class Meta(ShareableSerializer.Meta):
         model = Experiment
-        default_fields = ['started', 'done', 'failed', 'blocks_status',
+        default_fields = ['started', 'done', 'failed', 'status', 'blocks_status',
                           'results', 'attestation', 'declaration',
                           'errors', 'sharing', 'accessibility',
-                          'execution_info']
+                          'execution_info', 'execution_order']
 
     def get_started(self, obj):
         return obj.status not in [Experiment.PENDING, Experiment.SCHEDULED, Experiment.CANCELING]
@@ -229,6 +234,8 @@ class ExperimentResultsSerializer(ShareableSerializer):
                 results[block.name] = 'pending'
             elif block.status == Block.FAILED:
                 results[block.name] = 'failed'
+            elif block.status == Block.CANCELLED:
+                results[block.name] = 'cancelled'
             else:
                 results[block.name] = 'processing'
         return results
@@ -244,6 +251,12 @@ class ExperimentResultsSerializer(ShareableSerializer):
             }
         return results
 
+    def get_execution_order(self, obj):
+        results = []
+        for block in obj.blocks.order_by('execution_order').iterator():
+            results.append(block.name)
+        return results
+
     def get_results(self, obj):
         results = {}
         for k in obj.blocks.filter(analyzer=True).all():
@@ -263,3 +276,21 @@ class ExperimentResultsSerializer(ShareableSerializer):
 
     def get_description(self, obj):
         return obj.description.decode('utf-8')
+
+    def get_display_start_date(self, obj):
+        if obj.start_date is None:
+            return None
+
+        return {
+            'date': obj.start_date.strftime("%b %d, %Y, %-H:%M"),
+            'natural': naturaltime(obj.start_date),
+        }
+
+    def get_display_end_date(self, obj):
+        if obj.end_date is None:
+            return None
+
+        return {
+            'date': obj.end_date.strftime("%b %d, %Y, %-H:%M"),
+            'natural': naturaltime(obj.end_date),
+        }
diff --git a/beat/web/experiments/static/experiments/js/panels.js b/beat/web/experiments/static/experiments/js/panels.js
index aa0912f34..209dc576c 100644
--- a/beat/web/experiments/static/experiments/js/panels.js
+++ b/beat/web/experiments/static/experiments/js/panels.js
@@ -3375,3 +3375,296 @@ beat.experiments.panels.BlockParameters.prototype._createStringControls = functi
           $(select).addClass('default');
       });
 }
+
+
+/********************** CLASS: ExecutionDetails ************************************/
+
+//----------------------------------------------------------------------
+// Constructor
+//
+// panel_id: ID of the DOM element to use
+//----------------------------------------------------------------------
+beat.experiments.panels.ExecutionDetails = function(panel_id, url_prefix) {
+
+  this.id                 = panel_id;
+  this.element            = $('#' + panel_id)[0];
+  this.configuration      = null;
+  this.url_prefix         = url_prefix;
+}
+
+
+//----------------------------------------------------------------------
+// Setup the content of the panel from an experiment configuration
+//----------------------------------------------------------------------
+beat.experiments.panels.ExecutionDetails.prototype.setup = function(
+    configuration, templates, urls, experiment_results)
+{
+    this.configuration = configuration;
+
+    var rendered = this._createGlobalsSection(templates, urls);
+    rendered += this._createDatasetsSection(templates, urls);
+    rendered += this._createBlockSections(templates, urls, experiment_results);
+
+    $(this.element).html(rendered);
+}
+
+
+//----------------------------------------------------------------------
+// Update the content of the panel from the experiment results
+//----------------------------------------------------------------------
+beat.experiments.panels.ExecutionDetails.prototype.update = function(experiment_results)
+{
+    for (var i = 0; i < experiment_results.execution_order.length; ++i) {
+        var block_name = experiment_results.execution_order[i];
+        var status = experiment_results.blocks_status[block_name];
+
+        var div = $(this.element).find('#block-' + block_name);
+        if (div.data('beat-status') != status) {
+            div.data('beat-status', status);
+
+            div.find('#block-' + block_name + '-status-icon')[0].className = this._getIcon(status);
+
+            if ((status == 'failed') || (status == 'generated')) {
+                var execution_info = experiment_results.execution_info[block_name];
+
+                var span = div.find('#block-' + block_name + '-execution-time');
+                span.text(' @ ' + execution_info.linear_execution_time.toFixed(2) + 's')
+                span.show();
+
+                var li = div.find('#block-' + block_name + '-linear-execution-time');
+                li.text('Total execution time: ' + execution_info.linear_execution_time.toFixed(2) + 's')
+                li.show();
+
+                if (this.configuration.blockNbSlots(block_name) > 1) {
+                    li = div.find('#block-' + block_name + '-speed-up-real');
+                    li.text('Slots: ' + this.configuration.blockNbSlots(block_name) + ' (speed-up achieved: ' + execution_info.speed_up_real.toFixed(1) + 'x')
+                    li.show();
+
+                    li = div.find('#block-' + block_name + '-speed-up-maximal');
+                    li.text('Maximum speed-up achievable: ' + execution_info.speed_up_maximal.toFixed(1) + 'x')
+                    li.show();
+                } else {
+                    div.find('#block-' + block_name + '-slots').show();
+                }
+
+                li = div.find('#block-' + block_name + '-queuing-time');
+                li.text('Queuing time: ' + execution_info.queuing_time.toFixed(2) + 's')
+                li.show();
+
+                if (experiment_results.errors) {
+                    for (var j = 0; j < experiment_results.errors.length; ++j) {
+                        if (experiment_results.errors[j].block == block_name) {
+                            if (experiment_results.errors[j].details) {
+                                li = div.find('#block-' + block_name + '-error-report');
+                                li.find('.console-output').text(experiment_results.errors[j].details);
+                                li.show();
+                            }
+
+                            if (experiment_results.errors[j].stdout) {
+                                li = div.find('#block-' + block_name + '-stdout');
+                                li.find('.console-output').text(experiment_results.errors[j].stdout);
+                                li.show();
+                            }
+
+                            if (experiment_results.errors[j].stderr) {
+                                li = div.find('#block-' + block_name + '-stderr');
+                                li.find('.console-output').text(experiment_results.errors[j].stderr);
+                                li.show();
+                            }
+
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+
+//----------------------------------------------------------------------
+// Creates the 'Global Parameters' section
+//----------------------------------------------------------------------
+beat.experiments.panels.ExecutionDetails.prototype._createGlobalsSection = function(
+    templates, urls)
+{
+    var template = $(templates.globals).html();
+
+    Mustache.parse(template);
+
+    var environment = this.configuration.defaultEnvironment();
+
+    var tags = {
+        environment: {
+            name: environment.name,
+            version: environment.version,
+            url: urls.environment.replace('NAME', environment.name).replace('VERSION', environment.version),
+        },
+        queue: this.configuration.defaultQueue(),
+        algorithms: [],
+    }
+
+    var algorithm_names = this.configuration.algorithms();
+    for (var i = 0; i < algorithm_names.length; ++i) {
+        var algorithm_name = algorithm_names[i];
+        var algorithm_globals = this.configuration.algorithmGlobals(algorithm_name);
+
+        var items = [];
+
+        var parameters = Object.keys(algorithm_globals);
+        for (var j = 0; j < parameters.length; ++j) {
+            items.push({
+                name: parameters[j],
+                value: algorithm_globals[parameters[j]],
+            });
+        }
+
+        if (items.length > 0) {
+            tags.algorithms.push({
+                name: algorithm_name,
+                url: urls.algorithm.replace('AUTHOR/NAME/0', algorithm_name),
+                items: items,
+            });
+        }
+    }
+
+    return Mustache.render(template, tags);
+}
+
+
+//----------------------------------------------------------------------
+// Creates the 'Datasets' section
+//----------------------------------------------------------------------
+beat.experiments.panels.ExecutionDetails.prototype._createDatasetsSection = function(
+    templates, urls)
+{
+    var template = $(templates.datasets).html();
+
+    Mustache.parse(template);
+
+    var tags = {
+        datasets: [],
+    }
+
+    var dataset_names = this.configuration.datasetNames();
+    for (var i = 0; i < dataset_names.length; ++i) {
+        var dataset_name = dataset_names[i];
+        var dataset = this.configuration.dataset(dataset_name);
+
+        tags.datasets.push({
+            block_name: dataset_name,
+            url: urls.database.replace('NAME/0', dataset.database),
+            name: dataset.database,
+            protocol: dataset.protocol,
+            set: dataset.set,
+        });
+    }
+
+    return Mustache.render(template, tags);
+}
+
+
+//----------------------------------------------------------------------
+// Creates the 'Block' sections
+//----------------------------------------------------------------------
+beat.experiments.panels.ExecutionDetails.prototype._createBlockSections = function(
+    templates, urls, results)
+{
+    var template = $(templates.block).html();
+
+    Mustache.parse(template);
+
+    var tags = {
+        blocks: [],
+    }
+
+    for (var i = 0; i < results.execution_order.length; ++i) {
+        var block_name = results.execution_order[i];
+        var block = this.configuration.component(block_name);
+
+        var execution_info = results.execution_info[block_name];
+        var status = results.blocks_status[block_name];
+
+        var environment = this.configuration.blockEnvironment(block_name);
+
+        var entry = {
+            name: block_name,
+            analyzer: this.configuration.is_analyzer(block_name),
+            algorithm: block.algorithm,
+            url: urls.algorithm.replace('AUTHOR/NAME/0', block.algorithm),
+
+            status: status,
+            failed: status == 'failed',
+            cached: status == 'generated',
+            cached_or_failed: (status == 'failed') || (status == 'generated'),
+
+            nb_slots: (this.configuration.blockNbSlots(block_name) > 1 ? this.configuration.blockNbSlots(block_name) : 0),
+            environment: {
+                name: environment.name,
+                version: environment.version,
+                url: urls.environment.replace('NAME', environment.name).replace('VERSION', environment.version),
+            },
+            queue: this.configuration.blockQueue(block_name),
+
+            has_parameters: this.configuration.isParametrized(block_name),
+            parameters: [],
+
+            queuing_time: execution_info.queuing_time.toFixed(2),
+            linear_execution_time: execution_info.linear_execution_time.toFixed(2),
+            speed_up_real: execution_info.speed_up_real.toFixed(1),
+            speed_up_maximal: execution_info.speed_up_maximal.toFixed(1),
+
+            error_report: null,
+            stdout: null,
+            stderr: null,
+
+            icon: null,
+        }
+
+        if (block.parameters !== undefined) {
+            var parameters = Object.keys(block.parameters);
+            for (var j = 0; j < parameters.length; ++j) {
+                entry.parameters.push({
+                    name: parameters[j],
+                    value: block.parameters[parameters[j]],
+                });
+            }
+        }
+
+        if (results.errors) {
+            for (var j = 0; j < results.errors.length; ++j) {
+                if (results.errors[j].block == block_name) {
+                    entry.error_report = results.errors[j].details;
+                    entry.stdout = results.errors[j].stdout;
+                    entry.stderr = results.errors[j].stderr;
+                    break;
+                }
+            }
+        }
+
+        entry.icon = this._getIcon(entry.status);
+
+        tags.blocks.push(entry);
+    }
+
+    return Mustache.render(template, tags);
+}
+
+//---------------------------------------
+
+beat.experiments.panels.ExecutionDetails.prototype._getIcon = function(status) {
+    if (status == 'pending')
+        return "icon-scheduled fa fa-play";
+    else if (status == 'generated')
+        return "icon-done fa fa-check";
+    else if (status == 'failed')
+        return "icon-failed fa fa-bug";
+    else if (status == 'processing')
+        return "icon-running fa fa-spin fa-refresh";
+    else if (status == 'failed')
+        return "icon-failed fa fa-bug";
+    else if (status == 'cancelled')
+        return "icon-failed fa fa-power-off";
+    else
+        return "icon-pending fa fa-asterisk";
+}
diff --git a/beat/web/experiments/static/experiments/js/utils.js b/beat/web/experiments/static/experiments/js/utils.js
index 5f9bef846..19764591a 100644
--- a/beat/web/experiments/static/experiments/js/utils.js
+++ b/beat/web/experiments/static/experiments/js/utils.js
@@ -2219,114 +2219,3 @@ beat.experiments.utils.modal_rename = function (userid, current_name, list_url,
     });
   });
 }
-
-
-/**
- * Updates the toolchain viewer with status information from the blocks
- *
- * Parameters:
- *
- *   viewer (beat.toolchains.viewer.ToolchainViewer): An instance of the BEAT
- *     toolchain viewer that will be used to update the blocks graphically
- *   objects (jQuery.object): A jQuery object that contains the block
- *     information for the current experiment
- *   dt_block_name (String): The name of the data object inside each block
- *     where the current block name is stored.
- *   dt_block_status (String): The name of the data object inside each block
- *     where the current block state is stored.
- */
-beat.experiments.utils.update_viewer = function (viewer, objects, dt_block_name, dt_block_status) {
-  //gather block information
-  block_status = {};
-  objects.each(function(idx) {
-    var bk_name = $(this).data(dt_block_name);
-    if (!bk_name) return;
-    var bk_status = $(this).data(dt_block_status);
-    if (bk_status === 'cached') bk_status = 'generated'; //< viewer quirk
-    block_status[bk_name] = bk_status;
-  });
-  viewer.updateBlocksStatus(block_status);
-}
-
-
-/**
- * Updates blocks of a running experiment on the experiment "view" page
- *
- * Parameters:
- *
- *   url (String): The url to probe for new versions of the blocks to replace
- *   st (String): A jQuery selector for the div containing the data item
- *     'beat-experiment-status', that will be probed for the current experiment
- *     status. The blocks are only updated if the current experiment status is
- *     one of 'Scheduled', 'Running' or 'Canceling'.
- *   dt (String): The name of the data object inside the `st` selector where
- *     the current experiment state is stored.
- *   unveil (String): A jQuery selector of objects to unveil in case we're
- *     updating and the experiment is finished
- *   objects (String): A jQuery selector that will make us search for the
- *     replacement blocks on the `url' above.
- *   dt_block_name (String): The name of the data object inside each block
- *     where the current block name is stored.
- *   dt_block_status (String): The name of the data object inside each block
- *     where the current block state is stored.
- *   viewer (beat.toolchains.viewer.ToolchainViewer): An instance of the BEAT
- *     toolchain viewer that will be used to update the blocks graphically
- *   interval (Int): A timeout in milliseconds to use for updating the block
- *     representations and the toolchain viewer.
- */
-beat.experiments.utils.update_blocks = function (url, st, dt, unveil, objects, dt_block_name, viewer, interval) {
-
-  var _status = $(st).data(dt);
-
-  if (_status === 'Failed') {
-    beat.experiments.utils.update_viewer(viewer, $(objects), dt_block_name, dt);
-    return;
-  }
-
-  //only updates if in one of the "interesting" states
-  var interesting_states = ['Scheduled', 'Running', 'Canceling'];
-  if (interesting_states.indexOf(_status) <= -1) return;
-
-  function _do_update() {
-    var _status = $(st).data(dt);
-
-    if (interesting_states.indexOf(_status) <= -1) {
-      //experiment changed status - should reload
-      $(unveil).show();
-      if (viewer.running) viewer.onExperimentDone();
-      if (_status === 'Failed') {
-        beat.experiments.utils.update_viewer(viewer, $(objects), dt_block_name,
-            dt);
-      }
-      return;
-    }
-
-    var d = $.get(url);
-
-    d.done(function(data) {
-      var parsed = $($.parseHTML(data));
-      $(objects).each(function(idx) {
-        var _self = $(this);
-        var r = parsed.find('#' + _self.attr('id'));
-        //only replaces if it changed
-        var old_status = _self.data(dt);
-        var new_status = r.data(dt);
-        if (r && old_status !== new_status) _self.replaceWith(r);
-      });
-
-      if (!viewer.running) viewer.onExperimentStarted();
-      beat.experiments.utils.update_viewer(viewer, $(objects), dt_block_name, dt);
-    });
-  }
-
-  //if we get to this point, we install the interval function
-  $.ajaxSetup({
-    beforeSend: function(xhr, settings) {
-      var csrftoken = $.cookie('csrftoken');
-      xhr.setRequestHeader('X-CSRFToken', csrftoken);
-    }
-  });
-
-  var timeout_id = window.setInterval(_do_update, interval);
-
-}
diff --git a/beat/web/experiments/templates/experiments/view.html b/beat/web/experiments/templates/experiments/view.html
index f464825be..b65761560 100644
--- a/beat/web/experiments/templates/experiments/view.html
+++ b/beat/web/experiments/templates/experiments/view.html
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 {% comment %}
- * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/
+ * Copyright (c) 2017 Idiap Research Institute, http://www.idiap.ch/
  * Contact: beat.support@idiap.ch
  *
  * This file is part of the beat.web module of the BEAT platform.
@@ -51,8 +51,10 @@
 <script src="{% fingerprint "toolchains/js/dialogs.js" %}" type="text/javascript" charset="utf-8"></script>
 <script src="{% fingerprint "toolchains/js/utils.js" %}" type="text/javascript" charset="utf-8"></script>
 <script src="{% fingerprint "experiments/js/dialogs.js" %}" type="text/javascript" charset="utf-8"></script>
+<script src="{% fingerprint "experiments/js/panels.js" %}" type="text/javascript" charset="utf-8"></script>
 <script src="{% fingerprint "experiments/js/utils.js" %}" type="text/javascript" charset="utf-8"></script>
 <script src="{% fingerprint "bootstrap3-typeahead/bootstrap3-typeahead.min.js" %}" type="text/javascript" charset="utf-8"></script>
+<script src="{% fingerprint "mustache.js/mustache.min.js" %}" type="text/javascript" charset="utf-8"></script>
 {% if owner %}
 {% code_editor_scripts "rst" %}
 {% endif %}
@@ -61,10 +63,126 @@
 
 {% block content %}
 
-<div id="experiment-status" class="may-update" data-beat-status="{{ experiment.get_status_display }}"></div>
+{# Mustache templates for the blocks on the 'Execution details' page #}
+{% verbatim %}
+<script id="template-globals" type="x-tmpl-mustache">
+    <div id="globals" class="panel panel-default globals">
+        <div class="panel-heading" role="tab" id="heading-globals">
+            <h4 class="panel-title">
+                <a class="collapsed" role="button" data-toggle="collapse" data-parent="#globals"
+                   href="#collapse-globals" aria-expanded="false" aria-controls="collapse-globals">Global Parameters</a>
+            </h4>
+        </div>
+        <div id="collapse-globals" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-globals">
+            <div class="panel-body">
+                <ul>
+                    <li>Environment: <a href="{{ environment.url }}">{{ environment.name }}/{{ environment.version }}</a></li>
+                    <li>Queue: {{ queue }}</li>
+                    {{ #algorithms }}
+                    <li>Algorithm: <a href="{{ url }}">{{ name }}</a>:
+                        <ul>
+                            {{ #items }}
+                            <li>{{ name }}: <code>{{ value }}</code></li>
+                            {{ /items }}
+                        </ul>
+                    </li>
+                    {{ /algorithms }}
+                </ul>
+            </div>
+        </div>
+    </div>
+</script>
+
+<script id="template-datasets" type="x-tmpl-mustache">
+    <div id="datasets" class="panel panel-default dataset">
+        <div class="panel-heading" role="tab" id="heading-dataset">
+            <h4 class="panel-title">
+                <a class="collapsed" role="button" data-toggle="collapse" data-parent="#datasets"
+                   href="#collapse-dataset" aria-expanded="false" aria-controls="collapse-dataset">Datasets</a>
+            </h4>
+        </div>
+        <div id="collapse-dataset" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-dataset">
+            <div class="panel-body">
+                <ul>
+                    {{ #datasets }}
+                    <li>{{ block_name }} <i class="fa fa-arrow-right"></i> {{ set }} from protocol "{{ protocol }}" @ <a href="{{ url }}">{{ name }}</a></li>
+                    {{ /datasets }}
+                </ul>
+            </div>
+        </div>
+    </div>
+</script>
+
+<script id="template-block" type="x-tmpl-mustache">
+    {{ #blocks }}
+    <div id="block-{{ name }}" data-beat-status="{{ status }}"
+         class="panel panel-default {{ #analyzer }} analyzer{{ /analyzer }}">
+
+        <div class="panel-heading" role="tab" id="heading-{{ name }}">
+            <h4 class="panel-title">
+                <a {{ ^failed }}class="collapsed"{{ /failed }}
+                   role="button" data-toggle="collapse" data-parent="#block-{{ name }}"
+                   href="#collapse-block-{{ name }}" aria-controls="collapse-block-{{ name }}"
+                   aria-expanded="{{ failed }}">
+                    <i id="block-{{ name }}-status-icon" data-toggle="tooltip" data-placement="bottom" title="{{ status }}" class="{{ icon }}"></i>
+                    {{ name }} ({{ algorithm }})<span id="block-{{ name }}-execution-time">{{ #cached_or_failed }} @ {{ linear_execution_time }}s{{ /cached_or_failed }}</span>
+                </a>
+            </h4>
+        </div>
+        <div id="collapse-block-{{ name }}" class="panel-collapse collapse{{ #failed }} in{{ /failed }}" role="tabpanel" aria-labelledby="heading-block-{{ name }}">
+            <div class="panel-body">
+                <ul>
+                    <li>Algorithm: <a href="{{ url }}">{{ algorithm }}</a></li>
+
+                    <li id="block-{{ name }}-linear-execution-time"{{ ^cached_or_failed }} style="display: none;"{{ /cached_or_failed }}>Total execution time: {{ linear_execution_time }}s</li>
+
+                    {{ #nb_slots }}
+                        <li id="block-{{ name }}-speed-up-real"{{ ^cached_or_failed }} style="display: none;"{{ /cached_or_failed }}>Slots: {{ nb_slots }} (speed-up achieved: {{ speed_up_real }}x)</li>
+                        <li id="block-{{ name }}-speed-up-maximal"{{ ^cached_or_failed }} style="display: none;"{{ /cached_or_failed }}>Maximum speed-up achievable: {{ block.speed_up_maximal }}x</li>
+                    {{ /nb_slots }}
+
+                    {{ ^nb_slots }}
+                        <li id="block-{{ name }}-slots"{{ ^cached_or_failed }} style="display: none;"{{ /cached_or_failed }}>Slots: 1</li>
+                    {{ /nb_slots }}
+
+                    <li id="block-{{ name }}-queuing-time"{{ ^cached_or_failed }} style="display: none;"{{ /cached_or_failed }}>Queuing time: {{ queuing_time }}s</li>
+
+                    <li>Environment: <a href="{{ environment.url }}">{{ environment.name }}/{{ environment.version }}</a></li>
+                    <li>Queue: {{ queue }}</li>
+
+                    {{ #has_parameters }}
+                        <li>Explicit Parameters:
+                            <ul>
+                                {{ #parameters }}
+                                    <li>{{ name }}: <code>{{ value }}</code></li>
+                                {{ /parameters }}
+                            </ul>
+                        </li>
+                    {{ /has_parameters }}
+
+                    <li id="block-{{ name }}-error-report"{{ ^error_report }} style="display: none;"{{ /error_report }}>Captured Errors (on user code):
+                        <pre class="console-output">{{ error_report }}</pre>
+                    </li>
+
+                    <li id="block-{{ name }}-stdout"{{ ^stdout }} style="display: none;"{{ /stdout }}>Standard Output:
+                        <pre class="console-output">{{ stdout }}</pre>
+                    </li>
+
+                    <li id="block-{{ name }}-stderr"{{ ^stderr }} style="display: none;"{{ /stderr }}>Standard Error:
+                        <pre class="console-output">{{ stderr }}</pre>
+                    </li>
+                </ul>
+            </div>
+        </div>
+    </div>
+    {{ /blocks }}
+</script>
+{% endverbatim %}
+
 
-<div id="experiment-header" class="row may-update" data-beat-status="{{ experiment.get_status_display }}">
-  <div class="col-sm-8 vertical-center" onmouseover="expand_breadcrumb(this, 8, 4);" onmouseout="reset_breadcrumb(this, 8, 4);">
+<div id="experiment-header" class="row" data-beat-status="{{ experiment.get_status_display }}">
+  <div class="col-sm-8 vertical-center" onmouseover="expand_breadcrumb(this, 8, 4);"
+       onmouseout="reset_breadcrumb(this, 8, 4);">
     {% experiment_breadcrumb experiment %}
   </div><div class="col-sm-4 vertical-center">
     {% experiment_actions experiment False %}
@@ -80,22 +198,9 @@
 </div>
 {% endif %}
 
-<div id="experiment-date" class="row may-update" data-beat-status="{{ experiment.get_status_display }}-{{ experiment.start_date|naturaltime }}">
+<div id="experiment-date" class="row" style="display: none;">
   <div class="col-sm-12">
-    {% if experiment.end_date %}
-    <p class="help-block"><i class="fa fa-clock-o"></i> Finished executing <strong>{{ experiment.end_date|naturaltime }}</strong> (on {{ experiment.end_date }})</p>
-    {% elif experiment.start_date %}
-    <p class="help-block"><i class="fa fa-clock-o"></i> Started executing <strong>{{ experiment.start_date|naturaltime }}</strong> (on {{ experiment.start_date }})</p>
-    {% endif %}
-  </div>
-</div>
-
-<div class="row unveil" style="display:none;">
-  <div class="col-sm-12">
-    <div class="alert alert-warning alert-dismissible" role="alert">
-      <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
-      <i class="fa fa-warning"></i> The experiment finished. <a href="{{ experiment.get_absolute_url }}">Reload this page</a> when possible to access more details.
-    </div>
+    <p class="help-block"><i class="fa fa-clock-o"></i></p>
   </div>
 </div>
 
@@ -107,6 +212,7 @@
 </div>
 {% endif %}
 
+
 {% with experiment.get_status_display as status %}
 {% visible_reports experiment as reports %}
 {% with reports|length|add:experiment.has_attestation as nreferrers %}
@@ -132,159 +238,11 @@
         {% experiment_results "results" experiment %}
       </div>
       {% endifequal %}
+
       <div role="tabpanel" class="tab-pane{% ifnotequal status 'Done' %} active{% endifnotequal %}" id="exec">
-        {% with experiment.core as xpcore %}
-
-        <div class="col-sm-6">{# block information #}
-
-          <div class="panel-group" id="block-accordion" role="tablist" aria-multiselectable="true">
-
-            {# global settings #}
-            <div id="globals" class="panel panel-default globals">
-              <div class="panel-heading" role="tab" id="heading-globals">
-                <h4 class="panel-title">
-                  <a class="collapsed" role="button" data-toggle="collapse" data-parent="#globals" href="#collapse-globals" aria-expanded="false" aria-controls="collapse-globals">Global Parameters</a>
-                </h4>
-              </div>{# panel heading #}
-              <div id="collapse-globals" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-globals">
-                <div class="panel-body">
-                  {% with xpcore.data.globals as xpglobals %}
-                  <ul>
-                    <li>Environment: <a href="{% url 'backend:view-environment' xpglobals.environment.name xpglobals.environment.version %}">{{ xpglobals.environment.name }}/{{ xpglobals.environment.version }}</a></li>
-                    <li>Queue: {{ xpglobals.queue }}</li>
-                    {% for k, v in xpcore.data.globals.items %}
-                    {% if k != 'environment' and k != 'queue' %}
-                    <li>Algorithm: {% with k|split_fullname as parts %}<a href="{% url 'algorithms:view' parts.0 parts.1 parts.2 %}">{{ k }}</a>{% endwith %}:
-                      <ul>
-                        {% for parname, parvalue in v.items %}
-                        <li>{{ parname }}: <code>{{ parvalue }}</code></li>
-                        {% endfor %}
-                      </ul>
-                    {% endif %}
-                    {% endfor %}
-                    </li>
-                  </ul>
-                  {% endwith %}
-                </div>{# panel body #}
-              </div>{# collapse #}
-            </div>{# panel #}
-
-            {# database set descriptors #}
-            <div id="dataset" class="panel panel-default dataset">
-              <div class="panel-heading" role="tab" id="heading-dataset">
-                <h4 class="panel-title">
-                  <a class="collapsed" role="button" data-toggle="collapse" data-parent="#dataset" href="#collapse-dataset" aria-expanded="false" aria-controls="collapse-dataset">Datasets</a>
-                </h4>
-              </div>{# panel heading #}
-              <div id="collapse-dataset" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-dataset">
-                <div class="panel-body">
-                  <ul>
-                    {% for k,v in xpcore.datasets.items %}
-                    {% with v.database.name|split_fullname as parts %}
-                    <li>{{ k }} <i class="fa fa-arrow-right"></i> {{ v.set }} from protocol "{{ v.protocol }}" @ <a href="{% url 'databases:view' parts.0 parts.1 %}">{{ v.database.name }}</a></li>
-                    {% endwith %}
-                    {% endfor %}
-                  </ul>
-                </div>{# panel body #}
-              </div>{# collapse #}
-            </div>{# panel #}
-
-            {% ordered_blocks experiment as blocks %}
-            {% for block in blocks %}
-            {% with xpcore.blocks|getkey:block.name as core_block %}
-            {% with block.get_status_display as block_status %}
-            {% with block.error_report as error_report %}
-            <div id="{{ block.name }}" data-beat-block-name="{{ block.name }}" data-beat-status="{{ block.get_status_display|lower }}" class="panel panel-default may-update{% if block.analyzer %} analyzer{% endif %}">
-
-              <div class="panel-heading" role="tab" id="heading-{{ block.name }}">
-                <h4 class="panel-title">
-                  <a{% if error_report and block.done and block_status != 'Cancelled' %}{% else %} class="collapsed"{% endif %} role="button" data-toggle="collapse" data-parent="#{{ block.name }}" href="#collapse-{{ block.name }}" aria-expanded="{% if error_report and block.done and block_status != 'Cancelled' %}false{% else %}true{% endif %}" aria-controls="collapse-{{ block.name }}">
-                    <i data-toggle="tooltip" data-placement="bottom" title="{{ block_status }}"
-                      {% if block_status == 'Not cached' %}
-                      class="icon-scheduled fa fa-play"
-                      {% elif block_status == 'Cached' %}
-                      class="icon-done fa fa-check"
-                      {% elif block_status == 'Failed' %}
-                      class="icon-failed fa fa-bug"
-                      {% elif block_status == 'Processing' %}
-                      class="icon-running fa fa-spin fa-refresh"
-                      {% elif block_status == 'Cancelled' %}
-                      class="icon-failed fa fa-power-off"
-                      {% else %}
-                      class="icon-pending fa fa-asterisk"
-                      {% endif %}
-                      ></i>
-                    {{ block.name }} ({{ block.algorithm.fullname }}){% if block_status in 'Cached Failed' %} @ {{ block.linear_execution_time|floatformat:-2 }}s{% endif %}
-                    </a>
-                </h4>
-              </div>{# panel heading #}
-              <div id="collapse-{{ block.name }}" class="panel-collapse collapse{% if error_report and block.done and block_status != 'Cancelled' %} in{% endif %}" role="tabpanel" aria-labelledby="heading-{{ block.name }}">
-                <div class="panel-body">
-                  <ul>
-                    <li>Algorithm: <a href="{{ block.algorithm.get_absolute_url }}">{{ block.algorithm.fullname }}</a></li>
-                    {% if block_status == 'Cached' or block_status == 'Failed' %}
-                    <li>Total execution time: {{ block.linear_execution_time|floatformat:-2 }}s</li>
-                    {% comment %}
-                    <!-- not very reliable
-                    <ul>
-                      <li>Algorithm CPU time: {% if block.cpu_time > 0.01 %}{{ block.cpu_time|floatformat:-2 }}s{% else %}<i class="help">unavailable</i>{% endif %}</li>
-                      <li>Maximum RSS memory: {% if block.max_memory > 1 %}{{ block.max_memory }} bytes{% else %}<i class="help">unavailable</i>{% endif %}</li>
-                      <li>Disk reading time: {% if block.data_read_time > 0.01 %}{{ block.data_read_time|floatformat:-2 }}s{% else %}<i class="help">negligible</i>{% endif %}</li>
-                      <li>Data read: {% if block.data_read_size > 0.01 %}{{ block.data_read_time|floatformat:-2 }} bytes{% else %}<i class="help">negligible</i>{% endif %}</li>
-                      <li>Disk writing time: {% if block.data_written_time > 0.01 %}{{ block.data_written_time|floatformat:-2 }}s{% else %}<i class="help">negligible</i>{% endif %}</li>
-                      <li>Data written: {% if block.data_written_size > 0.01 %}{{ block.data_written_time|floatformat:-2 }} bytes{% else %}<i class="help">negligible</i>{% endif %}</li>
-                    </ul>
-                    -->
-                    {% endcomment %}
-                    {% if core_block.nb_slots and core_block.nb_slots > 1 %}
-                    <li>Slots: {{ core_block.nb_slots }} (speed-up achieved: {{ block.speed_up_real|floatformat:-1 }}x)</li>
-                    <li>Maximum speed-up achievable: {{ block.speed_up_maximal|floatformat:-1 }}x</li>
-                    {% else %}
-                    <li>Slots: 1</li>
-                    {% endif %}
-                    <li>Queuing time: {{ block.queuing_time|floatformat:-2 }}s</li>
-                    {% endif %}
-                    <li>Queue: {% if core_block.queue %}{{ core_block.queue }}{% else %}{{ xpcore.data.globals.queue }}{% endif %}</li>
-                    <li>Environment: <a href="{{ block.environment.get_absolute_url }}">{{ block.environment.fullname }}</a></li>
-                    {% if core_block.parameters %}
-                    <li>Explicit Parameters:
-                      <ul>
-                        {% for parname, parvalue in core_block.parameters.items %}
-                        <li>{{ parname }}: <code>{{ parvalue }}</code></li>
-                        {% endfor %}
-                      </ul>
-                    </li>
-                    {% endif %}
-                    {% if block.done and block_status != 'Cancelled' %}
-                    {% if error_report and block_status != 'Cached' %}
-                    <li>Captured Errors (on user code):
-                      <pre class="console-output">{{ error_report }}</pre>
-                    </li>
-                    {% endif %}
-                    {% if block.stdout %}
-                    <li>Standard Output:
-                      <pre class="console-output">{{ block.stdout }}</pre>
-                    </li>
-                    {% endif %}
-                    {% if block.stderr %}
-                    <li>Standard Error:
-                      <pre class="console-output">{{ block.stderr }}</pre>
-                    </li>
-                    {% endif %}
-                    {% endif %}{# block.done #}
-                  </ul>
-                </div>{# panel body #}
-              </div>{# collapse #}
-            </div>{# panel #}
-            {% endwith %}
-            {% endwith %}
-            {% endwith %}
-            {% endfor %}
-
-          </div>{# panel group #}
-
-        {% endwith %}
-        </div>{# column #}
+        <div class="col-sm-6"> {# execution details display #}
+          <div class="panel-group" id="execution-details" role="tablist" aria-multiselectable="true"></div>
+        </div>
 
         <div class="col-sm-6"> {# toolchain display #}
           <div id="toolchain-viewer" class="toolchain-viewer">
@@ -296,14 +254,17 @@
           </div>
         </div>
       </div>
+
       <div role="tabpanel" class="tab-pane" id="doc">
         {% doc_editor experiment 'api_experiments:object' %}
       </div>
+
       {% if owner %}
       <div role="tabpanel" class="tab-pane" id="sharing">
         {% experiment_sharing experiment %}
       </div>
       {% endif %}
+
       <div role="tabpanel" class="tab-pane" id="referrers">
         <p class="action-buttons help">Experiments which are referred by reports or by an associated attestation cannot be modified. You may revert the situation by deleting unpublished reports or the attestation in case they have not been published. Note you may still <i class="btn-fork fa fa-code-fork"></i> fork this experiment and re-run a modified or unmodified version of it.</p>
         {% if experiment.has_attestation %}
@@ -343,58 +304,164 @@
 
 <script type="text/javascript">
 $(document).ready(function(){
-  manage_tabs('ul#object-tabs');
+    manage_tabs('ul#object-tabs');
+
+
+    // Initialise the execution details panel
+    var execution_details_panel = new beat.experiments.panels.ExecutionDetails(
+        'execution-details', '{{ URL_PREFIX }}'
+    );
+
 
-  {% comment %}
-  The remainder of this code handles the updates on the experiment and block
-  status
+    // Initialise the toolchain viewer
+    var toolchain_viewer = new beat.toolchains.viewer.ToolchainViewer('toolchain-viewer');
 
-  Nota Bene: code repeated from toolchains/panels/viewer.html for the purpose
-  of controlling the filling as the experiment executes
-  {% endcomment %}
+    toolchain_viewer.element.style.cursor = 'pointer';
+    $(toolchain_viewer.element).click(function(){
+        window.location = '{{ experiment.get_absolute_url }}';
+    });
 
-  var viewer = new beat.toolchains.viewer.ToolchainViewer('toolchain-viewer');
 
-  {# load toolchain data, set on viewer #}
-  beat.toolchains.utils.load('{{ URL_PREFIX }}', '{{ experiment.toolchain.fullname }}',
-      function(toolchain, data) {
-        if (!toolchain) {
-          alert(data);
-          return;
+    // Update functions
+    var toolchain = null;
+    var experiment_configuration = null;
+    var experiment_results = null;
+
+    var fields = 'status,blocks_status,done,errors,execution_info,execution_order,' +
+                 'results,started,display_start_date,display_end_date';
+
+    function update_date(started, start_date, end_date) {
+        if (!started)
+            return;
+
+        $('#experiment-date').show();
+
+        if (end_date !== null) {
+            $('#experiment-date .help-block').html('<i class="fa fa-clock-o"></i> Finished executing <strong>' +
+                                                   end_date.natural + '</strong> (on ' + end_date.date + ')');
+        }
+        else {
+            $('#experiment-date .help-block').html('<i class="fa fa-clock-o"></i> Started executing <strong>' +
+                                                   start_date.natural + '</strong> (on ' + start_date.date + ')');
+        }
+    }
+
+    function update() {
+        $.get('{% url 'api_experiments:all' %}{{ experiment.fullname }}/?fields=' + fields,
+            function(experiment_results) {
+                execution_details_panel.update(experiment_results);
+
+                if (!toolchain_viewer.running && experiment_results.started && !experiment_results.done)
+                    toolchain_viewer.onExperimentStarted();
+
+                toolchain_viewer.updateBlocksStatus(experiment_results.blocks_status);
+
+                // (If necessary) Update the icon in the breadcrump
+                var header = $('#experiment-header');
+                if (header.data('beat-status') != experiment_results.status) {
+                    header.data('beat-status', experiment_results.status);
+
+                    var icon = header.find('.breadcrumb .icon-link i');
+                    if (experiment_results.status == 'Done')
+                        icon[0].className = "icon-done fa fa-check fa-lg";
+                    else if (experiment_results.status == 'Scheduled')
+                        icon[0].className = "icon-scheduled fa fa-calendar-check-o fa-lg";
+                    else if (experiment_results.status == 'Pending')
+                        icon[0].className = "icon-pending fa fa-play fa-lg";
+                    else if (experiment_results.status == 'Failed')
+                        icon[0].className = "icon-failed fa fa-bug fa-lg";
+                    else if (experiment_results.status == 'Canceling')
+                        icon[0].className = "icon-canceling fa fa-power-off fa-lg";
+                    else
+                        icon[0].className = "icon-running fa fa-refresh fa-spin fa-lg";
+                }
+
+                // Update the displayed date
+                update_date(experiment_results.started, experiment_results.display_start_date,
+                            experiment_results.display_end_date)
+
+                // (If necessary) Schedule an update for later
+                if (!experiment_results.done)
+                    setTimeout(update, 5000);
+                else
+                    location.reload();
+            }
+        );
+    }
+
+
+    // Load the toolchain and the experiment declaration
+    function setup() {
+        if ((toolchain === null) || (experiment_configuration === null) ||
+            (experiment_results === null))
+            return;
+
+        var configuration = new beat.toolchains.models.Configuration(experiment_configuration);
+
+        // Update the content of the execution details panel
+        templates = {
+            globals: '#template-globals',
+            datasets: '#template-datasets',
+            block: '#template-block',
         }
 
-        viewer.zoom = 0.5;
-        viewer.setToolchain(toolchain);
+        urls = {
+            environment: "{% url 'backend:view-environment' 'NAME' 'VERSION' %}",
+            algorithm: "{% url 'algorithms:view' 'AUTHOR' 'NAME' 0 %}",
+            database: "{% url 'databases:view' 'NAME' 0 %}",
+        }
+
+        execution_details_panel.setup(configuration, templates, urls, experiment_results);
+
+        // Update the content of the toolchain viewer
+        toolchain_viewer.zoom = 0.5;
+        toolchain_viewer.setToolchain(toolchain);
+        toolchain_viewer.setConfiguration(configuration);
+
+        if (experiment_results.started && !experiment_results.done)
+            toolchain_viewer.onExperimentStarted();
+
+        toolchain_viewer.updateBlocksStatus(experiment_results.blocks_status);
 
         var panel = $('#toolchain-viewer');
         if (!panel.is(':visible')) {
-          //assume we're the child of tabpane and try to get width from
-          //our grand-parent.
-          viewer.setSize(panel.parent().parent().width(), panel.height());
+            // Assume we're the child of tabpane and try to get width from
+            // our grand-parent.
+            toolchain_viewer.setSize(panel.parent().parent().width(), panel.height());
         }
         else {
-          viewer.setSize(panel.width(), panel.height());
+            toolchain_viewer.setSize(panel.width(), panel.height());
         }
 
-        $.get('{% url 'api_experiments:all' %}{{ experiment.fullname }}/?fields=declaration', function(data) { viewer.setConfiguration(new beat.toolchains.models.Configuration(data.declaration)); });
-      });
-
-  viewer.element.style.cursor = 'pointer';
-  $(viewer.element).click(function(){
-    window.location = '{{ experiment.get_absolute_url }}';
-  });
-
-  {# the function to update the blocks, probes every 5 seconds #}
-  beat.experiments.utils.update_blocks(
-      '{{ experiment.get_absolute_url }}',
-      '#experiment-status', //div to probe for the current status
-      'beat-status', //data field containing status information on relevant bits
-      '.unveil', //objects that should be unveiled once done
-      '.may-update', //objects that should be updated
-      'beat-block-name', //data field containing the block name
-      viewer,
-      5000 //update interval in milliseconds
-      );
+        // Update the displayed date
+        update_date(experiment_results.started, experiment_results.display_start_date,
+                    experiment_results.display_end_date)
+
+        // (If necessary) Schedule an update for later
+        if (!experiment_results.done)
+            setTimeout(update, 5000);
+    }
+
+    beat.toolchains.utils.load('{{ URL_PREFIX }}', '{{ experiment.toolchain.fullname }}',
+        function(loaded_toolchain, data) {
+            if (!loaded_toolchain) {
+                alert(data);
+                return;
+            }
+
+            toolchain = loaded_toolchain;
+
+            setup();
+        }
+    );
+
+    $.get('{% url 'api_experiments:all' %}{{ experiment.fullname }}/?fields=declaration,' + fields,
+        function(data) {
+            experiment_configuration = data.declaration;
+            experiment_results = data;
+            setup();
+        }
+    );
 })
 </script>
 {% endwith %}
diff --git a/beat/web/toolchains/static/toolchains/js/models.js b/beat/web/toolchains/static/toolchains/js/models.js
index ee21ca690..7506511a7 100644
--- a/beat/web/toolchains/static/toolchains/js/models.js
+++ b/beat/web/toolchains/static/toolchains/js/models.js
@@ -1491,6 +1491,15 @@ beat.toolchains.models.Configuration.prototype.setAlgorithmGlobal = function(alg
 }
 
 
+//----------------------------------------------------------------------------------------
+// Retrieves the name of all the datasets
+//----------------------------------------------------------------------------------------
+beat.toolchains.models.Configuration.prototype.datasetNames = function()
+{
+    return Object.keys(this.declaration.datasets);
+}
+
+
 //----------------------------------------------------------------------------------------
 // Retrieves the name of all the blocks
 //----------------------------------------------------------------------------------------
@@ -2159,6 +2168,15 @@ beat.toolchains.models.Configuration.prototype.removeDefaultQueueListener = func
 }
 
 
+//----------------------------------------------------------------------------------------
+// Indicates if a component is an analyzer
+//----------------------------------------------------------------------------------------
+beat.toolchains.models.Configuration.prototype.is_analyzer = function(name)
+{
+    return this.declaration.analyzers[name] !== undefined;
+}
+
+
 //-----------------------------------------------
 
 
diff --git a/buildout.cfg b/buildout.cfg
index 581cdec87..b3a455a71 100644
--- a/buildout.cfg
+++ b/buildout.cfg
@@ -134,6 +134,7 @@ packages = jquery#~1.11.3
            bootstrap3-typeahead#~4.0.0
            bootstrap-datepicker#~1.5.1
            mousetrap#~1.5.3
+           mustache#2.3.0
            raphael#~2.1.4
            spectrum#~1.7.1
            https://github.com/joshaven/string_score.git#~0.1.22
-- 
GitLab