diff --git a/thehive_button/package.json b/thehive_button/package.json
index 037318890a52bdc329a38e0dd83c18837bb633b9..b513a50a08facba8f4b96beb885d24036ebc7bff 100644
--- a/thehive_button/package.json
+++ b/thehive_button/package.json
@@ -1,10 +1,10 @@
 {
   "name": "thehive_button",
-  "version": "0.2.0",
+  "version": "0.3.0",
   "description": "Visualisation plugin which creates a simple button to create a new case in The Hive.",
   "main": "index.js",
   "kibana": {
-    "version": "7.2.2"
+    "version": "7.2.2",
   },
   "scripts": {
     "lint": "eslint .",
@@ -13,7 +13,6 @@
   },
   "dependencies": {
     "request": "^2.88.0",
-    "jquery-ui": "1"
   },
   "devDependencies": {
     "@elastic/eslint-config-kibana": "link:../../packages/eslint-config-kibana",
diff --git a/thehive_button/public/main.js b/thehive_button/public/main.js
index 96c3bc7b11cc4c1c8c901c23a9fa0399bafa97ef..1361795e41978f2ae1d8e146cd57d2c65b220534 100644
--- a/thehive_button/public/main.js
+++ b/thehive_button/public/main.js
@@ -1,9 +1,12 @@
-import './vis.less';
-import optionsTemplate from './options_template.html';
+//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 { VisController } from './vis_controller';
+import { TheHiveButtonVisComponent } from './vis_controller';
+
 import { VisFactoryProvider } from 'ui/vis/vis_factory';
 import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
+import { Schemas } from 'ui/vis/editors/default/schemas';
 import { Status } from 'ui/vis/update_status';
 
 function TheHiveButtonVisProvider(Private) {
@@ -12,62 +15,83 @@ function TheHiveButtonVisProvider(Private) {
   console.log("default URL:", THEHIVE_URL);
   console.log("default API key:", THEHIVE_API_KEY);
 
-  return VisFactory.createBaseVisualization({
+  return VisFactory.createReactVisualization({
     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],
+    //requiresUpdateStatus: [Status.PARAMS, Status.RESIZE, Status.UI_STATE],
     visConfig: {
+      component: TheHiveButtonVisComponent,
       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
+      /*collections: { 
+        colorSchemas: [
+          {id: "test1", label: "Test 1"},
+          {id: "test2", label: "Test 2"},
+        ],
+      },*/
+      optionsTemplate: '<div>TEST</div>',
+//      optionsTemplate: '<thehivebutton-vis-params></thehivebutton-vis-params>',
+      schemas: new Schemas([
+        // TODO all this (metric and bucket "terms") could be pre-defind
+        // (just a selection of fields to read Observables from would be nice)
+        // But I don't know how and where to pass the values manually.
+        /* Allowed params (from /src/legacy/ui/public/vis/editors/default/schemas.d.ts)
+            aggFilter: string | string[];
+            editor: boolean | string;
+            group: AggGroupNames;
+            max: number;
+            min: number;
+            name: string;
+            params: AggParam[];
+            title: string;
+        */
+        {
+          group: 'metrics',
+          name: 'metric',
+          title: "Metric (not used)",
+          min: 1,
+          max: 1,
+          aggFilter: 'count',
+          defaults: [
+            {
+              type: 'count',
+              schema: 'metric',
+            },
+          ],
+        },
+        {
+          group: 'buckets',
+          name: 'group',
+          title: 'Observables',
+          min: 1,
+          max: 1,
+          aggFilter: 'terms',//['!geohash_grid', '!geotile_grid', '!filter'],
+//           defaults: [
+//             {
+//               type: 'terms',
+//               //schema: 'group',
+//               field: 'ip',
+//               size: 100,
+//             }
+//           ]
+        },
+      ]),
     },
-    requestHandler: 'none',
-    responseHandler: 'none',
+    //requestHandler: 'courier', // default
+    //responseHandler: 'default', // return data as a table, see https://www.elastic.co/guide/en/kibana/7.2/development-visualization-response-handlers.html
   });
 }
 
-
-// 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/vis_controller.js b/thehive_button/public/vis_controller.js
index b0bd3415bbba926bf48ca3ee130ace3609c0ccc2..2ddeb1fb70341ad31761e11a79e26ba170cdff01 100644
--- a/thehive_button/public/vis_controller.js
+++ b/thehive_button/public/vis_controller.js
@@ -1,135 +1,434 @@
-import { Status } from 'ui/vis/update_status';
+//import { Status } from 'ui/vis/update_status';
 import chrome from 'ui/chrome';
 import { toastNotifications } from 'ui/notify';
-import case_form from './case_form.html';
-
-import React from 'react';
-
-import $ from 'jquery';
-import 'jquery-ui/themes/base/core.css';
-import 'jquery-ui/themes/base/theme.css';
-import 'jquery-ui/themes/base/resizable.css';
-import 'jquery-ui/themes/base/dialog.css';
-import 'jquery-ui/ui/core';
-import 'jquery-ui/ui/widgets/resizable';
-import 'jquery-ui/ui/widgets/dialog';
+//import vis_template from './vis_template.html';
 
+import React, { Component } from 'react';
 import {
   EuiButton,
-//  EuiPanel,
+  EuiButtonEmpty,
+  EuiModal,
+  EuiModalBody,
+  EuiModalFooter,
+  EuiModalHeader,
+  EuiModalHeaderTitle,
+  EuiOverlayMask,
+  EuiTitle,
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiForm,
+  EuiFormRow,
+  EuiFieldText,
+  EuiTextArea,
+  EuiSuperSelect,
+  EuiBasicTable,
+  EuiCheckbox,
+  makeId,
 } from '@elastic/eui';
 
 
-function createTheHiveButton(vis) {
-  // Create the button
-  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});
-
-  // Create the dialog to specify Case parameters
-  var dialog = document.createElement("div");
-  dialog.innerHTML = case_form; // content imported from ext. file
-  $(dialog).dialog({
-    autoOpen: false,
-    title: "Create a new Case ...",
-    width: 600,
-    resizable: true,
-    buttons: [
+// ********** React components **********
+
+// Main React component - the root of visualization
+export class TheHiveButtonVisComponent extends Component {
+  render() {
+    //console.log(this.props);
+    let ips = [];
+    if (this.props.visData) {
+      const first_col_id = this.props.visData.columns[0].id;
+      ips = this.props.visData.rows.map(row => row[first_col_id]);
+      //console.log(ips);
+    }
+    return (
+      <div>
+        <NewCaseButton params={this.props.vis.params} observables={ips} />
+      </div>
+    );
+  }
+
+  componentDidMount() {
+    this.props.renderComplete();
+  }
+
+  componentDidUpdate() {
+    this.props.renderComplete();
+  }
+}
+
+// Button to show the pop-up window (modal)
+// Props:
+//  .params - visualization parameters (from vis.params)
+//  .observables - list of IP addrsses or other observables to add to the Case
+class NewCaseButton extends Component {
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      isModalVisible: false,
+    };
+    // Each handler function in a class (method) must be "binded" this way
+    this.closeModal = this.closeModal.bind(this);
+    this.showModal = this.showModal.bind(this);
+  }
+
+  closeModal() {
+    this.setState({ isModalVisible: false });
+  }
+
+  showModal() {
+    this.setState({ isModalVisible: true });
+  }
+
+  render() {
+    let modal;
+    if (this.state.isModalVisible) {
+      modal = <ModalContent close={this.closeModal} params={this.props.params} observables={this.props.observables} />;
+    }
+    return (
+      <div>
+        <EuiButton fill iconType="alert" color="danger" onClick={this.showModal}>Create new Case ...</EuiButton>
+        {modal}
+      </div>
+    );
+  }
+}
+
+// The popup window with a form
+// Props:
+//  .close - function to close the Modal
+//  .params - visualization parameters (from vis.params)
+//  .observables - list of IP addrsses or other observables to add to the Case
+class ModalContent extends Component {
+  constructor(props) {
+    super(props);
+    const observables = props.observables;
+    const n_obs = observables.length;
+    
+    // Initialize state (all changeable case parameters)
+    // TODO: move state up to the button, so it persists when modal is closed (and add explicit reset button + reset on submit)
+    this.state = {
+      title: "",
+      description: "\n\n--\nCreated from Kibana",
+      severity: "2", // medium
+      tlp: "2", // amber
+      tags: [],
+      // An array of values for each editable part of observables, prefilled with default values
+      obsDescrs: new Array(n_obs).fill(""),
+      obsTLPs: new Array(n_obs).fill(2),
+      obsIOCs: new Array(n_obs).fill(false),
+      obsTags: new Array(n_obs).fill([]),
+      obsSelected: new Array(), // list of indices of selected observables
+    };
+    
+    // "Select" options
+    this.severityOptions = [
+      {value: "1", inputDisplay: "low"},
+      {value: "2", inputDisplay: "medium"},
+      {value: "3", inputDisplay: "high"},
+    ];
+    this.tlpOptions = [
+      {value: "0", inputDisplay: "white"},
+      {value: "1", inputDisplay: "green"},
+      {value: "2", inputDisplay: "amber"},
+      {value: "3", inputDisplay: "red"},
+    ];
+    
+    // Table column definition
+    this.columns = [
       {
-        text: "Cancel",
-        click: function() {
-          $( this ).dialog( "close" );
-        }
+        field: "id",
+        name: "Observable",
       },
       {
-        text: "Submit",
-        click: function(evt) {
-            hiveButtonSubmit(dialog, evt.currentTarget);
-        }
-      }
+        field: "descr",
+        name: "Description",
+        description: "Description of the observable in the context of the case",
+        render: (value, item) => (<EuiFieldText
+          value={item.descr}
+          onChange={(e) => this.onChangeDescr(e, item.i)}
+          disabled={!item.selected}
+        />)
+      },
+      /*{
+        field: "tlp",
+        name: "TLP",
+        dataType: "number",
+        // TODO render and process changes
+      },*/
+      {
+        field: "ioc",
+        name: "Is IOC",
+        dataType: "boolean",
+        description: "Indicates if the observable is an IOC",
+        render: (value, item) => (<EuiCheckbox
+          id={"ioc-checkbox-"+item.id}
+          checked={item.ioc}
+          onChange={(e) => this.onChangeIOC(e, item.i)}
+          disabled={!item.selected}
+        />)
+      },
+      /*{
+        field: "tags",
+        name: "Tags",
+        // TODO render and process changes
+      },*/
     ]
-  });
-  // add reference to visualisation parameters to the dialog element
-  dialog._params = button._vis.params;
-  
-  // add reference to newly created dialog to the button element
-  button._dialog_elem = dialog;
-  return button;
-}
-
-function hiveButtonOnClick(evt) {
-  // Button click -> just open the dialog
-  var button = evt.currentTarget;
-  $(button._dialog_elem).dialog("open");
-  return false;
-}  
+    
+    // Each handler function in a class (method) must be "bind" this way
+    this.submitCase = this.submitCase.bind(this);
+    this.onChangeDescr = this.onChangeDescr.bind(this);
+    this.onChangeTLP = this.onChangeTLP.bind(this);
+    this.onChangeIOC = this.onChangeIOC.bind(this);
+    this.onChangeTags = this.onChangeTags.bind(this);
+    this.onSelectionChange = this.onSelectionChange.bind(this);
+  }
   
-function hiveButtonSubmit(dialog, submit_button) {
-  var params = dialog._params;
-  console.log("Submit", submit_button, params);
+  // Event handlers - change observable parameters
+  onChangeDescr(evt, rowindex) {
+    const val = evt.target.value;
+    this.setState((state, props) => {
+      // Copy old obsDescrs, update the corresponding item and pass as new state
+      let descrs = [...state.obsDescrs];
+      descrs[rowindex] = val;
+      return {obsDescrs: descrs};
+    });
+  }
+  onChangeTLP(evt, rowindex) { // TODO
+    const val = evt.target.value;
+    this.setState((state, props) => {
+      // Copy old obsTLPs, update the corresponding item and pass as new state
+      let tlps = [...state.obsTLPs];
+      tlps[rowindex] = val;
+      return {obsTLPs: tlps};
+    });
+  }
+  onChangeIOC(evt, rowindex) {
+    const val = evt.target.checked;
+    this.setState((state, props) => {
+      // Copy old obsIOCs, update the corresponding item and pass as new state
+      let iocs = [...state.obsIOCs];
+      iocs[rowindex] = val;
+      return {obsIOCs: iocs};
+    });
+  }
+  onChangeTags(evt, rowindex) { // TODO
+    const val = evt.target.value;
+    this.setState((state, props) => {
+      // Copy old obsTags, update the corresponding item and pass as new state
+      let tags = [...state.obsTags];
+      tags[rowindex] = val;
+      return {obsTags: tags};
+    });
+  }
   
-  // load data from the <form>
-  var form = $("form", dialog);
-  console.log(form);
-  var title = $('input[name="case-title"]', form).val();
-  var descr = $('textarea[name="case-descr"]', form).val();
-  var severity = 2;
-  var start_date = null;
-  var owner = params.owner;
-  var flag = false;
-  var tlp = 2;
-  var tags = [];
-
-  // check validity
-  if (title == "") {
-    toastNotifications.addDanger("ERROR: Title cannot be empty");
-    return false;
+  // Event handler - a row is (de)selected
+  onSelectionChange(selectedItems) {
+    // Extract indices from the items and store them into state
+    const selectedIndices = selectedItems.map(item => item.i);
+    this.setState({obsSelected: selectedIndices});
   }
+  
+  // Main render function
+  // TODO should't it be easier to simply store whole table_data in State?
+  //   everything has to be re-rendered on any state change anyway
+  render() {
+    const obs = this.props.observables;
+    // Create data for the observables table
+    let table_data = new Array(obs.length);
+    for (let i = 0; i < obs.length; i++) {
+      table_data[i] = {
+        id: obs[i],
+        descr: this.state.obsDescrs[i],
+        tlp: this.state.obsTLPs[i],
+        ioc: this.state.obsIOCs[i],
+        tags: this.state.obsTags[i],
+        // auxiliary fields, not shown in table:
+        i: i, // row index
+        selected: this.state.obsSelected.includes(i),
+      };
+    }
+    //console.log("render(): Table data:", table_data);
+    
+    return (
+      <EuiOverlayMask>
+        <EuiModal onClose={this.props.close} maxWidth={false} initialFocus="[name=title]">
+          <EuiModalHeader>
+            <EuiModalHeaderTitle>Create a new case in The Hive</EuiModalHeaderTitle>
+          </EuiModalHeader>
 
-  // disable the button to prevent multiple submits
-  submit_button.innerHTML = "(working...)";
-  submit_button.setAttribute("disabled", true);
+          <EuiModalBody>
+            <EuiForm style={{width: "800px"}}>
+              <EuiFlexGroup>
+                <EuiFlexItem grow={1}>
+                  <EuiFormRow label="Title" fullWidth>
+                    <EuiFieldText name="title" value={this.state.title} onChange={(e) => this.setState({title: e.target.value})} required={true} fullWidth />
+                  </EuiFormRow>
+                </EuiFlexItem>
+                <EuiFlexItem grow={false}>
+                  <EuiFormRow label="Severity">
+                    <EuiSuperSelect
+                      options={this.severityOptions}
+                      valueOfSelected={this.state.severity}
+                      onChange={(val) => this.setState({severity: val})}
+                    />
+                  </EuiFormRow>
+                </EuiFlexItem>
+                <EuiFlexItem grow={false}>
+                  <EuiFormRow label="TLP">
+                    <EuiSuperSelect
+                      prepend="TLP"
+                      options={this.tlpOptions}
+                      valueOfSelected={this.state.tlp}
+                      onChange={(val) => this.setState({tlp: val})}
+                    />
+                  </EuiFormRow>
+                </EuiFlexItem>
+              </EuiFlexGroup>
+              <EuiFormRow label="Description" fullWidth>
+                <EuiTextArea
+                  defaultValue={this.state.description}
+                  onChange={(e) => this.setState({description: e.target.value})}
+                  rows={4}
+                  fullWidth
+                />
+              </EuiFormRow>
+              
+              <EuiTitle size="s"><h3>Add observables from current query ...</h3></EuiTitle>
+              <EuiBasicTable
+                columns={this.columns}
+                items={table_data}
+                itemId={(item) => item.id}
+                selection={ {onSelectionChange: this.onSelectionChange} }
+                noItemsMessage="No observables found"
+                rowProps={{
+                  // Hack to allow selection by clicking anywhere in the table row
+                  // (except input elements)
+                  onClick: (e) => {
+                    if (e.target.tagName != "INPUT") {
+                      // simulate click on the first checkbox in the row to (de)select the row
+                      e.currentTarget.querySelector("input").click();
+                      e.currentTarget.blur(); // without this the focus remains on the row after click (results in different color)
+                    }
+                  },
+                  tabIndex: "-1", // prevents focus on row by keyboard navigation
+                }}
+              />
+            </EuiForm>
+          </EuiModalBody>
 
+          <EuiModalFooter>
+            <EuiButtonEmpty onClick={this.props.close}>Cancel</EuiButtonEmpty>
+            <EuiButton onClick={this.submitCase} fill>Create Case</EuiButton>
+          </EuiModalFooter>
+        </EuiModal>
+      </EuiOverlayMask>
+    );
+  }
   
-  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');
-       }
-
-       // re-enable the button
-       submit_button.innerHTML = "Submit";
-       submit_button.removeAttribute("disabled");
-       
-       // close the dialog
-       $(dialog).dialog( "close" );
-    });
-  return false;
+  submitCase(evt) {
+    const params = this.props.params;
+    
+    // Get case parameters
+    const title = this.state.title;
+    const descr = this.state.description;
+    const severity = parseInt(this.state.severity);
+    const start_date = null;
+    const owner = params.owner;
+    const flag = false;
+    const tlp = parseInt(this.state.tlp);
+    const tags = this.state.tags;
+    
+    if (!title) {
+      toastNotifications.addDanger("Title can't be empty");
+      return;
+    }
+    
+    // Get list of selected observables and their params
+    let observables = [];
+    let selectionIndices = [...this.state.obsSelected]; // make a copy
+    selectionIndices.sort();
+    for (let i = 0; i < selectionIndices.length; i++) {
+      const j = selectionIndices[i]; // index of a selected obs. in the list of all observables
+      // fill in observable definition according to model at
+      // https://github.com/TheHive-Project/TheHiveDocs/blob/master/api/artifact.md
+      const obs = {
+        dataType: 'ip',
+        data: this.props.observables[j],
+        message: this.state.obsDescrs[j],
+        tlp: this.state.obsTLPs[j],
+        ioc: this.state.obsIOCs[j],
+        tags: this.state.obsTags[j],
+      };
+      observables.push(obs);
+    }
+    console.log("Selected observables:", observables);
+    
+    // Submit request to create the case, handle response
+    createHiveCase(params.url, params.apikey, title, descr, severity, start_date, owner, flag, tlp, tags)
+      .then((resp) => {
+        if ('error' in resp) {
+          // Error contacting The Hive
+          console.error("TheHiveButton: ERROR when trying to create new case:", resp.error);
+          toastNotifications.addDanger("ERROR: " + resp.error);
+          return;
+        }
+
+        console.log("TheHiveButton: Case created:", resp);
+        const case_id = resp.id;
+        const case_url = params.url + "index.html#/case/" + case_id + "/details";
+        
+        // Show notification
+        let obs_text;
+        if (observables.length > 0) {
+          obs_text = "Adding " + observables.length + " observables in background ...";
+        }
+        else {
+          obs_text = "(no observables added)";
+        }
+        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>
+              <p>{obs_text}</p>
+            </div>
+          ),
+        });
+         
+        // Open a new window with the case in The Hive
+        // (adding observables may take some time, so the case is opened first;
+        //  The Hive web is dynamic so the observables appear as they are added)
+        window.open(case_url, '_blank');
+        
+        if (observables.length == 0)
+          return;
+        
+        // Submit request to add observables
+        console.log("TheHiveButton: adding " + observables.length + " observables ...");
+        addCaseObservables(params.url, params.apikey, case_id, observables)
+          .then((resp) => {
+            if ('error' in resp) {
+              console.error("TheHiveButton: ERROR when trying to add observables: " + resp.error);
+              toastNotifications.addDanger("ERROR when trying to add observables: " + resp.error);
+            }
+            else {
+              console.log("TheHiveButton: Done, observables added.");
+              toastNotifications.add("Done, observables added.");
+            }
+          }); // addObservables().then() func
+      }); // createHiveCase.then() func
+    
+    // Close the popup window
+    this.props.close();
+  }
 }
 
 
