diff --git a/thehive_button/.eslintrc b/thehive_button/.eslintrc
new file mode 100644
index 0000000000000000000000000000000000000000..64eba86220ec489c9c364e9a443941d14a8d3b16
--- /dev/null
+++ b/thehive_button/.eslintrc
@@ -0,0 +1,7 @@
+---
+extends: "@elastic/kibana"
+
+settings:
+  import/resolver:
+    '@elastic/eslint-import-resolver-kibana':
+      rootPackageName: 'thehive_button'
diff --git a/thehive_button/.gitignore b/thehive_button/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..521e1c7295e90bed131707d9486f5e77fb2569c9
--- /dev/null
+++ b/thehive_button/.gitignore
@@ -0,0 +1,4 @@
+npm-debug.log*
+node_modules
+/build/
+/public/app.css
diff --git a/thehive_button/.kibana-plugin-helpers.json b/thehive_button/.kibana-plugin-helpers.json
new file mode 100644
index 0000000000000000000000000000000000000000..2c63c0851048d8f7bff41ecf0f8cee05f52fd120
--- /dev/null
+++ b/thehive_button/.kibana-plugin-helpers.json
@@ -0,0 +1,2 @@
+{
+}
diff --git a/thehive_button/README.md b/thehive_button/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..b728b8e83dcd2159ba75d83dc435316d68619726
--- /dev/null
+++ b/thehive_button/README.md
@@ -0,0 +1,4 @@
+# The Hive Button
+
+> Visualisation plugin which creates a simple button to create a new case in The Hive.
+
diff --git a/thehive_button/index.js b/thehive_button/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..fa69c75c30d7ee40f8d7089d6debd6cf69c8d402
--- /dev/null
+++ b/thehive_button/index.js
@@ -0,0 +1,19 @@
+import newCaseRoute from './server/routes/newcase';
+
+export default function (kibana) {
+  return new kibana.Plugin({
+    require: [], //['elasticsearch'],
+    name: 'thehive_button',
+    uiExports: {
+      visTypes: [
+        'plugins/thehive_button/main',
+      ],
+    },
+
+    init(server, options) { // eslint-disable-line no-unused-vars
+      // Add server routes and initialize the plugin here
+      newCaseRoute(server);
+    }
+  });
+}
+
diff --git a/thehive_button/package.json b/thehive_button/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..9c3124f4d84aa6913e0103900a134702a1fbde84
--- /dev/null
+++ b/thehive_button/package.json
@@ -0,0 +1,33 @@
+{
+  "name": "thehive_button",
+  "version": "0.1.1",
+  "description": "Visualisation plugin which creates a simple button to create a new case in The Hive.",
+  "main": "index.js",
+  "kibana": {
+    "version": "7.2.2"
+  },
+  "scripts": {
+    "lint": "eslint .",
+    "start": "plugin-helpers start",
+    "build": "plugin-helpers build"
+  },
+  "dependencies": {
+    "request": "^2.88.0"
+  },
+  "devDependencies": {
+    "@elastic/eslint-config-kibana": "link:../../packages/eslint-config-kibana",
+    "@elastic/eslint-import-resolver-kibana": "link:../../packages/kbn-eslint-import-resolver-kibana",
+    "@kbn/plugin-helpers": "link:../../packages/kbn-plugin-helpers",
+    "babel-eslint": "^9.0.0",
+    "eslint": "^5.6.0",
+    "eslint-plugin-babel": "^5.2.0",
+    "eslint-plugin-import": "^2.14.0",
+    "eslint-plugin-jest": "^21.26.2",
+    "eslint-plugin-jsx-a11y": "^6.1.2",
+    "eslint-plugin-mocha": "^5.2.0",
+    "eslint-plugin-no-unsanitized": "^3.0.2",
+    "eslint-plugin-prefer-object-spread": "^1.2.1",
+    "eslint-plugin-react": "^7.11.1",
+    "expect.js": "^0.3.1"
+  }
+}
diff --git a/thehive_button/public/env.js b/thehive_button/public/env.js
new file mode 100644
index 0000000000000000000000000000000000000000..4321b85f5ee1682abd17871889a165ae8d96b465
--- /dev/null
+++ b/thehive_button/public/env.js
@@ -0,0 +1,4 @@
+// Default plugin configuration
+export const THEHIVE_URL = 'https://hive.gn4-3-wp8-soc.sunet.se/';
+export const THEHIVE_API_KEY = '5LymseWiurZBrQN8Kqp8O+9KniTL5cE0';
+export const THEHIVE_OWNER = 'admin'; // default owner account of the created cases
diff --git a/thehive_button/public/main.js b/thehive_button/public/main.js
new file mode 100644
index 0000000000000000000000000000000000000000..96c3bc7b11cc4c1c8c901c23a9fa0399bafa97ef
--- /dev/null
+++ b/thehive_button/public/main.js
@@ -0,0 +1,73 @@
+import './vis.less';
+import optionsTemplate from './options_template.html';
+import { THEHIVE_API_KEY, THEHIVE_URL, THEHIVE_OWNER } from './env';
+import { VisController } from './vis_controller';
+import { VisFactoryProvider } from 'ui/vis/vis_factory';
+import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
+import { Status } from 'ui/vis/update_status';
+
+function TheHiveButtonVisProvider(Private) {
+  const VisFactory = Private(VisFactoryProvider);
+
+  console.log("default URL:", THEHIVE_URL);
+  console.log("default API key:", THEHIVE_API_KEY);
+
+  return VisFactory.createBaseVisualization({
+    name: 'thehive_button',
+    title: 'The Hive Case',
+    icon: 'alert',
+    description: 'A button to create a new Case in The Hive.',
+    visualization: VisController,
+    requiresUpdateStatus: [Status.PARAMS, Status.RESIZE, Status.UI_STATE],
+    visConfig: {
+      defaults: {
+        // add default parameters
+        url: THEHIVE_URL,
+        apikey: THEHIVE_API_KEY,
+        owner: THEHIVE_OWNER,
+      },
+    },
+    editor: 'default',
+//    editor: MyEditorController,
+    editorConfig: {
+//      optionsTemplate: '<div>TEST</div>',
+      optionsTemplate: optionsTemplate
+    },
+    requestHandler: 'none',
+    responseHandler: 'none',
+  });
+}
+
+
+// UNUSED
+/*class MyEditorController {
+  constructor(el, vis) {
+    this.el = el;
+    this.vis = vis;
+    //this.config = vis.type.editorConfig;
+
+    this.container = document.createElement('div');
+    this.container.className = 'myvis-config-container-div';
+    this.el.appendChild(this.container);
+  }
+
+  async render(visData) {
+    console.log("MyEditorController render(), config:", this.vis);
+//    console.log(this.vis.isEditorMode);
+//    console.log(this.vis.getUiState);
+//    console.log(this.vis.getState);
+//    console.log(this.vis.params);
+    this.container.innerHTML = optionsTemplate;
+//    this.vis.updateState();
+    this.vis.dirty = true;
+    return 'done rendering';
+  }
+
+//  destroy() {
+//    console.log('destroying');
+//  }
+}*/
+
+// register the provider with the visTypes registry
+VisTypesRegistryProvider.register(TheHiveButtonVisProvider);
+
diff --git a/thehive_button/public/options_template.html b/thehive_button/public/options_template.html
new file mode 100644
index 0000000000000000000000000000000000000000..5ec603b3f1e531a18e2bcef0e60e0e54e177ea06
--- /dev/null
+++ b/thehive_button/public/options_template.html
@@ -0,0 +1,7 @@
+<div class="form-group">
+  <p style="margin-bottom: 1em">The plugin is currently not configurable from here. Below are the predefined configuration values. They can be edited in "<tt>&lt;KIBANA_PATH&gt;/plugins/thehive_button/public/env.js</tt>" on the backend.</p>
+  <label>The Hive API URL</label>
+  <input ng-model=vis.params.url class=form-control disabled=disabled />
+  <label>The Hive API key</label>
+  <input ng-model=vis.params.apikey class=form-control disabled=disabled />
+</div>
diff --git a/thehive_button/public/vis.less b/thehive_button/public/vis.less
new file mode 100644
index 0000000000000000000000000000000000000000..b6f887afaef57a7674a0d0f06ee6f821a0fc015e
--- /dev/null
+++ b/thehive_button/public/vis.less
@@ -0,0 +1,3 @@
+.myvis-container-div {
+  padding: 1em;
+}
diff --git a/thehive_button/public/vis_controller.js b/thehive_button/public/vis_controller.js
new file mode 100644
index 0000000000000000000000000000000000000000..87f00dd7e0941d87c6fc3ae035d839ac0ab79763
--- /dev/null
+++ b/thehive_button/public/vis_controller.js
@@ -0,0 +1,160 @@
+import { Status } from 'ui/vis/update_status';
+import chrome from 'ui/chrome';
+import { toastNotifications } from 'ui/notify';
+//import vis_template from './vis_template.html';
+
+import React from 'react';
+import {
+  EuiButton,
+//  EuiPanel,
+} from '@elastic/eui';
+
+
+function createTheHiveButton(vis) {
+  var button = document.createElement('button');
+  button.className = 'euiButton euiButton--danger euiButton--fill';
+  button.innerHTML = '<span class="euiButton__content"><span class="euiButton__text" title="Create new Case in The Hive">Create new Case</span></span>';
+  button.addEventListener('click', hiveButtonOnClick);
+  button._vis = vis; // store reference to 'vis' to the element
+//  var button2 = <EuiButton fill iconType="alert" color="danger" onClick={(evt) => hiveButtonOnClick(evt, vis.params)}>Create new Case</EuiButton>;
+//  button2.setState({vis: vis});
+  return button;
+}
+
+function hiveButtonOnClick(evt) {
+  var button = evt.currentTarget;
+  var params = button._vis.params;
+  //console.log("OnCLick", params);
+  
+  // Add "loading" icon and disable the button
+  var loading_span = document.createElement('span');
+  loading_span.className = "euiLoadingSpinner euiLoadingSpinner--medium euiButton__spinner";
+  button.firstChild.insertBefore(loading_span, button.firstChild.childNodes[0]);
+  button.setAttribute("disabled", true);
+
+  var title = "TODO: SET THE TITLE";
+  var descr = "(Created from Kibana)\n\n...";
+  var severity = 2;
+  var start_date = null;
+  var owner = params.owner;
+  var flag = false;
+  var tlp = 2;
+  var tags = [];
+
+  createHiveCase(params.url, params.apikey, title, descr, severity, start_date, owner, flag, tlp, tags)
+    .then(function(value) {
+       if ('error' in value) {
+         // Error contacting The Hive
+         console.log("createHiveCase() ERROR:", value.error);
+         toastNotifications.addDanger("ERROR: " + value.error);
+       }
+       else {
+         // Success - show notification and open the Case in new tab
+         console.log("createHiveCase() completed:", value);
+         const case_url = params.url + "index.html#/case/" + value.id + "/details";
+         toastNotifications.add({
+           title: "Case created",
+           color: "success",
+           iconType: "checkInCircleFilled",
+           text: (
+             <div>
+               <p><b><a href={case_url} target="_blank">Edit the new Case</a></b></p>
+             </div>
+           ),
+         });
+         window.open(case_url, '_blank');
+       }
+       // remove "loading" icon and re-enable the button
+       button.firstChild.removeChild(loading_span);
+       button.removeAttribute("disabled");
+    });
+  return false;
+}
+
+
+// Create a new Case in The Hive via its API
+// Return a Promise which resolves to object with ID of the new case ('id' attr) or error message ('error' attr)
+function createHiveCase(base_url, api_key, title, descr, severity, startDate, owner, flag, tlp, tags) {
+  // Prepare data
+  // TODO: check '/' at the end of base URL
+  var data = JSON.stringify({
+    "base_url": base_url,
+    "api_key": api_key,
+    "body": {
+      "title": title,
+      "description": descr,
+      "severity": severity, // number: 1=low, 2=medium, 3=high
+      "startDate": startDate,
+      "owner": owner, // user name the case will be assigned to
+      "flag": flag, // bool
+      "tlp": tlp, // number: 0=white, 1=green, 2=amber, 3=red
+      "tags": tags, // array of strings
+    }
+  });
+  console.log("Sending request to Kibana API endpoint of thehive_button plugin:", data);
+  var kibana_endpoint_url = chrome.addBasePath('/api/thehive_button/new_case');
+
+  return new Promise(function (resolve, reject) {
+    // Create AJAX request
+    var xhr = new XMLHttpRequest();
+    
+    // Listener to process reply
+    xhr.onreadystatechange = function () {
+      if (this.readyState != 4) {
+        return; // response not ready yet
+      }
+      if (this.status == 200) {
+        const resp = JSON.parse(this.responseText);
+        console.log("Response from The Hive:", resp);
+        if ("error" in resp) {
+          resolve({"error": resp.error});
+        }
+        else if (resp.status_code != 201) {
+          resolve({"error": "Unexpected reply received from The Hive: [" + resp.status_code + "] " + resp.status_msg});
+        }
+        else {
+          resolve({"id": resp.body.id}); // return ID of the new case
+        }
+      }
+      else {
+        console.log("Error " + this.status + ": " + this.statusText);
+        resolve({"error": "Error " + this.status + ": " + this.statusText});
+      }
+    }
+
+    // Send the AJAX request
+    xhr.open("POST", kibana_endpoint_url);
+    xhr.setRequestHeader("Content-Type", "application/json");
+    xhr.setRequestHeader("kbn-xsrf", "thehive_plugin"); // this header must be set, although its content is probably irrelevant
+    xhr.send(data);
+  });
+}
+
+class VisController {
+  constructor(el, vis) {
+    this.vis = vis;
+    this.el = el;
+    //console.log('constructor called!');
+    this.container = document.createElement('div');
+    this.container.className = 'myvis-container-div';
+    this.button = createTheHiveButton(vis);
+    this.container.appendChild(this.button);
+    this.el.appendChild(this.container);
+  }
+
+  destroy() {
+    this.el.innerHTML = '';
+  }
+
+  async render(visData, status) {
+    //console.log('in promise!', this.vis)
+    //console.log(vis_template);
+    //var vis = this.vis;
+    //this.container.innerHTML = `${vis_template}`;
+    //this.container.innerHTML += '<p>Hello World from Promise! ' + JSON.stringify(status) + '</p>';
+    //console.log('Render: ' + JSON.stringify(status));
+    return 'done rendering';
+  }
+};
+export { VisController };
+
diff --git a/thehive_button/server/routes/newcase.js b/thehive_button/server/routes/newcase.js
new file mode 100644
index 0000000000000000000000000000000000000000..7400a5f91cbda1c9e203836dc6a77e17a359ee4f
--- /dev/null
+++ b/thehive_button/server/routes/newcase.js
@@ -0,0 +1,62 @@
+const request = require('request');
+
+export default function (server) {
+  server.route({
+    path: '/api/thehive_button/new_case',
+    method: 'POST',
+    handler: newCaseHandler,
+  });
+}
+
+// Handler of ajax requests to create a new Case in The Hive
+function newCaseHandler(req, resp) {
+  // Parse the request to get connection parameters
+  // (everything is configured in forntend and sent as part of the request,
+  //  since I don't know how to configure the backend)
+  var base_url = req.payload['base_url'];
+  var api_key = req.payload['api_key'];
+  var req_body = req.payload['body'];
+
+  // check it's a valid URL with slash at the end
+  if (!base_url) {
+    return {'error': 'Base URL not set'};
+  }
+  if (!base_url.match(/https?:\/\/(([a-z\d.-]+)|((\d{1,3}\.){3}\d{1,3}))(\:\d+)?(\/[-a-z\d%_.~+]*)*\//i)) {
+  //if (!base_url.match(/https?:\/\/.*\//)) {
+    return {'error': 'Invalid base URL (it must begin with "http[s]" and end with "/")'};
+  }
+  if (!api_key) {
+    return {'error': 'API key not set'};
+  }
+
+  return new Promise( function(resolve, reject) {
+    request({
+        method: 'POST',
+        url: base_url + 'api/case',
+        auth: {'bearer': api_key},
+        json: true,
+        body: req_body,
+        //rejectUnauthorized: false, // Disables server certificate check
+      },
+      // handler of the reply from The Hive - just return as reply
+      function (error, response, body) {
+        // TODO: find out how to set response code, for now we always return sucess and encode original status code in the content
+        if (error) {
+          console.log("ERROR when trying to send request to The Hive:", error);
+          resolve({'error': error.message});
+        }
+        else {
+          if (response.statusCode < 200 || response.statusCode >= 300) {
+            console.log("ERROR Unexpected reply received from The Hive:", response.statusCode, response.statusMessage, "\n", body)
+          }
+          resolve({
+            'status_code': response.statusCode,
+            'status_msg': response.statusMessage,
+            'body': body
+          });
+        }
+      } // handler function
+    ); // request()
+  }); // Promise()
+}
+