diff --git a/src/core/js/ext_entity_action_panel.js b/src/core/js/ext_entity_action_panel.js
new file mode 100644
index 0000000000000000000000000000000000000000..0573be0f2448ab1decc4156b48f584874d64c0c1
--- /dev/null
+++ b/src/core/js/ext_entity_action_panel.js
@@ -0,0 +1,384 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+
+'use strict';
+
+
+/**
+ * Add a special section to each entity one the current page where a thumbnail,
+ * a video preview or any other kind of summary for the entity is shown.
+ *
+ * Apart from some defaults, the content for the bottom line has to be
+ * configured in the file `conf/ext/json/ext_bottom_line.json` which must
+ * contain a {@link BottomLineConfig}. An example is located at
+ * `conf/core/json/ext_bottom_line.json`.
+ *
+ * @module ext_bottom_line
+ * @version 0.1.1
+ *
+ * @requires jQuery (library)
+ * @requires log (singleton from loglevel library)
+ * @requires resolve_references.is_in_viewport_vertically (funtion from ext_references.js)
+ * @requires load_config (function from caosdb.js)
+ * @requires getEntityPath (function from caosdb.js)
+ * @requires connection (module from webcaosdb.js)
+ */
+var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntityPath, connection, ext_applicable) {
+
+    /**
+     * @property {string|function} create - a function with one parameter
+     *     (entity) Note: This property can as well be a
+     *     javascript string which evaluates to a function.
+     */
+    /**
+     * @type {BottomLineConfig}
+     * @property {string|HTMLElement} fallback - Fallback content if none of
+     *     the creators are applicable.
+     * @property {string} version - the version of the configuration which must
+     *     match this module's version.
+     * @property {CreatorConfig[]} creators - an array of creators.
+     */
+
+    /**
+     * @type {CreatorConfig}
+     * @property {string} [id] - a unique id for the creator. optional, for
+     *     debuggin purposes.
+     * @property {function|string} is_applicable - If this is a string this has
+     *     to be valid javascript! An asynchronous function which accepts one
+     *     parameter, an entity in html representation, and which returns true
+     *     iff this creator is applicable for the given entity.
+     * @property {string} create - This has to be valid javascript! An
+     *     asynchronous function which accepts one parameter, an entity in html
+     *     representation. It returns a HTMLElement or text node which will be
+     *     shown in the bottom line container iff the creator is applicable.
+     */
+
+    /**
+     * Check if an entity has a path attribute and one of a set of extensions.
+     *
+     * Note: the array of extensions must contain only lower-case strings.
+     *
+     * @param {HTMLElement} entity
+     * @param {string[]} extensions - an array of file extesions, e.g. `jpg`.
+     * @return {boolean} true iff the entity has a path with one of the
+     *     extensionss.
+     */
+    const _path_has_file_extension = function(entity, extensions) {
+        const path = getEntityPath(entity);
+        if (path) {
+            for (var ext of extensions) {
+                if (path.toLowerCase().endsWith(ext)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Create a preview for videos.
+     *
+     * @param {HTMLElement} entity
+     * @return {HTMLElement} a VIDEO element.
+     */
+    const _create_video_preview = function(entity) {
+        var path = connection.getFileSystemPath() + getEntityPath(entity);
+        return $(`<div class="caosdb-v-bottom-line-video-preview"><video controls="controls"><source src="${path}"/></video></div>`)[0];
+    }
+
+    /**
+     * Create a preview for pictures.
+     *
+     * @param {HTMLElement} entity
+     * @return {HTMLElement} an IMG element.
+     */
+    const _create_picture_preview = function(entity) {
+        var path = connection.getFileSystemPath() + getEntityPath(entity);
+        return $(`<div class="caosdb-v-bottom-line-image-preview"><img src="${path}"/></div>`)[0];
+    }
+
+    var fallback_preview = undefined;
+
+    /**
+     * Default creators.
+     *
+     * @member {Creator[]}
+     */
+    const _default_creators = [{ // pictures
+            id: "_default_creators.pictures",
+            is_applicable: (entity) => _path_has_file_extension(
+                entity, ["jpg", "png", "gif", "svg"]),
+            create: _create_picture_preview
+        }, { // videos
+            id: "_default_creators.videos",
+            is_applicable: (entity) => _path_has_file_extension(
+                entity, ["mp4", "mov", "webm"]),
+            create: _create_video_preview,
+        }, { // fallback
+            id: "_default_creators.fallback",
+            is_applicable: (entity) => true,
+            create: (entity) => fallback_preview,
+        },
+
+    ];
+
+
+    const previewShownEvent = new Event("ext_bottom_line.preview.shown");
+
+
+    /**
+     * Store the list of creators.
+     *
+     * @member {Creator[]}
+     */
+    const _creators = [];
+
+    /**
+     * Return the preview container of the entity.
+     *
+     * @param {HTMLElement} entity - An entity in HTML representation.
+     * @returns {HTMLElement} the preview container or `undefined` if the entity
+     *     doesn't have any.
+     */
+    const get_preview_container = function(entity) {
+        return $(entity).children(`.${ext_applicable.
+            _css_class_preview_container}`)[0];
+    }
+
+    /**
+     * Add a preview container to the entity.
+     *
+     * The preview container is a HTMLElement with class {@link
+     * ext_applicable._css_class_preview_container}. It is intended to contain
+     * the preview after it has been created.
+     *
+     * The presence of a preview container is also the marker that an entity
+     * has been visited by the root_preview_handler yet and that a preview is
+     * loaded and added to the entity or on its way.
+     *
+     * Depending on the implementation the actual preview container might be
+     * wrapped into other elements which can be used for styling or other
+     * purposes.
+     *
+     * @param {HTMLElement} entity - An entity in HTML representation.
+     * @return {HTMLElement} the newly created container.
+     */
+    var add_preview_container = function(entity) {
+        const button_show = $('<button class="btn btn-xs"><span class="glyphicon glyphicon-menu-down"/> Show Preview</button>')
+            .css({
+                width: "100%"
+            })
+            .addClass(ext_applicable._css_class_preview_container_button);
+        const button_hide = $('<button class="btn btn-xs"><span class="glyphicon glyphicon-menu-up"/> Hide Preview</button>')
+            .css({
+                width: "100%"
+            })
+            .addClass(ext_applicable._css_class_preview_container_button)
+            .hide();
+        const style = {
+            padding: "0px 10px"
+        };
+        const container = $(`<div class="collapse"/>`)
+            .addClass(ext_applicable._css_class_preview_container)
+            .addClass(ext_applicable._css_class_preview_container_resolvable)
+            .css(style);
+        const show = function() {
+            button_show.hide();
+            button_hide.show();
+            container.collapse("show");
+        }
+        const hide = function() {
+            button_hide.hide();
+            button_show.show();
+            container.collapse("hide");
+        }
+        button_show.click(show);
+        button_hide.click(hide);
+        container.on("shown.bs.collapse", () => {
+            container[0].dispatchEvent(previewShownEvent);
+        });
+        $(entity).append(container);
+        $(entity).append(button_show);
+        $(entity).append(button_hide);
+
+        return container[0];
+    }
+
+    /**
+     * Create a preview for the entity and append it to the entity.
+     *
+     * The root_preview_handler calls the root_preview_creator which returns a
+     * preview which is to be added to the entity.
+     *
+     * @async
+     * @param {HTMLElement} entity - the entity for which the preview is to
+     *     created.
+     */
+    var root_preview_handler = async function(entity) {
+        const container = get_preview_container(entity) || add_preview_container(entity);
+        if (container) {
+            await ext_applicable.root_handler(entity, container, _creators);
+        } else {
+            logger.error(new Error("Could not create the preview container."));
+        }
+    }
+
+    /**
+     * Trigger the root_preview_handler for all entities within the view port
+     * when the view port.
+     */
+    var root_preview_handler_trigger = function() {
+      ext_applicable.root_handler_trigger(root_preview_handler);
+    }
+
+
+
+    /**
+     * Configure the creators.
+     *
+     * @param {BottomLineConfig} config
+     */
+    var configure = async function(config) {
+        logger.debug("configure", config);
+        if (config.version != "0.1") {
+            throw new Error("Wrong version in config.");
+        }
+
+        // append/load custom creators
+        _creators.splice(0, _creators.length);
+        for (let c of ext_applicable.make_creators(config.creators)) {
+            _creators.push(c);
+        }
+
+        // append default creators
+        for (let c of _default_creators) {
+            _creators.push(c);
+        }
+        fallback_preview = config.fallback || fallback_preview;
+    };
+
+
+    /**
+     * Initialize this module.
+     *
+     * I.e. configure the list of creators and setup the scroll listener which
+     * triggers the root_preview_handler.
+     *
+     * @property {BottomLineConfig} [config] - an optional config. Per default, the
+     *     configuration is fetched from the server.
+     */
+    const init = async function(config) {
+        logger.info("ext_bottom_line initialized");
+
+        try {
+            let _config = config || await load_config("json/ext_bottom_line.json");
+            await configure(_config);
+
+            ext_applicable.init_watcher(_config.delay || 500,
+              root_preview_handler_trigger);
+
+
+        } catch (err) {
+            logger.error(err);
+        }
+
+    }
+
+    return {
+        previewShownEvent: previewShownEvent,
+        init: init,
+        init_watcher: ext_applicable.init_watcher,
+        configure: configure,
+        add_preview_container: add_preview_container,
+        root_preview_handler: root_preview_handler,
+        _creators: _creators,
+    }
+}($, log.getLogger("ext_bottom_line"), resolve_references.is_in_viewport_vertically, load_config, getEntityPath, connection, ext_applicable);
+
+
+/**
+ * Helper for plotly
+ */
+var plotly_preview = function(logger, ext_bottom_line, plotly) {
+
+    /**
+     * Create a plot with plotly.
+     *
+     * The layout and any other plotly options are set by this function. The
+     * only parameters are `data` and `layout` which are the respective
+     * parameters of the `Plotly.newPlot` factory.
+     *
+     * Hence the full documentation of the `data` parameter is available at
+     * https://plotly.com/javascript/plotlyjs-function-reference/#plotlynewplot
+     *
+     *
+     * @param {object[]} data - array of objects containing the data which is
+     *     to be plotted.
+     * @param {object[]} layout - dictionary of settings defining the layout of
+     *     the plot.
+     * @returns {HTMLElement} the element which contains the plot.
+     */
+    const create_plot = function(data,
+        layout = {
+            margin: {
+                t: 0
+            },
+            height: 400,
+            widht: 400
+        }) {
+        var div = $('<div/>')[0];
+        plotly.newPlot(div, data, layout, {
+            responsive: true
+        });
+        return div;
+    }
+
+    const resize_plots_event_handler = function(e) {
+        var plots = $(e.target).find(".js-plotly-plot");
+        for (let plot of plots) {
+            plotly.Plots.resize(plot);
+        }
+    }
+
+    const init = function() {
+        window.addEventListener(ext_applicable.previewReadyEvent.type,
+            resize_plots_event_handler, true);
+        window.addEventListener(ext_bottom_line.previewShownEvent.type,
+            resize_plots_event_handler, true);
+    }
+
+    return {
+        create_plot: create_plot,
+        init: init,
+    };
+
+}(log.getLogger("plotly_preview"), ext_bottom_line, Plotly);
+
+
+// this will be replaced by require.js in the future.
+$(document).ready(function() {
+    if ("${BUILD_MODULE_EXT_BOTTOM_LINE}" === "ENABLED") {
+        caosdb_modules.register(plotly_preview);
+        caosdb_modules.register(ext_bottom_line);
+    }
+});
diff --git a/src/core/xsl/main.xsl b/src/core/xsl/main.xsl
index a8a5aa1e7961ba0f52e68036c398957df35e8703..35bc2d53a366530c63f1ff24fe620a4f37b5fce1 100644
--- a/src/core/xsl/main.xsl
+++ b/src/core/xsl/main.xsl
@@ -240,6 +240,11 @@
         <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_bottom_line.js')"/>
       </xsl:attribute>
     </xsl:element>
+    <xsl:element name="script">
+      <xsl:attribute name="src">
+        <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_entity_action_panel.js')"/>
+      </xsl:attribute>
+    </xsl:element>
     <xsl:element name="script">
       <xsl:attribute name="src">
         <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_revisions.js')"/>
diff --git a/test/core/index.html b/test/core/index.html
index e4d6d81634aa73c2d71385545fb917392ba5df1f..8d8a583cce9f2c429681dc99a8010c8b7b72d7c4 100644
--- a/test/core/index.html
+++ b/test/core/index.html
@@ -66,6 +66,7 @@
   <script src="js/ext_map.js"></script>
   <script src="js/ext_applicable.js"></script>
   <script src="js/ext_bottom_line.js"></script>
+  <script src="js/ext_entity_action_panel.js"></script>
   <script src="js/ext_revisions.js"></script>
   <script src="js/autocomplete.js"></script>
   <script src="js/ext_sss_markdown.js"></script>
@@ -87,6 +88,7 @@
   <script src="js/modules/ext_map.js.js"></script>
   <script src="js/modules/ext_applicable.js.js"></script>
   <script src="js/modules/ext_bottom_line.js.js"></script>
+  <script src="js/modules/ext_entity_action_panel.js.js"></script>
   <script src="js/modules/ext_revisions.js.js"></script>
   <script src="js/modules/autocomplete.js.js"></script>
   <script src="js/modules/ext_sss_markdown.js.js"></script>
diff --git a/test/core/js/modules/ext_entity_action_panel.js.js b/test/core/js/modules/ext_entity_action_panel.js.js
new file mode 100644
index 0000000000000000000000000000000000000000..f58fbe8514d861b6d57ed81e81116bf1ec99c90c
--- /dev/null
+++ b/test/core/js/modules/ext_entity_action_panel.js.js
@@ -0,0 +1,28 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+
+'use strict';
+
+var ext_entity_action_panel_test_suite = function ($, ext_entity_action_panel, QUnit) {
+
+}($, ext_entity_action_panel, QUnit);