+// ********** Functions to send data to Kibana endpoints **********
+
+
 // 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) {
@@ -149,7 +448,7 @@ function createHiveCase(base_url, api_key, title, descr, severity, startDate, ow
       "tags": tags, // array of strings
     }
   });
-  console.log("Sending request to Kibana API endpoint of thehive_button plugin:", data);
+  console.log("TheHiveButton: Sending request to API endpoint 'new_case':", data);
   var kibana_endpoint_url = chrome.addBasePath('/api/thehive_button/new_case');
 
   return new Promise(function (resolve, reject) {
@@ -163,7 +462,7 @@ function createHiveCase(base_url, api_key, title, descr, severity, startDate, ow
       }
       if (this.status == 200) {
         const resp = JSON.parse(this.responseText);
-        console.log("Response from The Hive:", resp);
+        console.log("TheHiveButton: Response from backend:", resp);
         if ("error" in resp) {
           resolve({"error": resp.error});
         }
@@ -175,7 +474,7 @@ function createHiveCase(base_url, api_key, title, descr, severity, startDate, ow
         }
       }
       else {
-        console.log("Error " + this.status + ": " + this.statusText);
+        console.log("TheHiveButton: Error " + this.status + ": " + this.statusText);
         resolve({"error": "Error " + this.status + ": " + this.statusText});
       }
     }
@@ -188,25 +487,43 @@ function createHiveCase(base_url, api_key, title, descr, severity, startDate, ow
   });
 }
 
-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 = '';
-  }
+// Add observables to an existing Case in The Hive
+// (send the list of observables to our backend endpoint, it pushes them to The Hive)
+function addCaseObservables(base_url, api_key, caseid, observables) {
+  const kibana_endpoint_url = chrome.addBasePath('/api/thehive_button/add_observables');
+  const data = JSON.stringify({
+    "base_url": base_url,
+    "api_key": api_key,
+    "caseid": caseid,
+    "observables": observables,
+  });
+  console.log("TheHiveButton: Sending request to API endpoint 'add_observables':", data);
+  
+  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("TheHiveButton: Response from backend:", resp);
+        resolve(resp);
+      }
+      else {
+        console.log("TheHiveButton: Error " + this.status + ": " + this.statusText);
+        resolve({"error": "Error " + this.status + ": " + this.statusText});
+      }
+    }
 
-  async render(visData, status) {
-    return 'done rendering';
-  }
-};
-export { VisController };
+    // 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);
+  });
+}
 
diff --git a/thehive_button/server/routes/newcase.js b/thehive_button/server/routes/newcase.js
index 7400a5f91cbda1c9e203836dc6a77e17a359ee4f..6f796ee82295b6e892a240ebf2eabe30ad207362 100644
--- a/thehive_button/server/routes/newcase.js
+++ b/thehive_button/server/routes/newcase.js
@@ -1,4 +1,8 @@
 const request = require('request');
+//const fs = require('fs');
+//const path = require('path');
+
+//const caFile = path.resolve(__dirname, '../../ca.cert.pem'); // TODO resolve where the CA file should be located / configured
 
 export default function (server) {
   server.route({
@@ -6,6 +10,11 @@ export default function (server) {
     method: 'POST',
     handler: newCaseHandler,
   });
+  server.route({
+    path: '/api/thehive_button/add_observables',
+    method: 'POST',
+    handler: addObservablesHandler,
+  });
 }
 
 // Handler of ajax requests to create a new Case in The Hive
@@ -25,6 +34,7 @@ function newCaseHandler(req, resp) {
   //if (!base_url.match(/https?:\/\/.*\//)) {
     return {'error': 'Invalid base URL (it must begin with "http[s]" and end with "/")'};
   }
+  // TODO add "/" to the end automatically
   if (!api_key) {
     return {'error': 'API key not set'};
   }
@@ -36,18 +46,19 @@ function newCaseHandler(req, resp) {
         auth: {'bearer': api_key},
         json: true,
         body: req_body,
-        //rejectUnauthorized: false, // Disables server certificate check
+        //ca: fs.readFileSync(caFile), // TODO resolve the issue with custom CA, where to get its cert?
+        rejectUnauthorized: false,
       },
       // 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);
+          console.error("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)
+            console.error("ERROR Unexpected reply received from The Hive:", response.statusCode, response.statusMessage, "\n", body)
           }
           resolve({
             'status_code': response.statusCode,
@@ -60,3 +71,84 @@ function newCaseHandler(req, resp) {
   }); // Promise()
 }
 
+// Note:
+// There are two ways to create multiple Observables (artifacts) via The Hive API:
+// 1. post one request with an array of observables in "data" field
+//    - this allows to create all in one request, but doesn't allow to set 
+//      different parameters (IOC, TLP, etc.) to different observables
+// 2. post each observable in a separate request
+// The second way is used here.
+
+// Handler of ajax requests to add Observables to a Case in The Hive
+function addObservablesHandler(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'];
+
+  // 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 "/")'};
+  }
+  // TODO add "/" to the end automatically
+  if (!api_key) {
+    return {'error': 'API key not set'};
+  }
+
+  const caseid = req.payload['caseid'];
+  const observables = req.payload['observables']; // array of obersvable specifications
+
+  return new Promise( async function(resolve, reject) {
+    // Run one request for each observable
+    // (A way to run multiple async tasks sequentially inspired by:
+    //  https://jrsinclair.com/articles/2019/how-to-run-async-js-in-parallel-or-sequential/ )
+    const starterPromise = Promise.resolve(null);
+    await observables.reduce(
+      (p, obs) => p.then(() => addObservable(base_url, api_key, caseid, obs)),
+      starterPromise
+    ).catch((err_msg) => {
+        console.error(err_msg); // log whole message
+        resolve({'error': err_msg.split("\n", 1)[0]}); // send the first line to frontend
+        return;
+      }
+    );
+    resolve({});
+  });
+}
+  
+function addObservable(base_url, api_key, caseid, obs) {
+  return new Promise( function(resolve, reject) {
+    console.log("Adding observable:", obs);
+    request({
+        method: 'POST',
+        url: base_url + 'api/case/' + caseid + "/artifact",
+        auth: {'bearer': api_key},
+        json: true,
+        body: obs,
+        //ca: fs.readFileSync(caFile), // TODO resolve the issue with custom CA, where to get its cert?
+        rejectUnauthorized: false,
+      },
+      // handler of the reply from The Hive - just return as reply
+      function (error, response, body) {
+        if (error) {
+          reject("ERROR when trying to send request to The Hive: " + error);
+        }
+        else if (response.statusCode < 200 || response.statusCode >= 300) {
+          reject("ERROR: Unexpected reply received from The Hive: " + response.statusCode + " " + response.statusMessage + "\n" + JSON.stringify(body));
+        }
+        else {
+          // success - continue with the next observable
+          resolve("OK");
+          resolve({})
+        }
+      } // handler function
+    ); // request()
+  }); //Promise()
+}
+
+