You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@metron.apache.org by rm...@apache.org on 2017/04/11 13:51:12 UTC

[01/12] incubator-metron git commit: METRON-623 Management UI [contributed by Raghu Mitra Kandikonda and Ryan Merriman] closes apache/incubator-metron#489

Repository: incubator-metron
Updated Branches:
  refs/heads/master 0e629f3be -> 1ef8cd8ff


http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/SensorEnrichmentConfigControllerIntegrationTest.java
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/SensorEnrichmentConfigControllerIntegrationTest.java b/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/SensorEnrichmentConfigControllerIntegrationTest.java
index 6a67473..dd4eff7 100644
--- a/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/SensorEnrichmentConfigControllerIntegrationTest.java
+++ b/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/SensorEnrichmentConfigControllerIntegrationTest.java
@@ -236,13 +236,23 @@ public class SensorEnrichmentConfigControllerIntegrationTest {
             .andExpect(content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8")))
             .andExpect(jsonPath("$[?(@.sensorTopic == 'broTest')]").doesNotExist());
 
-    this.mockMvc.perform(get(sensorEnrichmentConfigUrl + "/list/available").with(httpBasic(user,password)))
+    this.mockMvc.perform(get(sensorEnrichmentConfigUrl + "/list/available/enrichments").with(httpBasic(user,password)))
             .andExpect(status().isOk())
             .andExpect(content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8")))
             .andExpect(jsonPath("$[0]").value("geo"))
             .andExpect(jsonPath("$[1]").value("host"))
             .andExpect(jsonPath("$[2]").value("whois"));
 
+    this.mockMvc.perform(get(sensorEnrichmentConfigUrl + "/list/available/threat/triage/aggregators").with(httpBasic(user,password)))
+            .andExpect(status().isOk())
+            .andExpect(content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8")))
+            .andExpect(jsonPath("$[0]").value("MAX"))
+            .andExpect(jsonPath("$[1]").value("MIN"))
+            .andExpect(jsonPath("$[2]").value("SUM"))
+            .andExpect(jsonPath("$[3]").value("MEAN"))
+            .andExpect(jsonPath("$[4]").value("POSITIVE_MEAN"))
+    ;
+
     sensorEnrichmentConfigService.delete("broTest");
   }
 }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/StormControllerIntegrationTest.java
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/StormControllerIntegrationTest.java b/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/StormControllerIntegrationTest.java
index 69d8da6..111b510 100644
--- a/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/StormControllerIntegrationTest.java
+++ b/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/StormControllerIntegrationTest.java
@@ -189,7 +189,9 @@ public class StormControllerIntegrationTest {
             .andExpect(jsonPath("$.id", containsString("broTest")))
             .andExpect(jsonPath("$.status").value("ACTIVE"))
             .andExpect(jsonPath("$.latency").exists())
-            .andExpect(jsonPath("$.throughput").exists());
+            .andExpect(jsonPath("$.throughput").exists())
+            .andExpect(jsonPath("$.emitted").exists())
+            .andExpect(jsonPath("$.acked").exists());
 
     this.mockMvc.perform(get(stormUrl).with(httpBasic(user,password)))
             .andExpect(status().isOk())
@@ -246,7 +248,9 @@ public class StormControllerIntegrationTest {
             .andExpect(jsonPath("$.id", containsString("enrichment")))
             .andExpect(jsonPath("$.status").value("ACTIVE"))
             .andExpect(jsonPath("$.latency").exists())
-            .andExpect(jsonPath("$.throughput").exists());
+            .andExpect(jsonPath("$.throughput").exists())
+            .andExpect(jsonPath("$.emitted").exists())
+            .andExpect(jsonPath("$.acked").exists());
 
     this.mockMvc.perform(get(stormUrl).with(httpBasic(user,password)))
             .andExpect(status().isOk())
@@ -298,7 +302,9 @@ public class StormControllerIntegrationTest {
             .andExpect(jsonPath("$.id", containsString("indexing")))
             .andExpect(jsonPath("$.status").value("ACTIVE"))
             .andExpect(jsonPath("$.latency").exists())
-            .andExpect(jsonPath("$.throughput").exists());
+            .andExpect(jsonPath("$.throughput").exists())
+            .andExpect(jsonPath("$.emitted").exists())
+            .andExpect(jsonPath("$.acked").exists());
 
     this.mockMvc.perform(get(stormUrl).with(httpBasic(user,password)))
             .andExpect(status().isOk())

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/service/impl/GrokServiceImplTest.java
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/service/impl/GrokServiceImplTest.java b/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/service/impl/GrokServiceImplTest.java
index 6dd95c5..1935269 100644
--- a/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/service/impl/GrokServiceImplTest.java
+++ b/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/service/impl/GrokServiceImplTest.java
@@ -232,4 +232,18 @@ public class GrokServiceImplTest {
 
     grokService.saveTemporary(null, "squid");
   }
-}
\ No newline at end of file
+
+  @Test
+  public void getStatementFromClasspathShouldReturnStatement() throws Exception {
+    String expected = FileUtils.readFileToString(new File("../../metron-platform/metron-parsers/src/main/resources/patterns/squid"));
+    assertEquals(expected, grokService.getStatementFromClasspath("/patterns/squid"));
+  }
+
+  @Test
+  public void getStatementFromClasspathShouldThrowRestException() throws Exception {
+    exception.expect(RestException.class);
+    exception.expectMessage("Could not find a statement at path /bad/path");
+
+    grokService.getStatementFromClasspath("/bad/path");
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/service/impl/SensorEnrichmentConfigServiceImplTest.java
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/service/impl/SensorEnrichmentConfigServiceImplTest.java b/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/service/impl/SensorEnrichmentConfigServiceImplTest.java
index d292948..c26a210 100644
--- a/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/service/impl/SensorEnrichmentConfigServiceImplTest.java
+++ b/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/service/impl/SensorEnrichmentConfigServiceImplTest.java
@@ -242,6 +242,17 @@ public class SensorEnrichmentConfigServiceImplTest {
     }}, sensorEnrichmentConfigService.getAvailableEnrichments());
   }
 
+  @Test
+  public void getAvailableThreatTriageAggregatorsShouldReturnAggregators() throws Exception {
+    assertEquals(new ArrayList<String>() {{
+      add("MAX");
+      add("MIN");
+      add("SUM");
+      add("MEAN");
+      add("POSITIVE_MEAN");
+    }}, sensorEnrichmentConfigService.getAvailableThreatTriageAggregators());
+  }
+
   private SensorEnrichmentConfig getTestSensorEnrichmentConfig() {
     SensorEnrichmentConfig sensorEnrichmentConfig = new SensorEnrichmentConfig();
     EnrichmentConfig enrichmentConfig = new EnrichmentConfig();

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest/src/test/resources/README.vm
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest/src/test/resources/README.vm b/metron-interface/metron-rest/src/test/resources/README.vm
index 99f4286..ca54e6a 100644
--- a/metron-interface/metron-rest/src/test/resources/README.vm
+++ b/metron-interface/metron-rest/src/test/resources/README.vm
@@ -10,24 +10,24 @@ This module provides a RESTful API for interacting with Metron.
 
 #[[##]]# Installation
 1. Package the application with Maven:
-```
-mvn clean package
-```
+  ```
+  mvn clean package
+  ```
 
 1. Untar the archive in the target directory.  The directory structure will look like:
-```
-bin
-  start_metron_rest.sh
-lib
-  metron-rest-$METRON_VERSION.jar
-```
+  ```
+  bin
+    start_metron_rest.sh
+  lib
+    metron-rest-$METRON_VERSION.jar
+  ```
 
-1. Create an `application.yml` file with the contents of [application-docker.yml](src/main/resources/application-docker.yml).  Substitute the appropriate Metron service urls (Kafka, Zookeeper, Storm, etc) in properties containing `${docker.host.address}` and update the `spring.datasource.*` properties as needed (see the [Security](#security) section for more details).
+1. Create an `application.yml` file with the contents of [application-docker.yml](src/main/resources/application-docker.yml).  Substitute the appropriate Metron service urls (Kafka, Zookeeper, Storm, etc.) in properties containing `${docker.host.address}` and update the `spring.datasource.*` properties as needed (see the [Security](#security) section for more details).
 
 1. Start the application with this command:
-```
-./bin/start_metron_rest.sh /path/to/application.yml
-```
+  ```
+  ./bin/start_metron_rest.sh /path/to/application.yml
+  ```
 
 #[[##]]# Usage
 
@@ -45,36 +45,36 @@ For [production use](http://docs.spring.io/spring-boot/docs/1.4.1.RELEASE/refere
 1. Create a MySQL user for the Metron REST application (http://dev.mysql.com/doc/refman/5.7/en/adding-users.html).
 
 1. Connect to MySQL and create a Metron REST database:
-```
-CREATE DATABASE IF NOT EXISTS metronrest
-```
+  ```
+  CREATE DATABASE IF NOT EXISTS metronrest
+  ```
 
 1. Add users:
-```
-use metronrest;
-insert into users (username, password, enabled) values ('your_username','your_password',1);
-insert into authorities (username, authority) values ('your_username', 'ROLE_USER');
-```
+  ```
+  use metronrest;
+  insert into users (username, password, enabled) values ('your_username','your_password',1);
+  insert into authorities (username, authority) values ('your_username', 'ROLE_USER');
+  ```
 
 1. Replace the H2 connection information in the application.yml file with MySQL connection information:
-```
-spring:
-  datasource:
-        driverClassName: com.mysql.jdbc.Driver
-        url: jdbc:mysql://mysql_host:3306/metronrest
-        username: metron_rest_user
-        password: metron_rest_password
-        platform: mysql
-```
+  ```
+  spring:
+    datasource:
+          driverClassName: com.mysql.jdbc.Driver
+          url: jdbc:mysql://mysql_host:3306/metronrest
+          username: metron_rest_user
+          password: metron_rest_password
+          platform: mysql
+  ```
 
 1. Add a dependency for the MySQL JDBC connector in the metron-rest pom.xml:
-```
-<dependency>
-  <groupId>mysql</groupId>
-  <artifactId>mysql-connector-java</artifactId>
-  <version>${mysql.client.version}</version>
-</dependency>
-```
+  ```
+  <dependency>
+    <groupId>mysql</groupId>
+    <artifactId>mysql-connector-java</artifactId>
+    <version>${mysql.client.version}</version>
+  </dependency>
+  ```
 
 1. Follow the steps in the [Installation](#installation) section
 
@@ -85,7 +85,7 @@ Request and Response objects are JSON formatted.  The JSON schemas are available
 |            |
 | ---------- |
 #foreach( $restControllerInfo in $endpoints )
-| [ `$restControllerInfo.getMethod().toString() $restControllerInfo.getPath()`](#$restControllerInfo.getMethod().toString().toLowerCase()-$restControllerInfo.getPath().toLowerCase().replaceAll("/", ""))|
+| [ `$restControllerInfo.getMethod().toString() $restControllerInfo.getPath()`](#$restControllerInfo.getMethod().toString().toLowerCase()-$restControllerInfo.getPath().toLowerCase().replaceAll("[/\{\}]", ""))|
 #end
 
 #foreach( $restControllerInfo in $endpoints )

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/pom.xml
----------------------------------------------------------------------
diff --git a/metron-interface/pom.xml b/metron-interface/pom.xml
index 805c024..d762924 100644
--- a/metron-interface/pom.xml
+++ b/metron-interface/pom.xml
@@ -39,6 +39,7 @@
         </license>
     </licenses>
     <modules>
+        <module>metron-config</module>
         <module>metron-rest</module>
         <module>metron-rest-client</module>
     </modules>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 7907435..e86c5de 100644
--- a/pom.xml
+++ b/pom.xml
@@ -301,7 +301,20 @@
                         <exclude>**/hbase/data/**</exclude>
                         <exclude>**/kafkazk/data/**</exclude>
                         <exclude>**/wait-for-it.sh</exclude>
-			<exclude>**/*.out</exclude>
+			                  <exclude>**/*.out</exclude>
+                        <!-- Directory containing dependencies downloaded by NPM -->
+                        <exclude>node_modules/**</exclude>
+                        <!-- Nodejs installed locally by the frontend-maven-plugin -->
+                        <exclude>node/**</exclude>
+                        <!-- Javascript code coverage report generated by Istanbul -->
+                        <exclude>coverage/**</exclude>
+                        <!-- ACE editor assets are covered in the metron-config NOTICE file -->
+                        <exclude>**/src/assets/ace/**</exclude>
+                        <exclude>dist/assets/ace/**</exclude>
+                        <!-- Generated svg containing Font Awesome fonts are covered in the metron-config README and NOTICE file -->
+                        <exclude>dist/*.svg</exclude>
+
+                        <exclude>e2e/*.js.map</exclude>
                     </excludes>
                 </configuration>
             </plugin>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/site-book/bin/generate-md.sh
----------------------------------------------------------------------
diff --git a/site-book/bin/generate-md.sh b/site-book/bin/generate-md.sh
index 65bbfd7..0859b7c 100755
--- a/site-book/bin/generate-md.sh
+++ b/site-book/bin/generate-md.sh
@@ -49,6 +49,7 @@ EXCLUSION_LIST=(
     '/site/'
     '/site-book/'
     '/build_utils/'
+    '/node_modules/'
     '/\.github/'
 )
 


[11/12] incubator-metron git commit: METRON-623 Management UI [contributed by Raghu Mitra Kandikonda and Ryan Merriman] closes apache/incubator-metron#489

Posted by rm...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/e2e/sensor-list/sensor-list.po.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/e2e/sensor-list/sensor-list.po.ts b/metron-interface/metron-config/e2e/sensor-list/sensor-list.po.ts
new file mode 100644
index 0000000..b7f5167
--- /dev/null
+++ b/metron-interface/metron-config/e2e/sensor-list/sensor-list.po.ts
@@ -0,0 +1,240 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions andx
+ * limitations under the License.
+ */
+
+import { browser, element, by, protractor } from 'protractor/globals';
+import { waitForElementPresence, waitForStalenessOf } from '../utils/e2e_util';
+var Promise = require('bluebird');
+
+export class SensorListPage {
+
+    clickOnActionsAndWait(parserNames: string[], clickOnClassName: string, waitOnClassName: string) {
+        let queueForClick = Promise.resolve();
+        parserNames.forEach((name) => {
+            queueForClick = queueForClick.then((result) => {
+                return this.getIconButton(name, clickOnClassName).click();
+            });
+        });
+
+        return queueForClick.then(() => {
+            let promiseArray = [];
+            parserNames.map(name => {
+                promiseArray.push(this.waitForElement(this.getIconButton(name, waitOnClassName)));
+            });
+
+            return protractor.promise.all(promiseArray).then(args => {
+                return args;
+            });
+        });
+    }
+
+    clickOnDropdownAndWait(parserNames: string[], dropDownLinkName: string, waitOnClassName: string) {
+        return protractor.promise.all([this.toggleSelectAll(), this.toggleDropdown()]).then(() => {
+
+            return element(by.css('span[data-action=\"'+ dropDownLinkName +'\"]')).click().then(() => {
+                let promiseArray = [];
+                parserNames.map(name => {
+                    promiseArray.push(this.waitForElement(this.getIconButton(name, waitOnClassName)));
+                });
+
+                return protractor.promise.all(promiseArray).then(args => {
+                    return this.toggleSelectAll().then(() => {
+                        return args;
+                    })
+                });
+            });
+        });
+    }
+
+    closePane(className: string ='.close-button') {
+        return this.waitForElement(element(by.css(className))).then(() => {
+            return element(by.css(className)).click();
+        });
+    }
+
+    disableParsers(names: string[]) {
+        return this.clickOnActionsAndWait(names, 'i.fa-ban', 'i.fa-check-circle-o');
+    }
+
+    disableParsersFromDropdown(names: string[]) {
+        return this.clickOnDropdownAndWait(names, 'Disable', 'i.fa-check-circle-o');
+    }
+
+    deleteParser(name: string) {
+        return this.getIconButton(name, '.fa-trash-o').click().then(() => {
+            browser.sleep(1000);
+            return element(by.css('.metron-dialog .btn-primary')).click().then(() => {
+                browser.sleep(1000);
+                return element(by.css('.alert .close')).click().then(() => {
+                    return waitForStalenessOf(element(by.cssContainingText('td', name))).then(() =>{
+                        return true;
+                    });
+                })
+            });
+        });
+    }
+
+    enableParsers(names: string[]) {
+        return this.clickOnActionsAndWait(names, 'i.fa-check-circle-o', 'i.fa-ban');
+    }
+
+    enableParsersFromDropdown(names: string[]) {
+        return this.clickOnDropdownAndWait(names, 'Enable', 'i.fa-ban');
+    }
+
+    getActions(name: string) {
+        return element.all(by.css('table>tbody>tr')).filter(row => {
+            return row.all(by.tagName('td')).get(0).getText().then(pName => {
+                return pName === name;
+            })
+        }).get(0).all(by.tagName('i')).map(icon => {
+            return icon.getAttribute('class').then(classNames => {
+                let className = classNames.replace('fa ', '').replace('fa-lg', '').replace('fa-spin  fa-fw', '').trim();
+                return {classNames: className, displayed: icon.isDisplayed()};
+            });
+        });
+    }
+
+    getAddButton() {
+        return element(by.css('.metron-add-button.hexa-button .fa-plus')).isPresent();
+    }
+
+    getColumnValues(colId: number) {
+        return element.all(by.css('table tbody tr')).map(function(elm) {
+            return elm.all(by.css('td')).get(colId).getText();
+        });
+    }
+
+    getDropdownActionState() {
+        return protractor.promise.all([
+            element.all(by.css('.dropdown.open .dropdown-menu span:not(.disabled)')).count(),
+            element.all(by.css('.dropdown.open .dropdown-menu span.disabled')).count(),
+            element.all(by.css('.dropdown-menu')).isDisplayed()
+        ]).then(args => {
+            return  {
+                enabled: args[0],
+                disabled: args[1],
+                displayed: args[2][0],
+            }
+        });
+    }
+
+    getIconButton(name: string, className: string) {
+        return element.all(by.css('table>tbody>tr')).filter(row => {
+            return row.all(by.tagName('td')).get(0).getText().then(pName => {
+                return pName === name;
+            })
+        }).get(0).element(by.css(className));
+    }
+
+    getParserCount() {
+        browser.waitForAngular();
+        return element.all(by.css('table>tbody>tr')).count();
+    }
+
+    getRow(name: string) {
+        return element.all(by.css('table>tbody>tr')).filter(row => {
+            return row.all(by.tagName('td')).get(0).getText().then(pName => {
+                return pName === name;
+            })
+        }).get(0);
+    }
+
+    getSelectedRowCount() {
+        return element.all(by.css('tr.active')).count();
+    }
+
+    getSortOrder(name: string) {
+        return element(by.linkText(name)).element(by.tagName('i')).getAttribute('class');
+    }
+
+    getTableColumnNames() {
+        return element.all(by.css('table th a')).map(function(elm) {
+            return elm.getText();
+        });
+    }
+
+    getTitle() {
+        return element(by.css('.metron-title')).getText();
+    }
+
+    load() {
+        return browser.get('/sensors');
+    }
+
+    openDetailsPane(name: string) {
+        return this.getRow(name).click().then(() =>{
+            return browser.getCurrentUrl();
+        });
+    }
+
+    openEditPane(name: string) {
+        let row = element(by.cssContainingText('td', name));
+        return waitForElementPresence(row).then(() => {
+            return this.getIconButton(name, '.fa-pencil').click().then(() =>{
+                return browser.getCurrentUrl();
+            });
+        })
+    }
+
+    openEditPaneAndClose(name: string) {
+        return this.getIconButton(name, '.fa-pencil').click().then(() =>{
+            let url = browser.getCurrentUrl();
+            browser.sleep(500);
+            return this.closePane('.main.close-button').then(() => {
+                return url;
+            });
+        });
+    }
+
+    startParsers(names: string[]) {
+        return this.clickOnActionsAndWait(names, 'i.fa-play', 'i.fa-stop');
+    }
+
+    startParsersFromDropdown(names: string[]) {
+        return this.clickOnDropdownAndWait(names, 'Start', 'i.fa-stop');
+    }
+
+    stopParsers(names: string[]) {
+        return this.clickOnActionsAndWait(names, 'i.fa-stop', 'i.fa-play');
+    }
+
+    stopParsersFromDropdown(names: string[]) {
+        return this.clickOnDropdownAndWait(names, 'Stop', 'i.fa-play');
+    }
+
+    toggleDropdown() {
+        return element.all(by.id('dropdownMenu1')).click();
+    }
+
+    toggleRowSelect(name: string) {
+        element.all(by.css('label[for=\"'+name+'\"]')).click();
+    }
+
+    toggleSelectAll() {
+        return element.all(by.css('label[for="select-deselect-all"]')).click();
+    }
+
+    toggleSort(name: string) {
+        element.all(by.linkText(name)).click();
+    }
+
+    waitForElement ( _element ) {
+        var EC = protractor.ExpectedConditions;
+        return browser.wait(EC.visibilityOf(_element));
+    };
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/e2e/tsconfig.json
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/e2e/tsconfig.json b/metron-interface/metron-config/e2e/tsconfig.json
new file mode 100644
index 0000000..656bdb1
--- /dev/null
+++ b/metron-interface/metron-config/e2e/tsconfig.json
@@ -0,0 +1,16 @@
+{
+  "compileOnSave": false,
+  "compilerOptions": {
+    "declaration": false,
+    "emitDecoratorMetadata": true,
+    "experimentalDecorators": true,
+    "module": "commonjs",
+    "moduleResolution": "node",
+    "outDir": "../dist/out-tsc-e2e",
+    "sourceMap": true,
+    "target": "es5",
+    "typeRoots": [
+      "../node_modules/@types"
+    ]
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/e2e/use-cases/sensor-config-single-parser.e2e-spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/e2e/use-cases/sensor-config-single-parser.e2e-spec.ts b/metron-interface/metron-config/e2e/use-cases/sensor-config-single-parser.e2e-spec.ts
new file mode 100644
index 0000000..dd1d71f
--- /dev/null
+++ b/metron-interface/metron-config/e2e/use-cases/sensor-config-single-parser.e2e-spec.ts
@@ -0,0 +1,159 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {LoginPage} from '../login/login.po';
+import {SensorConfigPage} from '../sensor-config/sensor-config.po';
+import {SensorListPage} from '../sensor-list/sensor-list.po';
+import {SensorDetailsPage} from '../sensor-config-readonly/sensor-config-readonly.po';
+
+describe('Sensor Config for parser e2e1', function() {
+  let page = new SensorConfigPage();
+  let sensorListPage = new SensorListPage();
+  let sensorDetailsPage = new SensorDetailsPage();
+  let loginPage = new LoginPage();
+
+  beforeAll(() => {
+    loginPage.login();
+  });
+
+  afterAll(() => {
+    loginPage.logout();
+  });
+
+  it('should add e2e parser', (done) => {
+    let expectedGrokResponse = [
+      'action TCP_MISS',
+      'bytes 337891',
+      'code 200',
+      'elapsed 415',
+      'ip_dst_addr 207.109.73.154',
+      'ip_src_addr 127.0.0.1',
+      'method GET',
+      'original_string 1467011157.401 415 127.0.0.1 TCP_MISS/200 337891 GET http://www.aliexpress.com/af/shoes.html? - DIRECT/207.109.73.154 text/html', 'timestamp 1467011157.401', 'url http://www.aliexpress.com/af/shoes.html?' ];
+    let grokStatement = '%{NUMBER:timestamp} %{INT:elapsed} %{IPV4:ip_src_addr} %{WORD:action}/%{NUMBER:code} %{NUMBER:bytes} %{WORD:method} %{NOTSPACE:url} - %{WORD:UNWANTED}\/%{IPV4:ip_dst_addr} %{WORD:UNWANTED}\/%{WORD:UNWANTED}';
+    let sampleMessage = '1467011157.401 415 127.0.0.1 TCP_MISS/200 337891 GET http://www.aliexpress.com/af/shoes.html? - DIRECT/207.109.73.154 text/html';
+    let expectedFieldSchemaResponse = [ 'elapsed', 'code', 'ip_dst_addr', 'original_string', 'method', 'bytes', 'action', 'ip_src_addr', 'url', 'timestamp' ];
+
+    page.clickAddButton();
+    page.setParserName('e2e1');
+    page.setParserType('Grok');
+
+    page.clickGrokStatement();
+    page.setSampleMessage('sensor-grok', sampleMessage);
+    page.setGrokStatement(grokStatement);
+    page.testGrokStatement();
+    expect(page.getGrokResponse()).toEqual(expectedGrokResponse);
+    page.saveGrokStatement();
+    expect(page.getGrokStatementFromMainPane()).toEqual([grokStatement]);
+    page.setAdvancedConfig('grokPath', 'target/patterns/e2e1');
+
+
+    page.clickSchema();
+    page.setSampleMessage('sensor-field-schema', '1467011157.401 415 127.0.0.1 TCP_MISS/200 337891 GET http://www.aliexpress.com/af/shoes.html? - DIRECT/207.109.73.154 text/html');
+    page.clickSchema();
+    expect(page.getFieldSchemaValues()).toEqual(expectedFieldSchemaResponse);
+    page.setSchemaConfig('elapsed', ['TRIM', 'TO_INTEGER'], ['geo', 'host'], ['malicious_ip']);
+    expect(page.getTransformText()).toEqual(['TO_INTEGER(TRIM(elapsed))']);
+    page.saveFieldSchemaConfig();
+    page.setSchemaConfig('ip_dst_addr', [], ['geo'], ['malicious_ip']);
+    page.saveFieldSchemaConfig();
+    page.closeSchemaPane();
+    expect(page.getFieldSchemaSummary()).toEqual( [ 'TRANSFORMATIONS 1', 'ENRICHMENTS 3', 'THREAT INTEL 2' ]);
+
+    page.clickThreatTriage();
+    page.clickAddThreatTriageRule();
+    page.setThreatTriageRule('IN_SUBNET(ip_dst_addr, \'192.168.0.0/24\')');
+    page.saveThreatTriageRule();
+    expect(page.getThreatTrigaeRule()).toEqual([ 'IN_SUBNET(ip_dst_addr, \'192.168.0.0/24\')']);
+    page.closeThreatTriagePane();
+    expect(page.getThreatTriageSummary()).toEqual([ 'RULES 1' ]);
+
+    page.saveParser();
+    
+    done();
+
+  });
+
+  it('should have all the config for e2e parser', (done) => {
+    let grokStatement = '%{NUMBER:timestamp} %{INT:elapsed} %{IPV4:ip_src_addr} %{WORD:action}/%{NUMBER:code} %{NUMBER:bytes} %{WORD:method} %{NOTSPACE:url} - %{WORD:UNWANTED}/%{IPV4:ip_dst_addr} %{WORD:UNWANTED}/%{WORD:UNWANTED}';
+    let expectedFormData = {
+      title: 'e2e1',
+      parserName: 'e2e1',
+      parserType: 'org.apache.metron.parsers.GrokParser',
+      grokStatement: grokStatement,
+      fieldSchemaSummary: [ 'TRANSFORMATIONS 1', 'ENRICHMENTS 3', 'THREAT INTEL 2' ],
+      threatTriageSummary: [ 'RULES 1' ],
+      indexName: 'e2e1',
+      batchSize: '1',
+      advancedConfig: [ 'patternLabel', 'E2E1', 'grokPath', 'target/patterns/e2e1', 'enter field', 'enter value' ]
+    };
+    expect(sensorListPage.openEditPane('e2e1')).toEqual('http://localhost:4200/sensors(dialog:sensors-config/e2e1)');
+    expect(page.getFormData()).toEqual(expectedFormData);
+
+    page.closeMainPane().then(() => {
+      done();
+    });
+  })
+
+  it('should have all the config details for  e2e parser', () => {
+    let parserNotRunnigExpected = ['',
+      'PARSERS\nGrok',
+      'LAST UPDATED\n-',
+      'LAST EDITOR\n-',
+      'STATE\n-',
+      'ORIGINATOR\n-',
+      'CREATION DATE\n-',
+      ' ',
+      'STORM\nStopped',
+      'LATENCY\n-',
+      'THROUGHPUT\n-',
+      'EMITTED(10 MIN)\n-',
+      'ACKED(10 MIN)\n-',
+      ' ',
+      'KAFKA\nNo Kafka Topic',
+      'PARTITONS\n-',
+      'REPLICATION FACTOR\n-',
+      ''];
+    let grokStatement = '%{NUMBER:timestamp} %{INT:elapsed} %{IPV4:ip_src_addr} %{WORD:action}/%{NUMBER:code} %{NUMBER:bytes} %{WORD:method} %{NOTSPACE:url} - %{WORD:UNWANTED}\/%{IPV4:ip_dst_addr} %{WORD:UNWANTED}\/%{WORD:UNWANTED}';
+
+    expect(sensorDetailsPage.navigateTo('e2e1')).toEqual('http://localhost:4200/sensors(dialog:sensors-readonly/e2e1)');
+    expect(sensorDetailsPage.getTitle()).toEqual("e2e1");
+    expect(sensorDetailsPage.getParserConfig()).toEqual(parserNotRunnigExpected);
+    expect(sensorDetailsPage.getButtons()).toEqual([ 'EDIT', 'START', 'Delete' ]);
+    expect(sensorDetailsPage.getGrokStatement()).toEqual(grokStatement);
+    expect(sensorDetailsPage.getSchemaSummary()).toEqual(['Transforms\nelapsed']);
+    sensorDetailsPage.clickToggleShowMoreLess('show more', 1);
+    expect(sensorDetailsPage.getSchemaFullSummary()).toEqual([ 'Transforms\nelapsed\nTO_INTEGER(TRIM(elapsed))' ]);
+    sensorDetailsPage.clickToggleShowMoreLess('show less', 0);
+    expect(sensorDetailsPage.getThreatTriageSummary()).toEqual(['AGGREGATOR\nMAX\nIN_SUBNET(ip_dst_addr, \'192.168.0.0/24\')\nshow more']);
+    sensorDetailsPage.clickToggleShowMoreLess('show more', 2);
+    expect(sensorDetailsPage.getThreatTriageSummary()).toEqual(['AGGREGATOR\nMAX\nNAME\nSCORE\nIN_SUBNET(ip_dst_addr, \'192.168.0.0/24\')\n0\nshow less']);
+    sensorDetailsPage.clickToggleShowMoreLess('show less', 0);
+
+    sensorDetailsPage.closePane('e2e1');
+    
+  })
+
+
+  it('should delete the e2e parser', (done) => {
+    expect(sensorListPage.getParserCount()).toEqual(8);
+    expect(sensorListPage.deleteParser('e2e1')).toEqual(true);
+    expect(sensorListPage.getParserCount()).toEqual(7);
+    done();
+  })
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/e2e/utils/e2e_util.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/e2e/utils/e2e_util.ts b/metron-interface/metron-config/e2e/utils/e2e_util.ts
new file mode 100644
index 0000000..7ca9960
--- /dev/null
+++ b/metron-interface/metron-config/e2e/utils/e2e_util.ts
@@ -0,0 +1,30 @@
+import { browser, protractor } from 'protractor/globals';
+
+export function changeURL(url: string) {
+    return browser.get(url).then(() => {
+        return browser.getCurrentUrl().then((newURL) => {
+            return newURL;
+        })
+    })
+}
+
+export function waitForElementInVisibility (_element ) {
+    var EC = protractor.ExpectedConditions;
+    return browser.wait(EC.invisibilityOf(_element));
+}
+
+export function waitForElementPresence (_element ) {
+    var EC = protractor.ExpectedConditions;
+    return browser.wait(EC.presenceOf(_element));
+}
+
+export function waitForElementVisibility (_element ) {
+    var EC = protractor.ExpectedConditions;
+    return browser.wait(EC.visibilityOf(_element));
+}
+
+export function waitForStalenessOf (_element ) {
+    var EC = protractor.ExpectedConditions;
+    return browser.wait(EC.stalenessOf(_element));
+}
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/karma.conf.js
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/karma.conf.js b/metron-interface/metron-config/karma.conf.js
new file mode 100644
index 0000000..6023550
--- /dev/null
+++ b/metron-interface/metron-config/karma.conf.js
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/0.13/config/configuration-file.html
+
+module.exports = function (config) {
+  config.set({
+    basePath: '',
+    frameworks: ['jasmine', 'angular-cli'],
+    plugins: [
+      require('karma-jasmine'),
+      require('karma-chrome-launcher'),
+      require('karma-remap-istanbul'),
+      require('karma-phantomjs-launcher'),
+      require('angular-cli/plugins/karma')
+    ],
+    mime: {
+      'text/x-typescript': ['ts','tsx']
+    },
+    files: [
+      { pattern: './src/test.ts', watched: false },
+      { pattern: './src/assets/**', watched: false, included: false, nocache: false, served: true }
+    ],
+    proxies: {
+      '/assets': '/base/src/assets/'
+    },
+    preprocessors: {
+      './src/test.ts': ['angular-cli']
+    },
+    remapIstanbulReporter: {
+      reports: {
+        html: 'coverage',
+        lcovonly: './coverage/coverage.lcov'
+      }
+    },
+    angularCli: {
+      config: './angular-cli.json',
+      environment: 'dev'
+    },
+    reporters: ['progress', 'karma-remap-istanbul'],
+    port: 9876,
+    colors: true,
+    logLevel: config.LOG_INFO,
+    autoWatch: true,
+    browsers: ['PhantomJS'],
+    singleRun: false
+  });
+};

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/package.json
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/package.json b/metron-interface/metron-config/package.json
new file mode 100644
index 0000000..924d916
--- /dev/null
+++ b/metron-interface/metron-config/package.json
@@ -0,0 +1,65 @@
+{
+  "name": "metron-management-ui",
+  "version": "0.3.1",
+  "license": "MIT",
+  "config": {
+    "node_ace": "node_modules/ace-builds/src-min-noconflict/"
+  },
+  "angular-cli": {},
+  "scripts": {
+    "build": "./node_modules/angular-cli/bin/ng build -prod",
+    "start": "ng serve",
+    "lint": "tslint \"src/**/*.ts\"",
+    "test": "./node_modules/angular-cli/bin/ng test --watch=false",
+    "pree2e": "webdriver-manager update",
+    "e2e": "./node_modules/.bin/protractor",
+    "e2e-all": "./node_modules/.bin/protractor --suite=all",
+    "copy-ace": "cp -f $npm_package_config_node_ace/ext-language_tools.js $npm_package_config_node_ace/mode-json.js $npm_package_config_node_ace/theme-monokai.js $npm_package_config_node_ace/worker-json.js src/assets/ace",
+    "copy-ace-snippets": "cp -f $npm_package_config_node_ace/snippets/text.js $npm_package_config_node_ace/snippets/json.js  src/assets/ace/snippets",
+    "postinstall": "npm run copy-ace & npm run copy-ace-snippets"
+  },
+  "private": true,
+  "dependencies": {
+    "@types/ace": "0.0.32",
+    "@types/bootstrap": "^3.3.32",
+    "@types/jasmine": "2.2.30",
+    "@types/jquery": "^2.0.32",
+    "@types/tether": "^1.1.27",
+    "@angular/common": "2.0.0",
+    "@angular/compiler": "2.0.0",
+    "@angular/core": "2.0.0",
+    "@angular/forms": "2.0.0",
+    "@angular/http": "2.0.0",
+    "@angular/platform-browser": "2.0.0",
+    "@angular/platform-browser-dynamic": "2.0.0",
+    "@angular/router": "3.0.0",
+    "ace-builds": "^1.2.5",
+    "bootstrap": "4.0.0-alpha.5",
+    "core-js": "^2.4.1",
+    "font-awesome": "^4.6.3",
+    "jquery": "^2.2.4",
+    "rxjs": "5.0.0-beta.12",
+    "tether": "^1.3.4",
+    "ts-helpers": "^1.1.1",
+    "zone.js": "^0.6.23"
+  },
+  "devDependencies": {
+    "angular-cli": "1.0.0-beta.15",
+    "buffer-shims": "^1.0.0",
+    "codelyzer": "~0.0.26",
+    "copy": "^0.3.0",
+    "jasmine-core": "2.4.1",
+    "jasmine-spec-reporter": "2.5.0",
+    "karma": "1.2.0",
+    "karma-chrome-launcher": "^2.0.0",
+    "karma-cli": "^1.0.1",
+    "karma-jasmine": "^1.0.2",
+    "karma-phantomjs-launcher": "^1.0.4",
+    "karma-remap-istanbul": "^0.2.1",
+    "phantomjs-prebuilt": "^2.1.14",
+    "protractor": "4.0.5",
+    "ts-node": "1.2.1",
+    "tslint": "3.13.0",
+    "typescript": "~2.0.3"
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/pom.xml
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/pom.xml b/metron-interface/metron-config/pom.xml
new file mode 100644
index 0000000..97a7404
--- /dev/null
+++ b/metron-interface/metron-config/pom.xml
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Licensed to the Apache Software 
+  Foundation (ASF) under one or more contributor license agreements. See the 
+  NOTICE file distributed with this work for additional information regarding 
+  copyright ownership. The ASF licenses this file to You under the Apache License, 
+  Version 2.0 (the "License"); you may not use this file except in compliance 
+  with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 
+  Unless required by applicable law or agreed to in writing, software distributed 
+  under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 
+  OR CONDITIONS OF ANY KIND, either express or implied. See the License for 
+  the specific language governing permissions and limitations under the License. 
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.metron</groupId>
+        <artifactId>metron-interface</artifactId>
+        <version>0.3.1</version>
+    </parent>
+    <artifactId>metron-config</artifactId>
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <node.version>v6.2.0</node.version>
+        <npm.version>3.8.9</npm.version>
+    </properties>
+    <dependencies>
+    </dependencies>
+
+    <build>
+        <plugins>
+          <plugin>
+            <groupId>com.github.eirslett</groupId>
+            <artifactId>frontend-maven-plugin</artifactId>
+            <version>1.3</version>
+            <configuration>
+              <workingDirectory>./</workingDirectory>
+              <nodeVersion>${node.version}</nodeVersion>
+              <npmVersion>${npm.version}</npmVersion>
+              <npmInheritsProxyConfigFromMaven>false</npmInheritsProxyConfigFromMaven>
+            </configuration>
+            <executions>
+              <execution>
+                <phase>generate-resources</phase>
+                <id>install node and npm</id>
+                <goals>
+                  <goal>install-node-and-npm</goal>
+                </goals>
+              </execution>
+              <execution>
+                <phase>generate-resources</phase>
+                <id>npm install</id>
+                <goals>
+                  <goal>npm</goal>
+                </goals>
+                <configuration>
+                  <arguments>install</arguments>
+                </configuration>
+              </execution>
+              <execution>
+                <phase>generate-resources</phase>
+                <id>ng build</id>
+                <goals>
+                  <goal>npm</goal>
+                </goals>
+                <configuration>
+                  <arguments>run build</arguments>
+                </configuration>
+              </execution>
+              <execution>
+                <id>npm test</id>
+                <goals>
+                  <goal>npm</goal>
+                </goals>
+                <phase>test</phase>
+                <configuration>
+                  <arguments>test</arguments>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+          <plugin>
+            <artifactId>maven-clean-plugin</artifactId>
+            <version>3.0.0</version>
+            <configuration>
+              <filesets>
+                <fileset>
+                  <directory>coverage</directory>
+                  <followSymlinks>false</followSymlinks>
+                </fileset>
+                <fileset>
+                  <directory>dist</directory>
+                  <followSymlinks>false</followSymlinks>
+                </fileset>
+                <fileset>
+                  <directory>node</directory>
+                  <followSymlinks>false</followSymlinks>
+                </fileset>
+                <fileset>
+                  <directory>node_modules</directory>
+                  <followSymlinks>false</followSymlinks>
+                </fileset>
+              </filesets>
+            </configuration>
+          </plugin>
+          <plugin>
+            <artifactId>maven-assembly-plugin</artifactId>
+            <configuration>
+              <descriptor>assembly.xml</descriptor>
+            </configuration>
+            <executions>
+              <execution>
+                <id>make-assembly</id> <!-- this is used for inheritance merges -->
+                <phase>package</phase> <!-- bind to the packaging phase -->
+                <goals>
+                  <goal>single</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>exec-maven-plugin</artifactId>
+            <version>1.5.0</version>
+            <executions>
+              <execution>
+                <id>prepend-license-header</id>
+                <phase>prepare-package</phase>
+                <goals>
+                  <goal>exec</goal>
+                </goals>
+                <configuration>
+                  <executable>./scripts/prepend_license_header.sh</executable>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+    </build>
+</project>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/protractor.conf.js
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/protractor.conf.js b/metron-interface/metron-config/protractor.conf.js
new file mode 100644
index 0000000..7289122
--- /dev/null
+++ b/metron-interface/metron-config/protractor.conf.js
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Protractor configuration file, see link for more information
+// https://github.com/angular/protractor/blob/master/docs/referenceConf.js
+
+/*global jasmine */
+var SpecReporter = require('jasmine-spec-reporter');
+
+exports.config = {
+  allScriptsTimeout: 11000,
+  suites: {
+    all: [
+      './e2e/login/login.e2e-spec.ts',
+      './e2e/app/app.e2e-spec.ts',
+      './e2e/sensor-list/sensor-list.e2e-spec.ts',
+      './e2e/sensor-list/sensor-list-parser-actions.e2e-spec.ts',
+      './e2e/use-cases/sensor-config-single-parser.e2e-spec.ts',
+      './e2e/sensor-config-readonly/sensor-config-readonly.e2e-spec.ts',
+    ]
+  },
+  specs: [
+    './e2e/login/login.e2e-spec.ts',
+    './e2e/app/app.e2e-spec.ts',
+    './e2e/sensor-list/sensor-list.e2e-spec.ts',
+    './e2e/use-cases/sensor-config-single-parser.e2e-spec.ts'
+  ],
+  capabilities: {
+    'browserName': 'chrome'
+  },
+  directConnect: true,
+  baseUrl: 'http://localhost:4200',
+  framework: 'jasmine',
+  jasmineNodeOpts: {
+    showColors: true,
+    defaultTimeoutInterval: 30000,
+    print: function() {}
+  },
+  useAllAngular2AppRoots: true,
+  rootElement: 'metron-config-root',
+  beforeLaunch: function() {
+    require('ts-node').register({
+      project: 'e2e'
+    });
+  },
+  onPrepare: function() {
+    jasmine.getEnv().addReporter(new SpecReporter());
+  }
+};

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/proxy.conf.json
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/proxy.conf.json b/metron-interface/metron-config/proxy.conf.json
new file mode 100644
index 0000000..29466cc
--- /dev/null
+++ b/metron-interface/metron-config/proxy.conf.json
@@ -0,0 +1,10 @@
+{
+  "/api/v1": {
+    "target": "http://localhost:8080",
+    "secure": false
+  },
+  "/logout": {
+    "target": "http://localhost:8080",
+    "secure": false
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/scripts/package.json
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/scripts/package.json b/metron-interface/metron-config/scripts/package.json
new file mode 100644
index 0000000..28f9f3b
--- /dev/null
+++ b/metron-interface/metron-config/scripts/package.json
@@ -0,0 +1,21 @@
+{
+  "name": "metron-management-ui-web-server",
+  "version": "0.3.1",
+  "description": "Metron management ui web server",
+  "main": "server.js",
+  "dependencies": {
+    "compression": "1.6.2",
+    "express": "4.15.2",
+    "http-proxy-middleware": "0.17.4",
+    "optimist": "0.6.1",
+    "serve-favicon": "2.4.2",
+    "serve-static": "1.12.1"
+  },
+  "devDependencies": {},
+  "scripts": {
+    "start": "node server.js"
+  },
+  "private": true,
+  "author": "",
+  "license": "MIT"
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/scripts/prepend_license_header.sh
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/scripts/prepend_license_header.sh b/metron-interface/metron-config/scripts/prepend_license_header.sh
new file mode 100755
index 0000000..1957cd6
--- /dev/null
+++ b/metron-interface/metron-config/scripts/prepend_license_header.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+#
+#  Licensed to the Apache Software Foundation (ASF) under one or more
+#  contributor license agreements.  See the NOTICE file distributed with
+#  this work for additional information regarding copyright ownership.
+#  The ASF licenses this file to You under the Apache License, Version 2.0
+#  (the "License"); you may not use this file except in compliance with
+#  the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#
+LICENSE_HEADER="/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an \"AS IS\" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+ "
+for file in ./dist/*.js
+do
+    if !(grep -Fxq "$LICENSE_HEADER" $file)
+    then
+        echo "$LICENSE_HEADER$(cat $file)" > $file
+    fi
+done
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/scripts/server.js
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/scripts/server.js b/metron-interface/metron-config/scripts/server.js
new file mode 100644
index 0000000..7fb1728
--- /dev/null
+++ b/metron-interface/metron-config/scripts/server.js
@@ -0,0 +1,82 @@
+#!/usr/bin/env node
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+var os          = require('os');
+var app         = require('express')();
+var path        = require('path');
+var compression = require('compression')
+var serveStatic = require('serve-static');
+var favicon     = require('serve-favicon');
+var proxy       = require('http-proxy-middleware');
+var argv        = require('optimist')
+                  .demand(['p', 'r'])
+                  .alias('r', 'resturl')
+                  .usage('Usage: server.js -p [port] -r [restUrl]')
+                  .describe('p', 'Port to run metron management ui')
+                  .describe('r', 'Url where metron rest application is available')
+                  .argv;
+
+var port = argv.p;
+var metronUIAddress = '';
+var ifaces = os.networkInterfaces();
+var restUrl =  argv.r || argv.resturl;
+var conf = {
+  "rest": {
+    "target": restUrl,
+    "secure": false
+  }
+};
+
+Object.keys(ifaces).forEach(function (dev) {
+  ifaces[dev].forEach(function (details) {
+    if (details.family === 'IPv4') {
+      metronUIAddress += '\n';
+      metronUIAddress += 'http://' + details.address + ':' + port;
+    }
+  });
+});
+
+function setCustomCacheControl (res, path) {
+  if (serveStatic.mime.lookup(path) === 'text/html') {
+    res.setHeader('Cache-Control', 'public, max-age=10')
+  }
+  res.setHeader("Expires", new Date(Date.now() + 2592000000).toUTCString());
+}
+
+app.use(compression());
+
+app.use('/api/v1', proxy(conf.rest));
+app.use('/logout', proxy(conf.rest));
+
+app.use(favicon(path.join(__dirname, '../management-ui/favicon.ico')));
+
+app.use(serveStatic(path.join(__dirname, '../management-ui'), {
+  maxAge: '1d',
+  setHeaders: setCustomCacheControl
+}));
+
+app.get('*', function(req, res){
+  res.sendFile(path.resolve('../management-ui/index.html'));
+});
+
+app.listen(port, function(){
+  console.log("Metron server listening on " + metronUIAddress);
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/scripts/start_dev.sh
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/scripts/start_dev.sh b/metron-interface/metron-config/scripts/start_dev.sh
new file mode 100755
index 0000000..05ba601
--- /dev/null
+++ b/metron-interface/metron-config/scripts/start_dev.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+#
+#  Licensed to the Apache Software Foundation (ASF) under one or more
+#  contributor license agreements.  See the NOTICE file distributed with
+#  this work for additional information regarding copyright ownership.
+#  The ASF licenses this file to You under the Apache License, Version 2.0
+#  (the "License"); you may not use this file except in compliance with
+#  the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#
+SCRIPTS_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+$SCRIPTS_ROOT/../node_modules/angular-cli/bin/ng serve --proxy-config proxy.conf.json

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/scripts/start_management_ui.sh
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/scripts/start_management_ui.sh b/metron-interface/metron-config/scripts/start_management_ui.sh
new file mode 100755
index 0000000..cfd055b
--- /dev/null
+++ b/metron-interface/metron-config/scripts/start_management_ui.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+#
+#  Licensed to the Apache Software Foundation (ASF) under one or more
+#  contributor license agreements.  See the NOTICE file distributed with
+#  this work for additional information regarding copyright ownership.
+#  The ASF licenses this file to You under the Apache License, Version 2.0
+#  (the "License"); you may not use this file except in compliance with
+#  the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#
+
+METRON_VERSION=${project.version}
+METRON_HOME=/usr/metron/$METRON_VERSION
+
+cd $METRON_HOME/web/expressjs
+npm install
+node $METRON_HOME/web/expressjs/server.js $*

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/_main.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/_main.scss b/metron-interface/metron-config/src/app/_main.scss
new file mode 100644
index 0000000..f889c7a
--- /dev/null
+++ b/metron-interface/metron-config/src/app/_main.scss
@@ -0,0 +1,112 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 100;
+  src: local('Roboto Thin'), local('Roboto-Thin'), url("assets/fonts/Roboto/Roboto-Thin.ttf");
+}
+
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 300;
+  src: local('Roboto Light'), local('Roboto-Light'), url("assets/fonts/Roboto/Roboto-Light.ttf");
+}
+
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Roboto-Regular';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Roboto'), local('Roboto-Regular'), url("assets/fonts/Roboto/Roboto-Regular.ttf");
+}
+
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Roboto-Medium';
+  font-style: normal;
+  font-weight: 500;
+  src: local('Roboto Medium'), local('Roboto-Medium'), url("assets/fonts/Roboto/Roboto-Medium.ttf");
+}
+
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 700;
+  src: local('Roboto Bold'), local('Roboto-Bold'), url("assets/fonts/Roboto/Roboto-Bold.ttf");
+}
+
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 900;
+  src: local('Roboto Black'), local('Roboto-Black'), url("assets/fonts/Roboto/Roboto-Black.ttf");
+}
+
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Roboto';
+  font-style: italic;
+  font-weight: 100;
+  src: local('Roboto Thin Italic'), local('Roboto-ThinItalic'), url("assets/fonts/Roboto/Roboto-ThinItalic.ttf");
+}
+
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Roboto';
+  font-style: italic;
+  font-weight: 300;
+  src: local('Roboto Light Italic'), local('Roboto-LightItalic'), url("assets/fonts/Roboto/Roboto-LightItalic.ttf");
+}
+
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Roboto';
+  font-style: italic;
+  font-weight: 400;
+  src: local('Roboto Italic'), local('Roboto-Italic'), url("assets/fonts/Roboto/Roboto-Italic.ttf");
+}
+
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Roboto-MediumItalic';
+  font-style: italic;
+  font-weight: 500;
+  src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'), url("assets/fonts/Roboto/Roboto-MediumItalic.ttf");
+}
+
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Roboto';
+  font-style: italic;
+  font-weight: 700;
+  src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), url("assets/fonts/Roboto/Roboto-BoldItalic.ttf");
+}
+
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Roboto';
+  font-style: italic;
+  font-weight: 900;
+  src: local('Roboto Black Italic'), local('Roboto-BlackItalic'), url("assets/fonts/Roboto/Roboto-BlackItalic.ttf");
+}
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/_variables.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/_variables.scss b/metron-interface/metron-config/src/app/_variables.scss
new file mode 100644
index 0000000..000a126
--- /dev/null
+++ b/metron-interface/metron-config/src/app/_variables.scss
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+$gray-dark: #232323;
+$gray-light: #333333;
+$gray-border: #4d4d4d;
+$tundora: #404040;
+$text-color-white:  #999999;
+$nav-active-color: #32abe2;
+$nav-active-text-color: #ffffff;
+$table-cell-text-color: #a7a9ac;
+$form-label: #999999;
+$field-background: #2e2e2e;
+$form-field-text-color: #bdbdbd;
+$field-button-color: #27aae1;
+$edit-background: #0b4451;
+$edit-background-border: #17596d;
+$title-subscript-color: #666666;
+$form-field-separator-color: #404040;
+$form-button-border: #006ea0;
+$form-input-background: #2d2d2d;
+$warning-color: #C0661D;
+$black: #000000;
+$login-label: #606060;
+$silver-color: #BDBDBD;
+$dusty-grey: #9b9a9a;
+$ocean-green: #3AA570;
+$edit-child-background: #083b44;
+$edit-child-highlight: #07333a;
+$table-selection: $gray-light;
+$table-selection-lr-border: #2F2F2F;
+$table-selection-tb-border: #3E3E3E;
+
+$dialog-1x-width: 320px;
+$dialog-2x-width: 640px;
+$dialog-4x-width: 1380px;
+$slider-left-padding: 25px;
+
+$button-bar-height: 70px;
+
+@mixin transform($transforms) {
+  -moz-transform: $transforms;
+  -o-transform: $transforms;
+  -ms-transform: $transforms;
+  -webkit-transform: $transforms;
+  transform: $transforms;
+}
+
+@mixin place-holder-text
+{
+  font-family: Roboto;
+  font-size: 12px;
+  color:#bdbdbd !important;
+  font-weight: 300 !important;
+  text-align: center !important;
+  font-style: italic !important;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/app.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/app.component.html b/metron-interface/metron-config/src/app/app.component.html
new file mode 100644
index 0000000..f2f5c46
--- /dev/null
+++ b/metron-interface/metron-config/src/app/app.component.html
@@ -0,0 +1,25 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<div class="container-fluid header" *ngIf="loggedIn">
+  <metron-config-navbar></metron-config-navbar>
+</div>
+<div [ngClass]="{'container-fluid body-fill px-0': loggedIn, 'fill': !loggedIn}">
+  <div [ngClass]="{'card-group ': loggedIn}" class="fill">
+    <div  *ngIf="loggedIn" class="card fill navigation" ><metron-config-vertical-navbar></metron-config-vertical-navbar></div>
+    <div [ngClass]="{'card  fill content px-0 ' : loggedIn , 'fill': !loggedIn}"><router-outlet></router-outlet></div>
+  </div>
+</div>
+<router-outlet name="dialog" ></router-outlet>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/app.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/app.component.scss b/metron-interface/metron-config/src/app/app.component.scss
new file mode 100644
index 0000000..104b980
--- /dev/null
+++ b/metron-interface/metron-config/src/app/app.component.scss
@@ -0,0 +1,34 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "_variables.scss";
+
+.navigation{
+
+  background-color: $gray-light;
+  border-right: solid 1px #4d4d4d;
+  border-top: solid 1px #4d4d4d;
+  border-bottom: solid 1px #4d4d4d;
+  bottom: 0px;
+  padding-left: 0px;
+  width: 240px;
+}
+
+.content{
+  background: $gray-dark;
+  border: none;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/app.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/app.component.spec.ts b/metron-interface/metron-config/src/app/app.component.spec.ts
new file mode 100644
index 0000000..6ae511f
--- /dev/null
+++ b/metron-interface/metron-config/src/app/app.component.spec.ts
@@ -0,0 +1,89 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Inject} from '@angular/core';
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {ResponseOptions, RequestOptions, Response, Http} from '@angular/http';
+import {Router} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import {AppComponent} from './app.component';
+import {AuthenticationService} from './service/authentication.service';
+import {AppModule} from './app.module';
+import {APP_CONFIG, METRON_REST_CONFIG} from './app.config';
+import {IAppConfig} from './app.config.interface';
+
+class MockAuthenticationService extends AuthenticationService {
+
+  constructor(private http2: Http, private router2: Router, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, router2, config2);
+  }
+
+  public checkAuthentication() {
+  }
+
+  public getCurrentUser(options: RequestOptions): Observable<Response> {
+    return Observable.create(observer => {
+      observer.next(new Response(new ResponseOptions({body: 'test'})));
+      observer.complete();
+    });
+  }
+}
+
+class MockRouter {
+  navigateByUrl(url: string) {
+  }
+}
+
+describe('App: Static', () => {
+
+  let comp: AppComponent;
+  let fixture: ComponentFixture<AppComponent>;
+  let authenticationService: AuthenticationService;
+
+  beforeEach(async(() => {
+
+    TestBed.configureTestingModule({
+      imports: [AppModule],
+      providers: [
+        {provide: Http},
+        {provide: AuthenticationService, useClass: MockAuthenticationService},
+        {provide: Router, useClass: MockRouter},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    }).compileComponents()
+      .then(() => {
+        fixture = TestBed.createComponent(AppComponent);
+        comp = fixture.componentInstance;
+        authenticationService = fixture.debugElement.injector.get(AuthenticationService);
+      });
+  }));
+
+  it('should create the app', () => {
+    expect(comp).toBeTruthy();
+  });
+
+  it('should return true/false from loginevent and loggedIn should be set', () => {
+
+    expect(comp.loggedIn).toEqual(false);
+    authenticationService.onLoginEvent.emit(true);
+    expect(comp.loggedIn).toEqual(true);
+    authenticationService.onLoginEvent.emit(false);
+    expect(comp.loggedIn).toEqual(false);
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/app.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/app.component.ts b/metron-interface/metron-config/src/app/app.component.ts
new file mode 100644
index 0000000..9bfe3d2
--- /dev/null
+++ b/metron-interface/metron-config/src/app/app.component.ts
@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Component} from '@angular/core';
+import './rxjs-operators';
+import {AuthenticationService} from './service/authentication.service';
+
+
+@Component({
+  selector: 'metron-config-root',
+  templateUrl: 'app.component.html',
+  styleUrls: ['app.component.scss']
+})
+
+export class AppComponent {
+
+  loggedIn: boolean = false;
+
+  constructor(private authService: AuthenticationService) {
+    this.authService.onLoginEvent.subscribe(result => {
+      this.loggedIn = result;
+    });
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/app.config.interface.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/app.config.interface.ts b/metron-interface/metron-config/src/app/app.config.interface.ts
new file mode 100644
index 0000000..de9d7bd
--- /dev/null
+++ b/metron-interface/metron-config/src/app/app.config.interface.ts
@@ -0,0 +1,3 @@
+export interface IAppConfig {
+  apiEndpoint: string;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/app.config.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/app.config.ts b/metron-interface/metron-config/src/app/app.config.ts
new file mode 100644
index 0000000..af4b360
--- /dev/null
+++ b/metron-interface/metron-config/src/app/app.config.ts
@@ -0,0 +1,8 @@
+import { OpaqueToken } from '@angular/core';
+import {IAppConfig} from './app.config.interface';
+
+export let APP_CONFIG = new OpaqueToken('app.config');
+
+export const METRON_REST_CONFIG: IAppConfig = {
+    apiEndpoint: '/api/v1'
+};

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/app.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/app.module.ts b/metron-interface/metron-config/src/app/app.module.ts
new file mode 100644
index 0000000..25e4acd
--- /dev/null
+++ b/metron-interface/metron-config/src/app/app.module.ts
@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {NgModule} from '@angular/core';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {BrowserModule} from '@angular/platform-browser';
+import {HttpModule} from '@angular/http';
+import {AppComponent} from './app.component';
+import {SensorParserConfigService} from './service/sensor-parser-config.service';
+import {KafkaService} from './service/kafka.service';
+import {GrokValidationService} from './service/grok-validation.service';
+import {StellarService} from './service/stellar.service';
+import {MetronAlerts} from './shared/metron-alerts';
+import {LoginComponent} from './login/login.component';
+import {NavbarComponent} from './navbar/navbar.component';
+import {VerticalNavbarComponent} from './verticalnavbar/verticalnavbar.component';
+import {routing, appRoutingProviders} from './app.routes';
+import {AuthenticationService} from './service/authentication.service';
+import {AuthGuard} from './shared/auth-guard';
+import {LoginGuard} from './shared/login-guard';
+import {SensorParserConfigModule} from './sensors/sensor-parser-config/sensor-parser-config.module';
+import {SensorParserConfigReadonlyModule} from './sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.module';
+import {SensorParserListModule} from './sensors/sensor-parser-list/sensor-parser-list.module';
+import {MetronDialogBox} from './shared/metron-dialog-box';
+import {GeneralSettingsModule} from './general-settings/general-settings.module';
+import {SensorEnrichmentConfigService} from './service/sensor-enrichment-config.service';
+import {GlobalConfigService} from './service/global-config.service';
+import {APP_CONFIG, METRON_REST_CONFIG} from './app.config';
+import {StormService} from './service/storm.service';
+import {SensorParserConfigHistoryService} from './service/sensor-parser-config-history.service';
+import {SensorIndexingConfigService} from './service/sensor-indexing-config.service';
+import {HdfsService} from './service/hdfs.service';
+
+
+@NgModule({
+  imports: [ BrowserModule, routing, FormsModule, ReactiveFormsModule, HttpModule, SensorParserListModule,
+    SensorParserConfigModule, SensorParserConfigReadonlyModule, GeneralSettingsModule ],
+  declarations: [ AppComponent, LoginComponent, NavbarComponent, VerticalNavbarComponent ],
+  providers: [  AuthenticationService, AuthGuard, LoginGuard, SensorParserConfigService,
+    SensorParserConfigHistoryService, SensorEnrichmentConfigService, SensorIndexingConfigService,
+    StormService, KafkaService, GrokValidationService, StellarService, HdfsService,
+    GlobalConfigService, MetronAlerts, MetronDialogBox, appRoutingProviders, { provide: APP_CONFIG, useValue: METRON_REST_CONFIG }],
+  bootstrap:    [ AppComponent ]
+})
+export class AppModule { }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/app.routes.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/app.routes.ts b/metron-interface/metron-config/src/app/app.routes.ts
new file mode 100644
index 0000000..73db2fd
--- /dev/null
+++ b/metron-interface/metron-config/src/app/app.routes.ts
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {ModuleWithProviders} from '@angular/core';
+import {Routes, RouterModule} from '@angular/router';
+import {AuthGuard} from './shared/auth-guard';
+import {LoginGuard} from './shared/login-guard';
+
+export const routes: Routes = [
+  { path: '',  redirectTo: 'sensors', canActivate: [AuthGuard], pathMatch: 'full'},
+  { path: 'login', loadChildren: 'app/login/login.module#LoginModule', canActivate: [LoginGuard] },
+  { path: 'sensors', loadChildren: 'app/sensors/sensor-parser-list/sensor-parser-list.module#SensorParserListModule',
+    canActivate: [AuthGuard] },
+];
+
+export const appRoutingProviders: any[] = [
+];
+
+export const routing: ModuleWithProviders = RouterModule.forRoot(routes);

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/environment.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/environment.ts b/metron-interface/metron-config/src/app/environment.ts
new file mode 100644
index 0000000..1c4a54d
--- /dev/null
+++ b/metron-interface/metron-config/src/app/environment.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// The file for the current environment will overwrite this one during build
+// Different environments can be found in config/environment.{dev|prod}.ts
+// The build system defaults to the dev environment
+
+export const environment = {
+  production: false
+};

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/general-settings/general-settings.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/general-settings/general-settings.component.html b/metron-interface/metron-config/src/app/general-settings/general-settings.component.html
new file mode 100644
index 0000000..2c16f95
--- /dev/null
+++ b/metron-interface/metron-config/src/app/general-settings/general-settings.component.html
@@ -0,0 +1,57 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<div >
+  <div class="container-fluid details-pane-padding">
+    <div class="row mb-1 mx-0">
+      <div class="metron-title"> General Settings </div>
+    </div>
+
+    <div class="row mx-0">
+      <form>
+        <div class="col-md-4 pl-0">
+          <div class="section px-1 py-1">
+            <label class="h6"> <strong> Elasticsearch </strong> </label>
+            <div class="form-group">
+              <label>INDEX DATE FORMAT</label>
+              <input type="text" class="form-control" name="es.date.format" [(ngModel)]="globalConfig['es.date.format']">
+            </div>
+
+          </div>
+
+          <div class="section px-1 py-1 mt-1">
+            <label class="h6"> <strong> Validation </strong> </label>
+
+            <div class="form-group">
+              <label>FIELD</label>
+              <metron-config-ace-editor [(ngModel)]="fieldValidations" [ngModelOptions]="{standalone: true}" [type]="'JSON'" [placeHolder]="'Enter Field Validations'"> </metron-config-ace-editor>
+            </div>
+
+          </div>
+
+        </div>
+      </form>
+    </div>
+
+  </div>
+
+  <div class="container-fluid metron-button-bar-settings details-pane-padding" >
+    <div class="row px-1 py-1">
+      <button type="submit" class="btn btn-primary" (click)="onSave()">SAVE</button>
+      <button class="btn form-enable-disable-button" (click)="onCancel()">CANCEL</button>
+    </div>
+  </div>
+
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/general-settings/general-settings.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/general-settings/general-settings.component.scss b/metron-interface/metron-config/src/app/general-settings/general-settings.component.scss
new file mode 100644
index 0000000..8bf6d89
--- /dev/null
+++ b/metron-interface/metron-config/src/app/general-settings/general-settings.component.scss
@@ -0,0 +1,41 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../_variables.scss";
+
+.section
+{
+  border: solid 1px $gray-border;
+  background-color: $gray-light;
+}
+
+.metron-button-bar-settings
+{
+  bottom: 0;
+  z-index: 10;
+  width: 100%;
+  position: fixed;
+  border-right: solid 1px $gray-border;
+  border-bottom: solid 1px $gray-border;
+  border-top: solid 1px $gray-border;
+  background: $gray-dark;
+}
+
+textarea
+{
+  height: auto;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/general-settings/general-settings.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/general-settings/general-settings.component.spec.ts b/metron-interface/metron-config/src/app/general-settings/general-settings.component.spec.ts
new file mode 100644
index 0000000..51fc709
--- /dev/null
+++ b/metron-interface/metron-config/src/app/general-settings/general-settings.component.spec.ts
@@ -0,0 +1,161 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Inject} from '@angular/core';
+import {async, TestBed, ComponentFixture} from '@angular/core/testing';
+import {Http} from '@angular/http';
+import {GeneralSettingsComponent} from './general-settings.component';
+import {MetronAlerts} from '../shared/metron-alerts';
+import {MetronDialogBox} from '../shared/metron-dialog-box';
+import {GlobalConfigService} from '../service/global-config.service';
+import {GeneralSettingsModule} from './general-settings.module';
+import {Observable} from 'rxjs/Observable';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+
+class MockGlobalConfigService extends GlobalConfigService {
+  _config: any = {};
+  _postSuccess: boolean = true;
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public post(globalConfig: {}): Observable<{}> {
+    if (this._postSuccess) {
+      return Observable.create(observer => {
+        observer.next(globalConfig);
+        observer.complete();
+      });
+    }
+
+    return Observable.throw('Error');
+  }
+
+  public get(): Observable<{}> {
+    return Observable.create(observer => {
+      observer.next(this._config);
+      observer.complete();
+    });
+  }
+}
+
+describe('GeneralSettingsComponent', () => {
+
+  let metronAlerts: MetronAlerts;
+  let metronDialogBox: MetronDialogBox;
+  let component: GeneralSettingsComponent;
+  let globalConfigService: MockGlobalConfigService;
+  let fixture: ComponentFixture<GeneralSettingsComponent>;
+  let config = {
+    'solr.collection': 'metron',
+    'storm.indexingWorkers': 1,
+    'storm.indexingExecutors': 2,
+    'hdfs.boltBatchSize': 5000,
+    'hdfs.boltFieldDelimiter': '|',
+    'hdfs.boltFileRotationSize': 5,
+    'hdfs.boltCompressionCodecClass': 'org.apache.hadoop.io.compress.SnappyCodec',
+    'hdfs.indexOutput': '/tmp/metron/enriched',
+    'kafkaWriter.topic': 'outputTopic',
+    'kafkaWriter.keySerializer': 'org.apache.kafka.common.serialization.StringSerializer',
+    'kafkaWriter.valueSerializer': 'org.apache.kafka.common.serialization.StringSerializer',
+    'kafkaWriter.requestRequiredAcks': 1,
+    'solrWriter.indexName': 'alfaalfa',
+    'solrWriter.shards': 1,
+    'solrWriter.replicationFactor': 1,
+    'solrWriter.batchSize': 50,
+    'fieldValidations': {'field': 'validation'}
+  };
+
+  beforeEach(async(() => {
+
+    TestBed.configureTestingModule({
+      imports: [GeneralSettingsModule],
+      providers: [
+        {provide: Http},
+        MetronAlerts,
+        MetronDialogBox,
+        {provide: GlobalConfigService, useClass: MockGlobalConfigService},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    }).compileComponents()
+      .then(() => {
+        fixture = TestBed.createComponent(GeneralSettingsComponent);
+        component = fixture.componentInstance;
+        globalConfigService = fixture.debugElement.injector.get(GlobalConfigService);
+        metronAlerts = fixture.debugElement.injector.get(MetronAlerts);
+        metronDialogBox = fixture.debugElement.injector.get(MetronDialogBox);
+      });
+
+  }));
+
+  it('can instantiate GeneralSettingsComponent', async(() => {
+    expect(component instanceof GeneralSettingsComponent).toBe(true);
+  }));
+
+  it('should load global config', async(() => {
+    globalConfigService._config = config;
+    component.ngOnInit();
+
+    expect(component.globalConfig).toEqual(globalConfigService._config);
+  }));
+
+  it('should save global config', async(() => {
+    globalConfigService._config = config;
+    component.ngOnInit();
+    fixture.detectChanges();
+    spyOn(metronAlerts, 'showSuccessMessage');
+    spyOn(metronAlerts, 'showErrorMessage');
+
+    component.onSave();
+    expect(metronAlerts.showSuccessMessage).toHaveBeenCalledWith('Saved Global Settings');
+
+    globalConfigService._postSuccess = false;
+
+    component.onSave();
+    expect(metronAlerts.showErrorMessage).toHaveBeenCalledWith('Unable to save Global Settings: Error');
+
+  }));
+
+  it('should handle onCancel', async(() => {
+    let dialogReturnTrue = true;
+    let confirmationMsg = 'Cancelling will revert all the changes made to the form. Do you wish to continue ?';
+
+    spyOn(component, 'ngOnInit');
+    spyOn(metronDialogBox, 'showConfirmationMessage').and.callFake(function() {
+      return Observable.create(observer => {
+        observer.next(dialogReturnTrue);
+        observer.complete();
+      });
+    });
+
+    component.onCancel();
+
+    expect(component.ngOnInit).toHaveBeenCalled();
+    expect(component.ngOnInit['calls'].count()).toEqual(1);
+    expect(metronDialogBox.showConfirmationMessage['calls'].count()).toEqual(1);
+    expect(metronDialogBox.showConfirmationMessage).toHaveBeenCalledWith(confirmationMsg);
+
+    dialogReturnTrue = false;
+    component.onCancel();
+
+    expect(metronDialogBox.showConfirmationMessage['calls'].count()).toEqual(2);
+    expect(component.ngOnInit['calls'].count()).toEqual(1);
+
+  }));
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/general-settings/general-settings.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/general-settings/general-settings.component.ts b/metron-interface/metron-config/src/app/general-settings/general-settings.component.ts
new file mode 100644
index 0000000..11d4cad
--- /dev/null
+++ b/metron-interface/metron-config/src/app/general-settings/general-settings.component.ts
@@ -0,0 +1,72 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, OnInit } from '@angular/core';
+import {GlobalConfigService} from '../service/global-config.service';
+import {MetronAlerts} from '../shared/metron-alerts';
+import {MetronDialogBox} from '../shared/metron-dialog-box';
+
+@Component({
+  selector: 'metron-config-general-settings',
+  templateUrl: './general-settings.component.html',
+  styleUrls: ['./general-settings.component.scss']
+})
+export class GeneralSettingsComponent implements OnInit {
+
+  globalConfig: {} = {
+    'es.date.format': '-'
+  };
+
+  private fieldValidations: string;
+
+  constructor(private globalConfigService: GlobalConfigService, private metronAlerts: MetronAlerts,
+              private metronDialog: MetronDialogBox) {}
+
+  ngOnInit() {
+    this.globalConfigService.get().subscribe((config: {}) => {
+      this.globalConfig = config;
+      if (!this.globalConfig['es.date.format']) {
+        this.globalConfig['es.date.format'] = '-';
+      }
+      if (this.globalConfig['fieldValidations']) {
+        this.fieldValidations = JSON.stringify(this.globalConfig['fieldValidations'], null, '\t');
+      }
+    });
+  }
+
+  onSave() {
+    if (this.fieldValidations && this.fieldValidations.length > 0) {
+      this.globalConfig['fieldValidations'] = JSON.parse(this.fieldValidations);
+    }
+    this.globalConfigService.post(this.globalConfig).subscribe(() => {
+        this.metronAlerts.showSuccessMessage('Saved Global Settings');
+      },
+      error => {
+        this.metronAlerts.showErrorMessage('Unable to save Global Settings: ' + error);
+      });
+  }
+
+  onCancel() {
+    let confirmationMsg = 'Cancelling will revert all the changes made to the form. Do you wish to continue ?';
+    this.metronDialog.showConfirmationMessage(confirmationMsg).subscribe((result: boolean) => {
+      if (result) {
+        this.ngOnInit();
+      }
+    });
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/general-settings/general-settings.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/general-settings/general-settings.module.ts b/metron-interface/metron-config/src/app/general-settings/general-settings.module.ts
new file mode 100644
index 0000000..83537b7
--- /dev/null
+++ b/metron-interface/metron-config/src/app/general-settings/general-settings.module.ts
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {NgModule} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {routing} from './general-settings.routing';
+import {GeneralSettingsComponent} from './general-settings.component';
+import {AceEditorModule} from '../shared/ace-editor/ace-editor.module';
+
+
+@NgModule ({
+  imports: [ CommonModule, routing, FormsModule, ReactiveFormsModule, AceEditorModule ],
+  declarations: [ GeneralSettingsComponent ]
+})
+export class GeneralSettingsModule { }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/general-settings/general-settings.routing.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/general-settings/general-settings.routing.ts b/metron-interface/metron-config/src/app/general-settings/general-settings.routing.ts
new file mode 100644
index 0000000..8aefa6b
--- /dev/null
+++ b/metron-interface/metron-config/src/app/general-settings/general-settings.routing.ts
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { ModuleWithProviders }  from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+import {AuthGuard} from '../shared/auth-guard';
+import {GeneralSettingsComponent} from './general-settings.component';
+
+const routes: Routes = [
+  { path: 'general-settings', component: GeneralSettingsComponent, canActivate: [AuthGuard]}
+];
+
+export const routing: ModuleWithProviders = RouterModule.forChild(routes);

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/general-settings/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/general-settings/index.ts b/metron-interface/metron-config/src/app/general-settings/index.ts
new file mode 100644
index 0000000..6c04b7e
--- /dev/null
+++ b/metron-interface/metron-config/src/app/general-settings/index.ts
@@ -0,0 +1,18 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export * from './general-settings.component';


[06/12] incubator-metron git commit: METRON-623 Management UI [contributed by Raghu Mitra Kandikonda and Ryan Merriman] closes apache/incubator-metron#489

Posted by rm...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.spec.ts
new file mode 100644
index 0000000..5534bea
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.spec.ts
@@ -0,0 +1,742 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {SpyLocation} from '@angular/common/testing';
+import {Http, ResponseOptions, RequestOptions, Response} from '@angular/http';
+
+import {DebugElement, Inject} from '@angular/core';
+import {By} from '@angular/platform-browser';
+import {Router, NavigationStart} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import {SensorParserListComponent} from './sensor-parser-list.component';
+import {SensorParserConfigService} from '../../service/sensor-parser-config.service';
+import {MetronAlerts} from '../../shared/metron-alerts';
+import {TopologyStatus} from '../../model/topology-status';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import {AuthenticationService} from '../../service/authentication.service';
+import {SensorParserListModule} from './sensor-parser-list.module';
+import {MetronDialogBox} from '../../shared/metron-dialog-box';
+import {Sort} from '../../util/enums';
+import 'jquery';
+import {SensorParserConfigHistoryService} from '../../service/sensor-parser-config-history.service';
+import {SensorParserConfigHistory} from '../../model/sensor-parser-config-history';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../../app.config';
+import {StormService} from '../../service/storm.service';
+import {IAppConfig} from '../../app.config.interface';
+
+class MockAuthenticationService extends AuthenticationService {
+
+  constructor(private http2: Http, private router2: Router, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, router2, config2);
+  }
+
+  public checkAuthentication() {
+  }
+
+  public getCurrentUser(options: RequestOptions): Observable<Response> {
+    return Observable.create(observer => {
+      observer.next(new Response(new ResponseOptions({body: 'test'})));
+      observer.complete();
+    });
+  }
+}
+
+class MockSensorParserConfigHistoryService extends SensorParserConfigHistoryService {
+
+  private allSensorParserConfigHistory: SensorParserConfigHistory[];
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public setSensorParserConfigHistoryForTest(allSensorParserConfigHistory: SensorParserConfigHistory[]) {
+    this.allSensorParserConfigHistory = allSensorParserConfigHistory;
+  }
+
+  public getAll(): Observable<SensorParserConfigHistory[]> {
+    return Observable.create(observer => {
+      observer.next(this.allSensorParserConfigHistory);
+      observer.complete();
+    });
+  }
+}
+
+class MockSensorParserConfigService extends SensorParserConfigService {
+  private sensorParserConfigs: SensorParserConfig[];
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public setSensorParserConfigForTest(sensorParserConfigs: SensorParserConfig[]) {
+    this.sensorParserConfigs = sensorParserConfigs;
+  }
+
+  public getAll(): Observable<SensorParserConfig[]> {
+    return Observable.create(observer => {
+      observer.next(this.sensorParserConfigs);
+      observer.complete();
+    });
+  }
+
+  public deleteSensorParserConfigs(sensors: SensorParserConfig[]): Observable<{success: Array<string>, failure: Array<string>}> {
+    let result: {success: Array<string>, failure: Array<string>} = {success: [], failure: []};
+    let observable = Observable.create((observer => {
+      for (let i = 0; i < sensors.length; i++) {
+        result.success.push(sensors[i].sensorTopic);
+      }
+      observer.next(result);
+      observer.complete();
+    }));
+    return observable;
+  }
+}
+
+class MockStormService extends StormService {
+  private topologyStatuses: TopologyStatus[];
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public setTopologyStatusForTest(topologyStatuses: TopologyStatus[]) {
+    this.topologyStatuses = topologyStatuses;
+  }
+
+  public pollGetAll(): Observable<TopologyStatus[]> {
+    return Observable.create(observer => {
+      observer.next(this.topologyStatuses);
+      observer.complete();
+    });
+  }
+
+  public getAll(): Observable<TopologyStatus[]> {
+    return Observable.create(observer => {
+      observer.next(this.topologyStatuses);
+      observer.complete();
+    });
+  }
+}
+
+class MockRouter {
+  events: Observable<Event> = Observable.create(observer => {
+    observer.next(new NavigationStart(1, '/sensors'));
+    observer.complete();
+  });
+
+  navigateByUrl(url: string) {
+  }
+}
+
+class MockMetronDialogBox {
+
+  public showConfirmationMessage(message: string) {
+    return Observable.create(observer => {
+      observer.next(true);
+      observer.complete();
+    });
+  }
+}
+
+describe('Component: SensorParserList', () => {
+
+  let comp: SensorParserListComponent;
+  let fixture: ComponentFixture<SensorParserListComponent>;
+  let authenticationService: MockAuthenticationService;
+  let sensorParserConfigService: MockSensorParserConfigService;
+  let stormService: MockStormService;
+  let sensorParserConfigHistoryService: MockSensorParserConfigHistoryService;
+  let router: Router;
+  let metronAlerts: MetronAlerts;
+  let metronDialog: MetronDialogBox;
+  let dialogEl: DebugElement;
+
+  beforeEach(async(() => {
+
+    TestBed.configureTestingModule({
+      imports: [SensorParserListModule],
+      providers: [
+        {provide: Http},
+        {provide: Location, useClass: SpyLocation},
+        {provide: AuthenticationService, useClass: MockAuthenticationService},
+        {provide: SensorParserConfigService, useClass: MockSensorParserConfigService},
+        {provide: StormService, useClass: MockStormService},
+        {provide: SensorParserConfigHistoryService, useClass: MockSensorParserConfigHistoryService},
+        {provide: Router, useClass: MockRouter},
+        {provide: MetronDialogBox, useClass: MockMetronDialogBox},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG},
+        MetronAlerts
+      ]
+    }).compileComponents()
+      .then(() => {
+        fixture = TestBed.createComponent(SensorParserListComponent);
+        comp = fixture.componentInstance;
+        authenticationService = fixture.debugElement.injector.get(AuthenticationService);
+        sensorParserConfigService = fixture.debugElement.injector.get(SensorParserConfigService);
+        stormService = fixture.debugElement.injector.get(StormService);
+        sensorParserConfigHistoryService = fixture.debugElement.injector.get(SensorParserConfigHistoryService);
+        router = fixture.debugElement.injector.get(Router);
+        metronAlerts = fixture.debugElement.injector.get(MetronAlerts);
+        metronDialog = fixture.debugElement.injector.get(MetronDialogBox);
+        dialogEl = fixture.debugElement.query(By.css('.primary'));
+      });
+
+  }));
+
+  it('should create an instance', async(() => {
+
+    let component: SensorParserListComponent = fixture.componentInstance;
+    expect(component).toBeDefined();
+    fixture.destroy();
+
+  }));
+
+  it('getSensors should call getStatus and poll status and all variables should be initialised', async(() => {
+    let sensorParserConfigHistory1 = new SensorParserConfigHistory();
+    let sensorParserConfigHistory2 = new SensorParserConfigHistory();
+    let sensorParserConfig1 = new SensorParserConfig();
+    let sensorParserConfig2 = new SensorParserConfig();
+
+    sensorParserConfig1.sensorTopic = 'squid';
+    sensorParserConfig2.sensorTopic = 'bro';
+    sensorParserConfigHistory1.config = sensorParserConfig1;
+    sensorParserConfigHistory2.config = sensorParserConfig2;
+
+    let sensorParserStatus1 = new TopologyStatus();
+    let sensorParserStatus2 = new TopologyStatus();
+    sensorParserStatus1.name = 'squid';
+    sensorParserStatus1.status = 'KILLED';
+    sensorParserStatus2.name = 'bro';
+    sensorParserStatus2.status = 'KILLED';
+
+    sensorParserConfigHistoryService.setSensorParserConfigHistoryForTest([sensorParserConfigHistory1, sensorParserConfigHistory2]);
+    stormService.setTopologyStatusForTest([sensorParserStatus1, sensorParserStatus2]);
+
+    let component: SensorParserListComponent = fixture.componentInstance;
+
+    component.enableAutoRefresh = false;
+
+    component.ngOnInit();
+
+    expect(component.sensors[0]).toEqual(sensorParserConfigHistory1);
+    expect(component.sensors[1]).toEqual(sensorParserConfigHistory2);
+    expect(component.sensorsStatus[0]).toEqual(Object.assign(new TopologyStatus(), sensorParserStatus1));
+    expect(component.sensorsStatus[1]).toEqual(Object.assign(new TopologyStatus(), sensorParserStatus2));
+    expect(component.selectedSensors).toEqual([]);
+    expect(component.count).toEqual(2);
+
+    fixture.destroy();
+
+  }));
+
+  it('getParserType should return the Type of Parser', async(() => {
+
+    let component: SensorParserListComponent = fixture.componentInstance;
+
+    let sensorParserConfig1 = new SensorParserConfig();
+    sensorParserConfig1.sensorTopic = 'squid';
+    sensorParserConfig1.parserClassName = 'org.apache.metron.parsers.GrokParser';
+    let sensorParserConfig2 = new SensorParserConfig();
+    sensorParserConfig2.sensorTopic = 'bro';
+    sensorParserConfig2.parserClassName = 'org.apache.metron.parsers.bro.BasicBroParser';
+
+    expect(component.getParserType(sensorParserConfig1)).toEqual('Grok');
+    expect(component.getParserType(sensorParserConfig2)).toEqual('Bro');
+
+    fixture.destroy();
+
+  }));
+
+  it('navigateToSensorEdit should set selected sensor and change url', async(() => {
+    let event = new Event('mouse');
+    event.stopPropagation = jasmine.createSpy('stopPropagation');
+
+    spyOn(router, 'navigateByUrl');
+
+    let component: SensorParserListComponent = fixture.componentInstance;
+
+    let sensorParserConfig1 = new SensorParserConfig();
+    sensorParserConfig1.sensorTopic = 'squid';
+    component.navigateToSensorEdit(sensorParserConfig1, event);
+
+    let expectStr = router.navigateByUrl['calls'].argsFor(0);
+    expect(expectStr).toEqual(['/sensors(dialog:sensors-config/squid)']);
+
+    expect(event.stopPropagation).toHaveBeenCalled();
+
+    fixture.destroy();
+  }));
+
+  it('addAddSensor should change the URL', async(() => {
+    spyOn(router, 'navigateByUrl');
+
+    let component: SensorParserListComponent = fixture.componentInstance;
+
+    component.addAddSensor();
+
+    let expectStr = router.navigateByUrl['calls'].argsFor(0);
+    expect(expectStr).toEqual(['/sensors(dialog:sensors-config/new)']);
+
+    fixture.destroy();
+  }));
+
+
+  it('onRowSelected should add add/remove items from the selected stack', async(() => {
+
+    let component: SensorParserListComponent = fixture.componentInstance;
+    let event = {target: {checked: true}};
+
+    let sensorParserConfigHistory = new SensorParserConfigHistory();
+    let sensorParserConfig = new SensorParserConfig();
+
+    sensorParserConfig.sensorTopic = 'squid';
+    sensorParserConfigHistory.config = sensorParserConfig;
+
+    component.onRowSelected(sensorParserConfigHistory, event);
+
+    expect(component.selectedSensors[0]).toEqual(sensorParserConfigHistory);
+
+    event = {target: {checked: false}};
+
+    component.onRowSelected(sensorParserConfigHistory, event);
+    expect(component.selectedSensors).toEqual([]);
+
+    fixture.destroy();
+  }));
+
+  it('onSelectDeselectAll should populate items into selected stack', async(() => {
+
+    let component: SensorParserListComponent = fixture.componentInstance;
+
+    let sensorParserConfig1 = new SensorParserConfig();
+    let sensorParserConfig2 = new SensorParserConfig();
+    let sensorParserConfigHistory1 = new SensorParserConfigHistory();
+    let sensorParserConfigHistory2 = new SensorParserConfigHistory();
+
+    sensorParserConfig1.sensorTopic = 'squid';
+    sensorParserConfigHistory1.config = sensorParserConfig1;
+    sensorParserConfig2.sensorTopic = 'bro';
+    sensorParserConfigHistory2.config = sensorParserConfig2;
+
+    component.sensors.push(sensorParserConfigHistory1);
+    component.sensors.push(sensorParserConfigHistory2);
+
+    let event = {target: {checked: true}};
+
+    component.onSelectDeselectAll(event);
+
+    expect(component.selectedSensors).toEqual([sensorParserConfigHistory1, sensorParserConfigHistory2]);
+
+    event = {target: {checked: false}};
+
+    component.onSelectDeselectAll(event);
+
+    expect(component.selectedSensors).toEqual([]);
+
+    fixture.destroy();
+  }));
+
+  it('onSensorRowSelect should change the url and updated the selected items stack', async(() => {
+
+    let sensorParserConfig1 = new SensorParserConfig();
+    sensorParserConfig1.sensorTopic = 'squid';
+
+    let component: SensorParserListComponent = fixture.componentInstance;
+    let event = {target: {type: 'div', parentElement: {firstChild: {type: 'div'}}}};
+
+    sensorParserConfigService.setSeletedSensor(sensorParserConfig1);
+    component.onSensorRowSelect(sensorParserConfig1, event);
+
+    expect(sensorParserConfigService.getSelectedSensor()).toEqual(null);
+
+    component.onSensorRowSelect(sensorParserConfig1, event);
+
+    expect(sensorParserConfigService.getSelectedSensor()).toEqual(sensorParserConfig1);
+
+    sensorParserConfigService.setSeletedSensor(sensorParserConfig1);
+    event = {target: {type: 'checkbox', parentElement: {firstChild: {type: 'div'}}}};
+
+    component.onSensorRowSelect(sensorParserConfig1, event);
+
+    expect(sensorParserConfigService.getSelectedSensor()).toEqual(sensorParserConfig1);
+
+    fixture.destroy();
+  }));
+
+  it('onSensorRowSelect should change the url and updated the selected items stack', async(() => {
+
+    let component: SensorParserListComponent =  fixture.componentInstance;
+
+    let sensorParserConfigHistory = new SensorParserConfigHistory();
+    let sensorParserConfig = new SensorParserConfig();
+
+    sensorParserConfig.sensorTopic = 'squid';
+    sensorParserConfigHistory.config = sensorParserConfig;
+
+    component.toggleStartStopInProgress(sensorParserConfigHistory);
+    expect(sensorParserConfig['startStopInProgress']).toEqual(true);
+
+    component.toggleStartStopInProgress(sensorParserConfigHistory);
+    expect(sensorParserConfig['startStopInProgress']).toEqual(false);
+  }));
+
+  it('onDeleteSensor should call the appropriate url',  async(() => {
+
+    spyOn(metronAlerts, 'showSuccessMessage');
+    spyOn(metronDialog, 'showConfirmationMessage').and.callThrough();
+
+    let event = new Event('mouse');
+    event.stopPropagation = jasmine.createSpy('stopPropagation');
+
+    let component: SensorParserListComponent =  fixture.componentInstance;
+    let sensorParserConfigHistory1 = new SensorParserConfigHistory();
+    let sensorParserConfigHistory2 = new SensorParserConfigHistory();
+    let sensorParserConfig1 = new SensorParserConfig();
+    let sensorParserConfig2 = new SensorParserConfig();
+
+    sensorParserConfig1.sensorTopic = 'squid';
+    sensorParserConfig2.sensorTopic = 'bro';
+    sensorParserConfigHistory1.config = sensorParserConfig1;
+    sensorParserConfigHistory2.config = sensorParserConfig2;
+
+    component.selectedSensors.push(sensorParserConfigHistory1);
+    component.selectedSensors.push(sensorParserConfigHistory2);
+
+    component.onDeleteSensor();
+
+    expect(metronAlerts.showSuccessMessage).toHaveBeenCalled();
+
+    component.deleteSensor(event, [sensorParserConfigHistory1.config]);
+
+    expect(metronDialog.showConfirmationMessage).toHaveBeenCalled();
+    expect(metronDialog.showConfirmationMessage['calls'].count()).toEqual(2);
+    expect(metronDialog.showConfirmationMessage['calls'].count()).toEqual(2);
+    expect(metronDialog.showConfirmationMessage['calls'].all()[0].args).toEqual(['Are you sure you want to delete sensor(s) squid, bro ?']);
+    expect(metronDialog.showConfirmationMessage['calls'].all()[1].args).toEqual(['Are you sure you want to delete sensor(s) squid ?']);
+
+    expect(event.stopPropagation).toHaveBeenCalled();
+
+    fixture.destroy();
+
+  }));
+
+  it('onStopSensor should call the appropriate url', async(() => {
+    let event = new Event('mouse');
+    event.stopPropagation = jasmine.createSpy('stopPropagation');
+
+    let sensorParserConfigHistory1 = new SensorParserConfigHistory();
+    let sensorParserConfig1 = new SensorParserConfig();
+
+    sensorParserConfig1.sensorTopic = 'squid';
+    sensorParserConfigHistory1.config = sensorParserConfig1;
+
+    let observableToReturn = Observable.create(observer => {
+      observer.next({status: 'success', message: 'Some Message'});
+      observer.complete();
+    });
+
+    spyOn(metronAlerts, 'showSuccessMessage');
+    spyOn(stormService, 'stopParser').and.returnValue(observableToReturn);
+
+    let component: SensorParserListComponent = fixture.componentInstance;
+
+    component.onStopSensor(sensorParserConfigHistory1, event);
+
+    expect(stormService.stopParser).toHaveBeenCalled();
+    expect(event.stopPropagation).toHaveBeenCalled();
+    expect(metronAlerts.showSuccessMessage).toHaveBeenCalled();
+
+    fixture.destroy();
+  }));
+
+  it('onStartSensor should call the appropriate url', async(() => {
+    let event = new Event('mouse');
+    event.stopPropagation = jasmine.createSpy('stopPropagation');
+
+    let sensorParserConfigHistory1 = new SensorParserConfigHistory();
+    let sensorParserConfig1 = new SensorParserConfig();
+
+    sensorParserConfig1.sensorTopic = 'squid';
+    sensorParserConfigHistory1.config = sensorParserConfig1;
+
+    let observableToReturn = Observable.create(observer => {
+      observer.next({status: 'success', message: 'Some Message'});
+      observer.complete();
+    });
+
+    spyOn(metronAlerts, 'showSuccessMessage');
+    spyOn(stormService, 'startParser').and.returnValue(observableToReturn);
+
+    let component: SensorParserListComponent = fixture.componentInstance;
+
+    component.onStartSensor(sensorParserConfigHistory1, event);
+
+    expect(stormService.startParser).toHaveBeenCalled();
+    expect(event.stopPropagation).toHaveBeenCalled();
+    expect(metronAlerts.showSuccessMessage).toHaveBeenCalled();
+
+    fixture.destroy();
+  }));
+
+  it('onEnableSensor should call the appropriate url', async(() => {
+    let event = new Event('mouse');
+    event.stopPropagation = jasmine.createSpy('stopPropagation');
+
+    let sensorParserConfigHistory1 = new SensorParserConfigHistory();
+    let sensorParserConfig1 = new SensorParserConfig();
+
+    sensorParserConfig1.sensorTopic = 'squid';
+    sensorParserConfigHistory1.config = sensorParserConfig1;
+
+    let observableToReturn = Observable.create(observer => {
+      observer.next({status: 'success', message: 'Some Message'});
+      observer.complete();
+    });
+
+    spyOn(metronAlerts, 'showSuccessMessage');
+    spyOn(stormService, 'activateParser').and.returnValue(observableToReturn);
+
+    let component: SensorParserListComponent = fixture.componentInstance;
+
+    component.onEnableSensor(sensorParserConfigHistory1, event);
+
+    expect(stormService.activateParser).toHaveBeenCalled();
+    expect(event.stopPropagation).toHaveBeenCalled();
+    expect(metronAlerts.showSuccessMessage).toHaveBeenCalled();
+
+    fixture.destroy();
+  }));
+
+  it('onDisableSensor should call the appropriate url', async(() => {
+    let event = new Event('mouse');
+    event.stopPropagation = jasmine.createSpy('stopPropagation');
+
+    let sensorParserConfigHistory1 = new SensorParserConfigHistory();
+    let sensorParserConfig1 = new SensorParserConfig();
+
+    sensorParserConfig1.sensorTopic = 'squid';
+    sensorParserConfigHistory1.config = sensorParserConfig1;
+
+    let observableToReturn = Observable.create(observer => {
+      observer.next({status: 'success', message: 'Some Message'});
+      observer.complete();
+    });
+
+    spyOn(metronAlerts, 'showSuccessMessage');
+    spyOn(stormService, 'deactivateParser').and.returnValue(observableToReturn);
+
+    let component: SensorParserListComponent = fixture.componentInstance;
+
+    component.onDisableSensor(sensorParserConfigHistory1, event);
+
+    expect(stormService.deactivateParser).toHaveBeenCalled();
+    expect(event.stopPropagation).toHaveBeenCalled();
+    expect(metronAlerts.showSuccessMessage).toHaveBeenCalled();
+
+    fixture.destroy();
+  }));
+
+  it('onStartSensors/onStopSensors should call start on all sensors that have status != ' +
+    'Running and status != Running respectively', async(() => {
+    let component: SensorParserListComponent = fixture.componentInstance;
+
+    spyOn(component, 'onStartSensor');
+    spyOn(component, 'onStopSensor');
+    spyOn(component, 'onDisableSensor');
+    spyOn(component, 'onEnableSensor');
+
+    let sensorParserConfigHistory1 = new SensorParserConfigHistory();
+    let sensorParserConfigHistory2 = new SensorParserConfigHistory();
+    let sensorParserConfigHistory3 = new SensorParserConfigHistory();
+    let sensorParserConfigHistory4 = new SensorParserConfigHistory();
+    let sensorParserConfigHistory5 = new SensorParserConfigHistory();
+    let sensorParserConfigHistory6 = new SensorParserConfigHistory();
+    let sensorParserConfigHistory7 = new SensorParserConfigHistory();
+
+    let sensorParserConfig1 = new SensorParserConfig();
+    let sensorParserConfig2 = new SensorParserConfig();
+    let sensorParserConfig3 = new SensorParserConfig();
+    let sensorParserConfig4 = new SensorParserConfig();
+    let sensorParserConfig5 = new SensorParserConfig();
+    let sensorParserConfig6 = new SensorParserConfig();
+    let sensorParserConfig7 = new SensorParserConfig();
+
+    sensorParserConfig1.sensorTopic = 'squid';
+    sensorParserConfigHistory1['status'] = 'Running';
+    sensorParserConfigHistory1.config = sensorParserConfig1;
+
+    sensorParserConfig2.sensorTopic = 'bro';
+    sensorParserConfigHistory2['status'] = 'Stopped';
+    sensorParserConfigHistory2.config = sensorParserConfig2;
+
+    sensorParserConfig3.sensorTopic = 'test';
+    sensorParserConfigHistory3['status'] = 'Stopped';
+    sensorParserConfigHistory3.config = sensorParserConfig3;
+
+    sensorParserConfig4.sensorTopic = 'test1';
+    sensorParserConfigHistory4['status'] = 'Stopped';
+    sensorParserConfigHistory4.config = sensorParserConfig4;
+
+    sensorParserConfig5.sensorTopic = 'test2';
+    sensorParserConfigHistory5['status'] = 'Running';
+    sensorParserConfigHistory5.config = sensorParserConfig5;
+
+    sensorParserConfig6.sensorTopic = 'test2';
+    sensorParserConfigHistory6['status'] = 'Disabled';
+    sensorParserConfigHistory6.config = sensorParserConfig6;
+
+    sensorParserConfig7.sensorTopic = 'test3';
+    sensorParserConfigHistory7['status'] = 'Disabled';
+    sensorParserConfigHistory7.config = sensorParserConfig7;
+
+    component.selectedSensors = [sensorParserConfigHistory1, sensorParserConfigHistory2, sensorParserConfigHistory3,
+      sensorParserConfigHistory4, sensorParserConfigHistory5, sensorParserConfigHistory6, sensorParserConfigHistory7];
+
+    component.onStartSensors();
+    expect(component.onStartSensor['calls'].count()).toEqual(3);
+
+    component.onStopSensors();
+    expect(component.onStopSensor['calls'].count()).toEqual(4);
+
+    component.onDisableSensors();
+    expect(component.onDisableSensor['calls'].count()).toEqual(2);
+
+    component.onEnableSensors();
+    expect(component.onEnableSensor['calls'].count()).toEqual(2);
+
+    fixture.destroy();
+  }));
+
+  it('sort', async(() => {
+    let component: SensorParserListComponent = fixture.componentInstance;
+
+    component.sensors = [
+      Object.assign(new SensorParserConfigHistory(), {
+        'config': {
+          'parserClassName': 'org.apache.metron.parsers.GrokParser',
+          'sensorTopic': 'abc',
+        },
+        'createdBy': 'raghu',
+        'modifiedBy': 'abc',
+        'createdDate': '2016-11-25 09:09:12',
+        'modifiedByDate': '2016-11-25 09:09:12'
+      }),
+      Object.assign(new SensorParserConfigHistory(), {
+        'config': {
+          'parserClassName': 'org.apache.metron.parsers.Bro',
+          'sensorTopic': 'plm',
+        },
+        'createdBy': 'raghu',
+        'modifiedBy': 'plm',
+        'createdDate': '2016-11-25 12:39:21',
+        'modifiedByDate': '2016-11-25 12:39:21'
+      }),
+      Object.assign(new SensorParserConfigHistory(), {
+        'config': {
+          'parserClassName': 'org.apache.metron.parsers.GrokParser',
+          'sensorTopic': 'xyz',
+        },
+        'createdBy': 'raghu',
+        'modifiedBy': 'xyz',
+        'createdDate': '2016-11-25 12:44:03',
+        'modifiedByDate': '2016-11-25 12:44:03'
+      })
+    ];
+
+    component.onSort({sortBy: 'sensorTopic', sortOrder: Sort.ASC});
+    expect(component.sensors[0].config.sensorTopic).toEqual('abc');
+    expect(component.sensors[1].config.sensorTopic).toEqual('plm');
+    expect(component.sensors[2].config.sensorTopic).toEqual('xyz');
+
+    component.onSort({sortBy: 'sensorTopic', sortOrder: Sort.DSC});
+    expect(component.sensors[0].config.sensorTopic).toEqual('xyz');
+    expect(component.sensors[1].config.sensorTopic).toEqual('plm');
+    expect(component.sensors[2].config.sensorTopic).toEqual('abc');
+
+    component.onSort({sortBy: 'parserClassName', sortOrder: Sort.ASC});
+    expect(component.sensors[0].config.parserClassName).toEqual('org.apache.metron.parsers.Bro');
+    expect(component.sensors[1].config.parserClassName).toEqual('org.apache.metron.parsers.GrokParser');
+    expect(component.sensors[2].config.parserClassName).toEqual('org.apache.metron.parsers.GrokParser');
+
+    component.onSort({sortBy: 'parserClassName', sortOrder: Sort.DSC});
+    expect(component.sensors[0].config.parserClassName).toEqual('org.apache.metron.parsers.GrokParser');
+    expect(component.sensors[1].config.parserClassName).toEqual('org.apache.metron.parsers.GrokParser');
+    expect(component.sensors[2].config.parserClassName).toEqual('org.apache.metron.parsers.Bro');
+
+    component.onSort({sortBy: 'modifiedBy', sortOrder: Sort.ASC});
+    expect(component.sensors[0].modifiedBy).toEqual('abc');
+    expect(component.sensors[1].modifiedBy).toEqual('plm');
+    expect(component.sensors[2].modifiedBy).toEqual('xyz');
+
+    component.onSort({sortBy: 'modifiedBy', sortOrder: Sort.DSC});
+    expect(component.sensors[0].modifiedBy).toEqual('xyz');
+    expect(component.sensors[1].modifiedBy).toEqual('plm');
+    expect(component.sensors[2].modifiedBy).toEqual('abc');
+
+  }));
+
+  it('sort', async(() => {
+    let component: SensorParserListComponent = fixture.componentInstance;
+
+    component.sensors = [
+      Object.assign(new SensorParserConfigHistory(), {
+        'config': {
+          'parserClassName': 'org.apache.metron.parsers.GrokParser',
+          'sensorTopic': 'abc',
+        },
+        'createdBy': 'raghu',
+        'modifiedBy': 'abc',
+        'createdDate': '2016-11-25 09:09:12',
+        'modifiedByDate': '2016-11-25 09:09:12'
+    })];
+
+    component.sensorsStatus = [
+        Object.assign(new TopologyStatus(), {
+          'name': 'abc',
+          'status': 'ACTIVE',
+          'latency': '10',
+          'throughput': '23'
+        })
+    ];
+
+    component.updateSensorStatus();
+    expect(component.sensors[0]['status']).toEqual('Running');
+    expect(component.sensors[0]['latency']).toEqual('10s');
+    expect(component.sensors[0]['throughput']).toEqual('23kb/s');
+
+    component.sensorsStatus[0].status = 'KILLED';
+    component.updateSensorStatus();
+    expect(component.sensors[0]['status']).toEqual('Stopped');
+    expect(component.sensors[0]['latency']).toEqual('-');
+    expect(component.sensors[0]['throughput']).toEqual('-');
+
+    component.sensorsStatus[0].status = 'INACTIVE';
+    component.updateSensorStatus();
+    expect(component.sensors[0]['status']).toEqual('Disabled');
+    expect(component.sensors[0]['latency']).toEqual('-');
+    expect(component.sensors[0]['throughput']).toEqual('-');
+
+    component.sensorsStatus = [];
+    component.updateSensorStatus();
+    expect(component.sensors[0]['status']).toEqual('Stopped');
+    expect(component.sensors[0]['latency']).toEqual('-');
+    expect(component.sensors[0]['throughput']).toEqual('-');
+
+  }));
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.ts
new file mode 100644
index 0000000..ccc8aca
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.ts
@@ -0,0 +1,368 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Router, NavigationStart} from '@angular/router';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import {SensorParserConfigService} from '../../service/sensor-parser-config.service';
+import {MetronAlerts} from '../../shared/metron-alerts';
+import {MetronDialogBox} from '../../shared/metron-dialog-box';
+import {Sort} from '../../util/enums';
+import {SortEvent} from '../../shared/metron-table/metron-table.directive';
+import {StormService} from '../../service/storm.service';
+import {TopologyStatus} from '../../model/topology-status';
+import {SensorParserConfigHistory} from '../../model/sensor-parser-config-history';
+import {SensorParserConfigHistoryService} from '../../service/sensor-parser-config-history.service';
+
+@Component({
+  selector: 'metron-config-sensor-parser-list',
+  templateUrl: 'sensor-parser-list.component.html',
+  styleUrls: ['sensor-parser-list.component.scss']
+})
+export class SensorParserListComponent implements OnInit {
+
+  componentName: string = 'Sensors';
+  @ViewChild('table') table;
+
+  count: number = 0;
+  sensors: SensorParserConfigHistory[] = [];
+  sensorsStatus: TopologyStatus[] = [];
+  selectedSensors: SensorParserConfigHistory[] = [];
+  enableAutoRefresh: boolean = true;
+
+  constructor(private sensorParserConfigService: SensorParserConfigService,
+              private sensorParserConfigHistoryService: SensorParserConfigHistoryService,
+              private stormService: StormService,
+              private router: Router,
+              private metronAlerts:  MetronAlerts,
+              private metronDialogBox: MetronDialogBox) {
+    router.events.subscribe(event => {
+      if (event instanceof NavigationStart && event.url === '/sensors') {
+        this.onNavigationStart();
+      }
+    });
+  }
+
+  getSensors(justOnce: boolean) {
+    this.sensorParserConfigHistoryService.getAll().subscribe(
+      (results: SensorParserConfigHistory[]) => {
+        this.sensors = results;
+        this.selectedSensors = [];
+        this.count = this.sensors.length;
+        if (!justOnce) {
+          this.getStatus();
+          this.pollStatus();
+        } else {
+          this.getStatus();
+        }
+
+      }
+    );
+  }
+
+  onSort($event: SortEvent) {
+    switch ($event.sortBy) {
+      case 'sensorTopic':
+        this.sensors.sort((obj1: SensorParserConfigHistory, obj2: SensorParserConfigHistory) => {
+          if ($event.sortOrder === Sort.ASC) {
+            return obj1.config[$event.sortBy].localeCompare(obj2.config[$event.sortBy]);
+          }
+          return obj2.config[$event.sortBy].localeCompare(obj1.config[$event.sortBy]);
+        });
+        break;
+      case 'parserClassName':
+        this.sensors.sort((obj1: SensorParserConfigHistory, obj2: SensorParserConfigHistory) => {
+          if ($event.sortOrder === Sort.ASC) {
+            return this.getParserType(obj1.config).localeCompare(this.getParserType(obj2.config));
+          }
+          return this.getParserType(obj2.config).localeCompare(this.getParserType(obj1.config));
+        });
+        break;
+      case 'status':
+      case 'modifiedBy':
+      case 'modifiedByDate':
+      case 'latency':
+      case 'throughput':
+        this.sensors.sort((obj1: SensorParserConfigHistory, obj2: SensorParserConfigHistory) => {
+          if (!obj1[$event.sortBy] || !obj1[$event.sortBy]) {
+            return 0;
+          }
+          if ($event.sortOrder === Sort.ASC) {
+            return obj1[$event.sortBy].localeCompare(obj2[$event.sortBy]);
+          }
+
+          return obj2[$event.sortBy].localeCompare(obj1[$event.sortBy]);
+        });
+        break;
+    }
+  }
+
+  private pollStatus() {
+    this.stormService.pollGetAll().subscribe(
+        (results: TopologyStatus[]) => {
+          this.sensorsStatus = results;
+          this.updateSensorStatus();
+        },
+        error => {
+          this.updateSensorStatus();
+        }
+    );
+  }
+
+  private getStatus() {
+    this.stormService.getAll().subscribe(
+      (results: TopologyStatus[]) => {
+        this.sensorsStatus = results;
+        this.updateSensorStatus();
+      },
+      error => {
+        this.updateSensorStatus();
+      }
+    );
+  }
+
+  updateSensorStatus() {
+      for (let sensor of this.sensors) {
+
+        let status: TopologyStatus = this.sensorsStatus.find(status => {
+          return status.name === sensor.config.sensorTopic;
+        });
+
+        if (status) {
+          if (status.status === 'ACTIVE') {
+            sensor['status'] = 'Running';
+          }
+          if (status.status === 'KILLED') {
+            sensor['status'] = 'Stopped';
+          }
+          if (status.status === 'INACTIVE') {
+            sensor['status'] = 'Disabled';
+          }
+        } else {
+          sensor['status'] = 'Stopped';
+        }
+
+        sensor['latency'] = status && status.status === 'ACTIVE' ? (status.latency + 's') : '-';
+        sensor['throughput'] = status && status.status === 'ACTIVE' ? (Math.round(status.throughput * 100) / 100) + 'kb/s' : '-';
+      }
+  }
+
+  getParserType(sensor: SensorParserConfig): string {
+    let items = sensor.parserClassName.split('.');
+    return items[items.length - 1].replace('Basic', '').replace('Parser', '');
+  }
+
+  ngOnInit() {
+    this.getSensors(false);
+    this.sensorParserConfigService.dataChanged$.subscribe(
+      data => {
+        this.getSensors(true);
+      }
+    );
+  }
+
+  addAddSensor() {
+    this.router.navigateByUrl('/sensors(dialog:sensors-config/new)');
+  }
+
+  navigateToSensorEdit(selectedSensor: SensorParserConfig, event) {
+    this.sensorParserConfigService.setSeletedSensor(selectedSensor);
+    this.router.navigateByUrl('/sensors(dialog:sensors-config/' + selectedSensor.sensorTopic + ')');
+    event.stopPropagation();
+  }
+
+  onRowSelected(sensor: SensorParserConfigHistory, $event) {
+    if ($event.target.checked) {
+      this.selectedSensors.push(sensor);
+    } else {
+      this.selectedSensors.splice(this.selectedSensors.indexOf(sensor), 1);
+    }
+  }
+
+  onSelectDeselectAll($event) {
+    let checkBoxes = this.table.nativeElement.querySelectorAll('tr td:last-child input[type="checkbox"]');
+
+    for (let ele of checkBoxes) {
+      ele.checked = $event.target.checked;
+    }
+
+    if ($event.target.checked) {
+      this.selectedSensors = this.sensors.slice(0).map((sensorParserInfo: SensorParserConfigHistory) => sensorParserInfo);
+    } else {
+      this.selectedSensors = [];
+    }
+  }
+
+  onSensorRowSelect(sensor: SensorParserConfig, $event) {
+    if ($event.target.type !== 'checkbox' && $event.target.parentElement.firstChild.type !== 'checkbox') {
+
+      if (this.sensorParserConfigService.getSelectedSensor() === sensor) {
+        this.sensorParserConfigService.setSeletedSensor(null);
+        this.router.navigateByUrl('/sensors');
+        return;
+      }
+
+      this.sensorParserConfigService.setSeletedSensor(sensor);
+      this.router.navigateByUrl('/sensors(dialog:sensors-readonly/' + sensor.sensorTopic + ')');
+    }
+  }
+
+  deleteSensor($event, selectedSensorsToDelete: SensorParserConfig[]) {
+    if ($event) {
+      $event.stopPropagation();
+    }
+
+    let sensorNames = selectedSensorsToDelete.map(sensor => { return sensor.sensorTopic; });
+    let confirmationsMsg = 'Are you sure you want to delete sensor(s) ' + sensorNames.join(', ') + ' ?';
+
+    this.metronDialogBox.showConfirmationMessage(confirmationsMsg).subscribe(result => {
+      if (result) {
+        this.sensorParserConfigService.deleteSensorParserConfigs(selectedSensorsToDelete)
+            .subscribe((deleteResult: {success: Array<string>, failure: Array<string>}) => {
+          if (deleteResult.success.length > 0) {
+            this.metronAlerts.showSuccessMessage('Deleted sensors: ' + deleteResult.success.join(', '));
+          }
+
+          if (deleteResult.failure.length > 0) {
+            this.metronAlerts.showErrorMessage('Unable to deleted sensors: ' + deleteResult.failure.join(', '));
+          }
+        });
+      }
+    });
+  }
+
+  onDeleteSensor() {
+    let selectedSensorsToDelete = this.selectedSensors.map(info => {
+      return info.config;
+    });
+    this.deleteSensor(null, selectedSensorsToDelete);
+  }
+
+  onStopSensors() {
+    for (let sensor of this.selectedSensors) {
+      if (sensor['status'] === 'Running' || sensor['status'] === 'Disabled') {
+        this.onStopSensor(sensor, null);
+      }
+    }
+  }
+
+  onStopSensor(sensor: SensorParserConfigHistory, event) {
+    this.toggleStartStopInProgress(sensor);
+
+    this.stormService.stopParser(sensor.config.sensorTopic).subscribe(result => {
+        this.metronAlerts.showSuccessMessage('Stopped sensor ' + sensor.config.sensorTopic);
+        this.toggleStartStopInProgress(sensor);
+      },
+      error => {
+        this.metronAlerts.showErrorMessage('Unable to stop sensor ' + sensor.config.sensorTopic);
+        this.toggleStartStopInProgress(sensor);
+      });
+
+    if (event !== null) {
+      event.stopPropagation();
+    }
+  }
+
+  onStartSensors() {
+    for (let sensor of this.selectedSensors) {
+      if (sensor['status'] === 'Stopped') {
+        this.onStartSensor(sensor, null);
+      }
+    }
+  }
+
+  onStartSensor(sensor: SensorParserConfigHistory, event) {
+    this.toggleStartStopInProgress(sensor);
+
+    this.stormService.startParser(sensor.config.sensorTopic).subscribe(result => {
+        if (result['status'] === 'ERROR') {
+          this.metronAlerts.showErrorMessage('Unable to start sensor ' + sensor.config.sensorTopic + ': ' + result['message']);
+        } else {
+          this.metronAlerts.showSuccessMessage('Started sensor ' + sensor.config.sensorTopic);
+        }
+
+        this.toggleStartStopInProgress(sensor);
+      },
+      error => {
+        this.metronAlerts.showErrorMessage('Unable to start sensor ' + sensor.config.sensorTopic);
+        this.toggleStartStopInProgress(sensor);
+      });
+
+    if (event !== null) {
+      event.stopPropagation();
+    }
+  }
+
+  onDisableSensors() {
+    for (let sensor of this.selectedSensors) {
+      if (sensor['status'] === 'Running') {
+        this.onDisableSensor(sensor, null);
+      }
+    }
+  }
+
+  onDisableSensor(sensor: SensorParserConfigHistory, event) {
+    this.toggleStartStopInProgress(sensor);
+
+    this.stormService.deactivateParser(sensor.config.sensorTopic).subscribe(result => {
+        this.metronAlerts.showSuccessMessage('Disabled sensor ' + sensor.config.sensorTopic);
+        this.toggleStartStopInProgress(sensor);
+      },
+      error => {
+        this.metronAlerts.showErrorMessage('Unable to disable sensor ' + sensor.config.sensorTopic);
+        this.toggleStartStopInProgress(sensor);
+      });
+
+    if (event !== null) {
+      event.stopPropagation();
+    }
+  }
+
+  onEnableSensors() {
+    for (let sensor of this.selectedSensors) {
+      if (sensor['status'] === 'Disabled') {
+        this.onEnableSensor(sensor, null);
+      }
+    }
+  }
+
+  onEnableSensor(sensor: SensorParserConfigHistory, event) {
+    this.toggleStartStopInProgress(sensor);
+
+    this.stormService.activateParser(sensor.config.sensorTopic).subscribe(result => {
+        this.metronAlerts.showSuccessMessage('Enabled sensor ' + sensor.config.sensorTopic);
+        this.toggleStartStopInProgress(sensor);
+      },
+      error => {
+        this.metronAlerts.showErrorMessage('Unable to enabled sensor ' + sensor.config.sensorTopic);
+        this.toggleStartStopInProgress(sensor);
+      });
+
+    if (event != null) {
+      event.stopPropagation();
+    }
+  }
+
+  toggleStartStopInProgress(sensor: SensorParserConfigHistory) {
+    sensor.config['startStopInProgress'] = !sensor.config['startStopInProgress'];
+  }
+
+  onNavigationStart() {
+    this.sensorParserConfigService.setSeletedSensor(null);
+    this.selectedSensors = [];
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.module.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.module.ts
new file mode 100644
index 0000000..d057623
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.module.ts
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import {routing} from './sensor-parser-list.routing';
+import {SensorParserListComponent} from './sensor-parser-list.component';
+import {SharedModule} from '../../shared/shared.module';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../../app.config';
+import {MetronTableModule} from '../../shared/metron-table/metron-table.module';
+
+@NgModule ({
+  imports: [ routing, SharedModule, MetronTableModule ],
+  declarations: [ SensorParserListComponent ],
+  providers: [{ provide: APP_CONFIG, useValue: METRON_REST_CONFIG }]
+})
+export class SensorParserListModule { }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.routing.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.routing.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.routing.ts
new file mode 100644
index 0000000..4910aa5
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.routing.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { ModuleWithProviders }  from '@angular/core';
+import { RouterModule } from '@angular/router';
+import {SensorParserListComponent} from './sensor-parser-list.component';
+import {AuthGuard} from '../../shared/auth-guard';
+
+export const routing: ModuleWithProviders = RouterModule.forChild([
+  { path: '', component: SensorParserListComponent, canActivate: [AuthGuard]}
+]);

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.component.html b/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.component.html
new file mode 100644
index 0000000..291327f
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.component.html
@@ -0,0 +1,49 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<div class="metron-slider-pane-edit fill load-left-to-right dialog2x">
+
+    <div class="form-group">
+        <div class="form-title">Configure Raw JSON</div>
+        <i class="fa fa-times pull-right close-button" aria-hidden="true" (click)="onCancel()"></i>
+    </div>
+
+
+  <form role="form" class="enrichments-form">
+
+      <div class="form-group">
+          <label attr.for="newSensorParserConfig">SENSOR PARSER CONFIG</label>
+          <metron-config-ace-editor [(ngModel)]="newSensorParserConfig" [ngModelOptions]="{standalone: true}" [type]="'JSON'" [placeHolder]="'Enter Sensor Parser Config'"> </metron-config-ace-editor>
+      </div>
+      <div class="form-group">
+          <label attr.for="newSensorEnrichmentConfig">SENSOR ENRICHMENT CONFIG</label>
+          <metron-config-ace-editor [(ngModel)]="newSensorEnrichmentConfig" [ngModelOptions]="{standalone: true}" [type]="'JSON'" [placeHolder]="'Enter Sensor Enrichment Config'"> </metron-config-ace-editor>
+      </div>
+      <div class="form-group">
+          <label attr.for="newIndexingConfigurations">INDEXING CONFIGURATIONS</label>
+          <metron-config-ace-editor [(ngModel)]="newIndexingConfigurations" [ngModelOptions]="{standalone: true}" [type]="'JSON'" [placeHolder]="'Enter Indexing Configurations'"> </metron-config-ace-editor>
+      </div>
+
+      <div class="form-group">
+          <div class="form-seperator-edit"></div>
+          <div class="button-row">
+              <button type="submit" class="btn form-enable-disable-button" (click)="onSave()">SAVE</button>
+              <button class="btn form-enable-disable-button" (click)="onCancel()" >CANCEL</button>
+          </div>
+      </div>
+
+  </form>
+
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.component.scss b/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.component.scss
new file mode 100644
index 0000000..3a9f5e3
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.component.scss
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+textarea
+{
+  height: auto;
+}
+
+.form-group
+{
+  padding-left: 25px;
+  padding-right: 20px;
+  padding-bottom: 10px;
+}
+
+.button-row {
+  padding-top: 10px;
+}
+
+.form-enable-disable-button {
+  width: 16%;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.component.spec.ts
new file mode 100644
index 0000000..f680f10
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.component.spec.ts
@@ -0,0 +1,191 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {async, TestBed, ComponentFixture} from '@angular/core/testing';
+import {SensorRawJsonComponent} from './sensor-raw-json.component';
+import {SharedModule} from '../../shared/shared.module';
+import {SimpleChanges, SimpleChange} from '@angular/core';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import {SensorEnrichmentConfig, EnrichmentConfig, ThreatIntelConfig} from '../../model/sensor-enrichment-config';
+import {SensorRawJsonModule} from './sensor-raw-json.module';
+import {IndexingConfigurations} from '../../model/sensor-indexing-config';
+import '../../rxjs-operators';
+
+describe('Component: SensorRawJsonComponent', () => {
+
+    let fixture: ComponentFixture<SensorRawJsonComponent>;
+    let component: SensorRawJsonComponent;
+    let sensorParserConfigString = '{"parserClassName":"org.apache.metron.parsers.bro.BasicBroParser","sensorTopic":"bro",' +
+        '"parserConfig": {},"fieldTransformations":[]}';
+    let sensorParserConfig: SensorParserConfig = new SensorParserConfig();
+    sensorParserConfig.sensorTopic = 'bro';
+    sensorParserConfig.parserClassName = 'org.apache.metron.parsers.bro.BasicBroParser';
+    sensorParserConfig.parserConfig = {};
+
+    let sensorParserConfigWithClassNameString = `{"parserClassName":"org.apache.metron.parsers.bro.BasicBroParser","sensorTopic":"bro", 
+        "parserConfig": {},"fieldTransformations":[], "writerClassName": "org.example.writerClassName", 
+        "errorWriterClassName": "org.example.errorWriterClassName", 
+        "filterClassName": "org.example.filterClassName", "invalidWriterClassName": "org.example.invalidWriterClassName"}`;
+    let sensorParserConfigWithClassName = Object.assign(new SensorParserConfig(), sensorParserConfig);
+    sensorParserConfigWithClassName.writerClassName = 'org.example.writerClassName';
+    sensorParserConfigWithClassName.errorWriterClassName = 'org.example.errorWriterClassName';
+    sensorParserConfigWithClassName.filterClassName = 'org.example.filterClassName';
+    sensorParserConfigWithClassName.invalidWriterClassName = 'org.example.invalidWriterClassName';
+
+    let sensorEnrichmentConfigString = '{"enrichment" : {"fieldMap": ' +
+        '{"geo": ["ip_dst_addr", "ip_src_addr"],"host": ["host"]}},"threatIntel": {"fieldMap": {"hbaseThreatIntel":' +
+        ' ["ip_src_addr", "ip_dst_addr"]},"fieldToTypeMap": {"ip_src_addr" : ["malicious_ip"],"ip_dst_addr" : ["malicious_ip"]}}}';
+    let sensorEnrichmentConfig = new SensorEnrichmentConfig();
+    sensorEnrichmentConfig.enrichment = Object.assign(new EnrichmentConfig(), {
+      'fieldMap': {
+        'geo': ['ip_dst_addr', 'ip_src_addr'],
+        'host': ['host']
+      }
+    });
+    sensorEnrichmentConfig.threatIntel = Object.assign(new ThreatIntelConfig(), {
+          'fieldMap': {
+            'hbaseThreatIntel': ['ip_src_addr', 'ip_dst_addr']
+          },
+          'fieldToTypeMap': {
+            'ip_src_addr' : ['malicious_ip'],
+            'ip_dst_addr' : ['malicious_ip']
+          }
+        });
+
+    let sensorEnrichmentConfigWithConfigString = `{"configuration": "some-configuration", 
+         "enrichment" : {"fieldMap": {"geo": ["ip_dst_addr", "ip_src_addr"],"host": ["host"]}},
+         "threatIntel": {"fieldMap": {"hbaseThreatIntel":["ip_src_addr", "ip_dst_addr"]},
+         "fieldToTypeMap": {"ip_src_addr" : ["malicious_ip"],"ip_dst_addr" : ["malicious_ip"]}}}`;
+    let sensorEnrichmentConfigWithConfig = Object.assign(new SensorEnrichmentConfig(), sensorEnrichmentConfig);
+    sensorEnrichmentConfigWithConfig.configuration = 'some-configuration';
+
+    let sensorIndexingConfigString = `{"hdfs": {"index": "bro","batchSize": 5,"enabled":true},
+    "elasticsearch": {"index": "bro","batchSize": 5,"enabled":true},
+    "solr": {"index": "bro","batchSize": 5,"enabled":true}}`;
+    let sensorIndexingConfig = new IndexingConfigurations();
+    sensorIndexingConfig.hdfs.index = 'bro';
+    sensorIndexingConfig.hdfs.batchSize = 5;
+    sensorIndexingConfig.hdfs.enabled = true;
+    sensorIndexingConfig.elasticsearch.index = 'bro';
+    sensorIndexingConfig.elasticsearch.batchSize = 5;
+    sensorIndexingConfig.elasticsearch.enabled = true;
+    sensorIndexingConfig.solr.index = 'bro';
+    sensorIndexingConfig.solr.batchSize = 5;
+    sensorIndexingConfig.solr.enabled = true;
+
+    let sensorIndexingConfigChangedString = `{"hdfs": {"index": "squid","batchSize": 1,"enabled":true},
+    "elasticsearch": {"index": "squid","batchSize": 1,"enabled":true},
+    "solr": {"index": "squid","batchSize": 1,"enabled":true}}`;
+    let sensorIndexingConfigChanged = new IndexingConfigurations();
+    sensorIndexingConfigChanged.hdfs.index = 'squid';
+    sensorIndexingConfigChanged.hdfs.batchSize = 1;
+    sensorIndexingConfigChanged.hdfs.enabled = true;
+    sensorIndexingConfigChanged.elasticsearch.index = 'squid';
+    sensorIndexingConfigChanged.elasticsearch.batchSize = 1;
+    sensorIndexingConfigChanged.elasticsearch.enabled = true;
+    sensorIndexingConfigChanged.solr.index = 'squid';
+    sensorIndexingConfigChanged.solr.batchSize = 1;
+    sensorIndexingConfigChanged.solr.enabled = true;
+
+
+    beforeEach(async(() => {
+        TestBed.configureTestingModule({
+            imports: [SharedModule, SensorRawJsonModule],
+        });
+
+        fixture = TestBed.createComponent(SensorRawJsonComponent);
+        component = fixture.componentInstance;
+    }));
+
+    it('should create an instance', () => {
+        expect(component).toBeDefined();
+    });
+
+    it('should create an instance', () => {
+        spyOn(component, 'init');
+        let changes: SimpleChanges = {'showRawJson': new SimpleChange(false, true)};
+
+        component.ngOnChanges(changes);
+        expect(component.init).toHaveBeenCalled();
+
+        changes = {'showRawJson': new SimpleChange(true, false)};
+        component.ngOnChanges(changes);
+        expect(component.init['calls'].count()).toEqual(1);
+
+        fixture.destroy();
+    });
+
+    it('should initialise the fields', () => {
+
+        component.init();
+        expect(component.newSensorParserConfig).toEqual(undefined);
+        expect(component.newSensorEnrichmentConfig).toEqual(undefined);
+        expect(component.newIndexingConfigurations).toEqual(undefined);
+
+        component.sensorParserConfig = sensorParserConfig;
+        component.sensorEnrichmentConfig = sensorEnrichmentConfig;
+        component.indexingConfigurations = sensorIndexingConfig;
+        component.init();
+        expect(component.newSensorParserConfig).toEqual(JSON.stringify(sensorParserConfig, null, '\t'));
+        expect(component.newSensorEnrichmentConfig).toEqual(JSON.stringify(sensorEnrichmentConfig, null, '\t'));
+        expect(component.newIndexingConfigurations).toEqual(JSON.stringify(sensorIndexingConfig, null, '\t'));
+
+        fixture.destroy();
+    });
+
+    it('should save the fields', () => {
+        spyOn(component.hideRawJson, 'emit');
+        spyOn(component.onRawJsonChanged, 'emit');
+        component.sensorParserConfig = new SensorParserConfig();
+        component.sensorEnrichmentConfig = new SensorEnrichmentConfig();
+        component.indexingConfigurations = new IndexingConfigurations();
+
+        component.newSensorParserConfig = sensorParserConfigString;
+        component.newSensorEnrichmentConfig = sensorEnrichmentConfigString;
+        component.newIndexingConfigurations = sensorIndexingConfigString;
+        component.onSave();
+        expect(component.sensorParserConfig).toEqual(sensorParserConfig);
+        expect(component.sensorEnrichmentConfig).toEqual(sensorEnrichmentConfig);
+        expect(component.indexingConfigurations).toEqual(sensorIndexingConfig);
+        expect(component.hideRawJson.emit).toHaveBeenCalled();
+        expect(component.onRawJsonChanged.emit).toHaveBeenCalled();
+
+
+        component.newSensorParserConfig = sensorParserConfigWithClassNameString;
+        component.newSensorEnrichmentConfig = sensorEnrichmentConfigWithConfigString;
+        component.newIndexingConfigurations = sensorIndexingConfigChangedString;
+        component.onSave();
+        expect(component.sensorParserConfig).toEqual(sensorParserConfigWithClassName);
+        expect(component.sensorEnrichmentConfig).toEqual(sensorEnrichmentConfigWithConfig);
+        expect(component.indexingConfigurations).toEqual(sensorIndexingConfigChanged);
+        expect(component.hideRawJson.emit['calls'].count()).toEqual(2);
+        expect(component.onRawJsonChanged.emit['calls'].count()).toEqual(2);
+
+        fixture.destroy();
+    });
+
+    it('should hide panel', () => {
+        spyOn(component.hideRawJson, 'emit');
+
+        component.onCancel();
+
+        expect(component.hideRawJson.emit).toHaveBeenCalled();
+
+        fixture.destroy();
+    });
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.component.ts b/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.component.ts
new file mode 100644
index 0000000..38d9e90
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.component.ts
@@ -0,0 +1,103 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Component, Input, EventEmitter, Output, OnChanges, SimpleChanges} from '@angular/core';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import {SensorEnrichmentConfig, EnrichmentConfig, ThreatIntelConfig} from '../../model/sensor-enrichment-config';
+import {IndexingConfigurations, SensorIndexingConfig} from '../../model/sensor-indexing-config';
+
+declare var ace: any;
+
+@Component({
+  selector: 'metron-config-sensor-raw-json',
+  templateUrl: './sensor-raw-json.component.html',
+  styleUrls: ['./sensor-raw-json.component.scss']
+})
+
+export class SensorRawJsonComponent implements OnChanges {
+
+  @Input() showRawJson: boolean;
+  @Input() sensorParserConfig: SensorParserConfig;
+  @Input() sensorEnrichmentConfig: SensorEnrichmentConfig;
+  @Input() indexingConfigurations: IndexingConfigurations;
+
+  @Output() hideRawJson: EventEmitter<boolean> = new EventEmitter<boolean>();
+  @Output() onRawJsonChanged: EventEmitter<boolean> = new EventEmitter<boolean>();
+
+  newSensorParserConfig: string;
+  newSensorEnrichmentConfig: string;
+  newIndexingConfigurations: string;
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (changes['showRawJson'] && changes['showRawJson'].currentValue) {
+      this.init();
+    }
+  }
+
+  init(): void {
+    if (this.sensorParserConfig) {
+      this.newSensorParserConfig = JSON.stringify(this.sensorParserConfig, null, '\t');
+    }
+
+    if (this.sensorEnrichmentConfig) {
+      this.newSensorEnrichmentConfig = JSON.stringify(this.sensorEnrichmentConfig, null, '\t');
+    }
+
+    if (this.indexingConfigurations) {
+      this.newIndexingConfigurations = JSON.stringify(this.indexingConfigurations, null, '\t');
+    }
+  }
+
+  onSave() {
+    let newParsedSensorParserConfig = JSON.parse(this.newSensorParserConfig);
+    this.sensorParserConfig.sensorTopic = newParsedSensorParserConfig.sensorTopic;
+    this.sensorParserConfig.parserConfig = newParsedSensorParserConfig.parserConfig;
+    this.sensorParserConfig.parserClassName = newParsedSensorParserConfig.parserClassName;
+    this.sensorParserConfig.fieldTransformations = newParsedSensorParserConfig.fieldTransformations;
+
+    if (newParsedSensorParserConfig.writerClassName != null) {
+      this.sensorParserConfig.writerClassName = newParsedSensorParserConfig.writerClassName;
+    }
+    if (newParsedSensorParserConfig.errorWriterClassName != null) {
+      this.sensorParserConfig.errorWriterClassName = newParsedSensorParserConfig.errorWriterClassName;
+    }
+    if (newParsedSensorParserConfig.filterClassName != null) {
+      this.sensorParserConfig.filterClassName = newParsedSensorParserConfig.filterClassName;
+    }
+    if (newParsedSensorParserConfig.invalidWriterClassName != null) {
+      this.sensorParserConfig.invalidWriterClassName = newParsedSensorParserConfig.invalidWriterClassName;
+    }
+
+    let newParsedSensorEnrichmentConfig = JSON.parse(this.newSensorEnrichmentConfig);
+    this.sensorEnrichmentConfig.enrichment = Object.assign(new EnrichmentConfig(), newParsedSensorEnrichmentConfig.enrichment);
+    this.sensorEnrichmentConfig.threatIntel = Object.assign(new ThreatIntelConfig(), newParsedSensorEnrichmentConfig.threatIntel);
+    if (newParsedSensorEnrichmentConfig.configuration != null) {
+      this.sensorEnrichmentConfig.configuration = newParsedSensorEnrichmentConfig.configuration;
+    }
+
+    let newParsedIndexingConfigurations = JSON.parse(this.newIndexingConfigurations);
+    this.indexingConfigurations.hdfs = Object.assign(new SensorIndexingConfig(), newParsedIndexingConfigurations.hdfs);
+    this.indexingConfigurations.elasticsearch = Object.assign(new SensorIndexingConfig(), newParsedIndexingConfigurations.elasticsearch);
+    this.indexingConfigurations.solr = Object.assign(new SensorIndexingConfig(), newParsedIndexingConfigurations.solr);
+    this.hideRawJson.emit(true);
+    this.onRawJsonChanged.emit(true);
+  }
+
+  onCancel(): void {
+    this.hideRawJson.emit(true);
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.module.ts b/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.module.ts
new file mode 100644
index 0000000..965eb73
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-raw-json/sensor-raw-json.module.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import {SharedModule} from '../../shared/shared.module';
+import {SensorRawJsonComponent} from './sensor-raw-json.component';
+import {AceEditorModule} from '../../shared/ace-editor/ace-editor.module';
+
+@NgModule ({
+  imports: [ SharedModule, AceEditorModule ],
+  declarations: [ SensorRawJsonComponent ],
+  exports: [ SensorRawJsonComponent ]
+})
+export class SensorRawJsonModule {}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.html b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.html
new file mode 100644
index 0000000..1a1f9f0
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.html
@@ -0,0 +1,49 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<div class="metron-slider-pane-edit fill load-left-to-right dialog1x">
+
+    <div class="form-title">Edit Rule</div>
+    <i class="fa fa-times pull-right close-button" aria-hidden="true" (click)="onCancel()"></i>
+
+
+    <form role="form" class="threat-intel-form">
+        <div class="form-group">
+          <label attr.for="statement">NAME</label>
+          <input type="text" class="form-control" name="ruleName" [(ngModel)]="newRiskLevelRule.name">
+        </div>
+        <div class="form-group">
+            <label attr.for="statement">TEXT</label>
+            <textarea rows="30" class="form-control" name="ruleValue" [(ngModel)]="newRiskLevelRule.rule" style="height: inherit"></textarea>
+        </div>
+    </form>
+    <form class="form-inline">
+        <div class="form-group">
+            <label attr.for="statement" style="width: 100%">SCORE ADJUSTMENT</label>
+            <input class="score-slider" name="scoreSlider" type="range" min="0" max="100" [(ngModel)]="newRiskLevelRule.score" style="width: 72%; margin-right: 4px">
+            <div style="width: 25%; display: inline-block">
+                <metron-config-number-spinner name="scoreText" [(ngModel)]="newRiskLevelRule.score" [min]="0" [max]="100"> </metron-config-number-spinner>
+            </div>
+        </div>
+    </form>
+
+    <div class="form-group">
+        <div class="form-seperator-edit"></div>
+        <div class="button-row">
+            <button type="submit" class="btn form-enable-disable-button" (click)="onSave()">SAVE</button>
+            <button class="btn form-enable-disable-button" (click)="onCancel()" >CANCEL</button>
+        </div>
+    </div>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.scss b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.scss
new file mode 100644
index 0000000..ca6c7d7
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.scss
@@ -0,0 +1,90 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+$range-gradient-start: #AC9B5A;
+$range-gradient-end: #D8747C;
+.form-title
+{
+  padding-left: 25px;
+}
+
+.form-group
+{
+  padding-left: 25px;
+  padding-right: 20px;
+  padding-bottom: 10px;
+}
+
+.close-button
+{
+  padding-right: 20px;
+}
+
+.score-adjustment
+{
+  display: inline-block;
+}
+
+.button-row {
+  padding-top: 10px;
+}
+
+.form-enable-disable-button {
+  width: 32%;
+}
+
+input[type="range"] {
+  height: 4px;
+  -webkit-appearance: none;
+  -moz-apperance: none;
+  background: $range-gradient-start;
+  background: -webkit-linear-gradient(left, $range-gradient-start, $range-gradient-end);
+  background: -moz-linear-gradient(left, $range-gradient-start, $range-gradient-end);
+  background: -ms-linear-gradient(left, $range-gradient-start, $range-gradient-end);
+  background: -o-linear-gradient(left, $range-gradient-start, $range-gradient-end);
+  background: linear-gradient(to right, $range-gradient-start, $range-gradient-end);
+  border-radius: 4px;
+
+  &:focus
+  {
+    outline: none;
+  }
+}
+
+input[type="range"]::-webkit-slider-thumb{
+  -webkit-appearance:none;
+  -moz-apperance:none;
+  width:15px;
+  height:15px;
+  -webkit-border-radius:20px;
+  -moz-border-radius:20px;
+  -ms-border-radius:20px;
+  -o-border-radius:20px;
+  border-radius:20px;
+  background-color: $range-gradient-start;
+}
+
+input[type="range"]::-moz-range-track
+{
+  background:transparent; border:0px;
+}
+
+input[type=range]::-moz-range-thumb
+{
+  background-color: $range-gradient-start;
+}
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.spec.ts
new file mode 100644
index 0000000..2f8cd24
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.spec.ts
@@ -0,0 +1,74 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {async, TestBed, ComponentFixture} from '@angular/core/testing';
+import {SensorRuleEditorComponent} from './sensor-rule-editor.component';
+import {SharedModule} from '../../../shared/shared.module';
+import {NumberSpinnerComponent} from '../../../shared/number-spinner/number-spinner.component';
+import {RiskLevelRule} from '../../../model/risk-level-rule';
+
+describe('Component: SensorRuleEditorComponent', () => {
+
+    let fixture: ComponentFixture<SensorRuleEditorComponent>;
+    let component: SensorRuleEditorComponent;
+
+    beforeEach(async(() => {
+        TestBed.configureTestingModule({
+            imports: [SharedModule
+            ],
+            declarations: [ SensorRuleEditorComponent, NumberSpinnerComponent ],
+            providers: [
+              SensorRuleEditorComponent
+            ]
+        });
+
+        fixture = TestBed.createComponent(SensorRuleEditorComponent);
+        component = fixture.componentInstance;
+    }));
+
+    it('should create an instance', () => {
+        expect(component).toBeDefined();
+    });
+
+    it('should edit rules', async(() => {
+        let numCancelled = 0;
+        let savedRule = new RiskLevelRule();
+        component.onCancelTextEditor.subscribe((cancelled: boolean) => {
+          numCancelled++;
+        });
+        component.onSubmitTextEditor.subscribe((rule: RiskLevelRule) => {
+          savedRule = rule;
+        });
+
+        component.riskLevelRule =  {name: 'rule1', rule: 'initial rule', score: 1, comment: ''};
+        component.ngOnInit();
+        component.onSave();
+        let rule1 = Object.assign(new RiskLevelRule(), {name: 'rule1', rule: 'initial rule', score: 1, comment: ''});
+        expect(savedRule).toEqual(rule1);
+
+        component.riskLevelRule = {name: 'rule2', rule: 'new rule', score: 2, comment: ''};
+        component.ngOnInit();
+        component.onSave();
+        let rule2 = Object.assign(new RiskLevelRule(), {name: 'rule2', rule: 'new rule', score: 2, comment: ''});
+        expect(savedRule).toEqual(rule2);
+
+        expect(numCancelled).toEqual(0);
+        component.onCancel();
+        expect(numCancelled).toEqual(1);
+    }));
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.ts b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.ts
new file mode 100644
index 0000000..1bdfea1
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.ts
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Component, Input, EventEmitter, Output, OnInit} from '@angular/core';
+import {RiskLevelRule} from '../../../model/risk-level-rule';
+
+@Component({
+  selector: 'metron-config-sensor-rule-editor',
+  templateUrl: './sensor-rule-editor.component.html',
+  styleUrls: ['./sensor-rule-editor.component.scss']
+})
+
+export class SensorRuleEditorComponent implements OnInit {
+
+  @Input() riskLevelRule: RiskLevelRule;
+
+  @Output() onCancelTextEditor: EventEmitter<boolean> = new EventEmitter<boolean>();
+  @Output() onSubmitTextEditor: EventEmitter<RiskLevelRule> = new EventEmitter<RiskLevelRule>();
+  newRiskLevelRule = new RiskLevelRule();
+
+  constructor() { }
+
+  ngOnInit() {
+    Object.assign(this.newRiskLevelRule, this.riskLevelRule);
+  }
+
+  onSave(): void {
+    this.onSubmitTextEditor.emit(this.newRiskLevelRule);
+  }
+
+  onCancel(): void {
+    this.onCancelTextEditor.emit(true);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.module.ts b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.module.ts
new file mode 100644
index 0000000..a99c8cf
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.module.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import {SharedModule} from '../../../shared/shared.module';
+import {SensorRuleEditorComponent} from './sensor-rule-editor.component';
+import {NumberSpinnerModule} from '../../../shared/number-spinner/number-spinner.module';
+
+@NgModule ({
+  imports: [ SharedModule, NumberSpinnerModule ],
+  declarations: [ SensorRuleEditorComponent ],
+  exports: [ SensorRuleEditorComponent ]
+})
+export class SensorRuleEditorModule {}


[02/12] incubator-metron git commit: METRON-623 Management UI [contributed by Raghu Mitra Kandikonda and Ryan Merriman] closes apache/incubator-metron#489

Posted by rm...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/util/httpUtils.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/util/httpUtils.spec.ts b/metron-interface/metron-config/src/app/util/httpUtils.spec.ts
new file mode 100644
index 0000000..ff1e39d
--- /dev/null
+++ b/metron-interface/metron-config/src/app/util/httpUtils.spec.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {HttpUtil} from './httpUtil';
+import {Response, ResponseOptions, ResponseType} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {RestError} from '../model/rest-error';
+
+describe('HttpUtil', () => {
+
+  it('should create an instance', () => {
+    expect(HttpUtil.handleError).toBeTruthy();
+    expect(HttpUtil.extractString).toBeTruthy();
+    expect(HttpUtil.extractData).toBeTruthy();
+  });
+
+  it('should handleError', () => {
+    let error500: RestError = {message: 'This is error', responseCode: 500, fullMessage: 'This is error'};
+    let responseOptions = new ResponseOptions();
+    responseOptions.body = error500;
+    let response = new Response(responseOptions);
+    response.type = ResponseType.Basic;
+    expect(HttpUtil.handleError(response)).toEqual(Observable.throw(error500));
+
+    let error404 = new RestError();
+    error404.responseCode = 404;
+    response = new Response(new ResponseOptions());
+    response.type = ResponseType.Basic;
+    response.status = 404;
+    expect(HttpUtil.handleError(response)).toEqual(Observable.throw(error404));
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/util/stringUtils.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/util/stringUtils.ts b/metron-interface/metron-config/src/app/util/stringUtils.ts
new file mode 100644
index 0000000..81c3d57
--- /dev/null
+++ b/metron-interface/metron-config/src/app/util/stringUtils.ts
@@ -0,0 +1,31 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export class StringUtil {
+  public static getIndexOfLastNonAlphaNumeric(str: string): number {
+    let tLastIndex = str.length - 1;
+
+    for (; tLastIndex >= 0; tLastIndex--) {
+      let ch = str[tLastIndex];
+      if (/[^a-zA-Z0-9_]/.test(ch)) {
+        break;
+      }
+    }
+
+    return tLastIndex;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/verticalnavbar/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/verticalnavbar/index.ts b/metron-interface/metron-config/src/app/verticalnavbar/index.ts
new file mode 100644
index 0000000..8519f13
--- /dev/null
+++ b/metron-interface/metron-config/src/app/verticalnavbar/index.ts
@@ -0,0 +1,18 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export * from './verticalnavbar.component';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/verticalnavbar/verticalnavbar.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/verticalnavbar/verticalnavbar.component.scss b/metron-interface/metron-config/src/app/verticalnavbar/verticalnavbar.component.scss
new file mode 100644
index 0000000..a21e128
--- /dev/null
+++ b/metron-interface/metron-config/src/app/verticalnavbar/verticalnavbar.component.scss
@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../_variables.scss";
+
+.bootstrap-vertical-nav
+{
+  padding: 10px 0px 0px 5px;
+
+  .nav-link-title
+  {
+    padding: 5px 0px 17px 17px;
+    font-size: 14px;
+  }
+
+  .nav-link.active
+  {
+    border-left: 3px solid $nav-active-color;
+    font-family: Roboto-Regular;
+    font-size: 14px;
+    font-weight: normal;
+    color: $nav-active-text-color;
+  }
+
+  .nav-link
+  {
+      padding-left: 17px;
+      border-left: 3px solid $gray-light;
+      font-family: Roboto-Regular;
+      font-size: 14px;
+      font-weight: normal;
+      color: $text-color-white;
+  }
+}
+
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/verticalnavbar/verticalnavbar.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/verticalnavbar/verticalnavbar.component.spec.ts b/metron-interface/metron-config/src/app/verticalnavbar/verticalnavbar.component.spec.ts
new file mode 100644
index 0000000..7be3ccd
--- /dev/null
+++ b/metron-interface/metron-config/src/app/verticalnavbar/verticalnavbar.component.spec.ts
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {Router} from '@angular/router';
+import {VerticalNavbarComponent} from './verticalnavbar.component';
+
+class MockRouter {
+  url: string = '';
+}
+
+describe('VerticalNavbarComponent', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      providers: [
+        VerticalNavbarComponent,
+        {provide: Router, useClass: MockRouter}
+      ]
+    }).compileComponents();
+
+  }));
+
+  it('can instantiate VerticalNavbarComponent',
+    inject([VerticalNavbarComponent], (verticalNavbarComponent: VerticalNavbarComponent) => {
+      expect(verticalNavbarComponent instanceof VerticalNavbarComponent).toBe(true);
+  }));
+
+  it('check isActive for a URL VerticalNavbarComponent',
+    inject([VerticalNavbarComponent, Router], (component: VerticalNavbarComponent, router: Router) => {
+
+      router.url = '/abc';
+      expect(component.isActive(['/def'])).toEqual(false);
+      expect(component.isActive(['/abc'])).toEqual(true);
+      expect(component.isActive(['/def', '/abc'])).toEqual(true);
+
+  }));
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/verticalnavbar/verticalnavbar.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/verticalnavbar/verticalnavbar.component.ts b/metron-interface/metron-config/src/app/verticalnavbar/verticalnavbar.component.ts
new file mode 100644
index 0000000..4067f6c
--- /dev/null
+++ b/metron-interface/metron-config/src/app/verticalnavbar/verticalnavbar.component.ts
@@ -0,0 +1,35 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Component, ChangeDetectionStrategy} from '@angular/core';
+import {Router} from '@angular/router';
+
+@Component({
+  selector: 'metron-config-vertical-navbar',
+  templateUrl: 'verticalnavbar.html',
+  styleUrls: ['verticalnavbar.component.scss'],
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+
+export class VerticalNavbarComponent {
+
+  constructor(private router: Router) {}
+
+  isActive(validRoutes: string[]) {
+    return validRoutes.indexOf(this.router.url) !== -1;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/verticalnavbar/verticalnavbar.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/verticalnavbar/verticalnavbar.html b/metron-interface/metron-config/src/app/verticalnavbar/verticalnavbar.html
new file mode 100644
index 0000000..a4052b3
--- /dev/null
+++ b/metron-interface/metron-config/src/app/verticalnavbar/verticalnavbar.html
@@ -0,0 +1,29 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<div #verticalnavbar class="bootstrap-vertical-nav full-height">
+
+  <div class="nav-link-title">Operations</div>
+
+  <ul class="nav">
+    <li class="nav-item">
+      <a [routerLink]="['/sensors']"  class="nav-link" [ngClass]="{'active': isActive(['/', '/sensors'])}" href="#">Sensors</a>
+    </li>
+    <li class="nav-item">
+      <a [routerLink]="['/general-settings']"  class="nav-link" [ngClass]="{'active': isActive(['/general-settings'])}" href="#">General Settings</a>
+    </li>
+  </ul>
+
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/.npmignore
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/.npmignore b/metron-interface/metron-config/src/assets/.npmignore
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/ace/LICENSE
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/ace/LICENSE b/metron-interface/metron-config/src/assets/ace/LICENSE
new file mode 100644
index 0000000..4760be2
--- /dev/null
+++ b/metron-interface/metron-config/src/assets/ace/LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) 2010, Ajax.org B.V.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of Ajax.org B.V. nor the
+      names of its contributors may be used to endorse or promote products
+      derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/ace/mode-grok.js
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/ace/mode-grok.js b/metron-interface/metron-config/src/assets/ace/mode-grok.js
new file mode 100644
index 0000000..9e6329b
--- /dev/null
+++ b/metron-interface/metron-config/src/assets/ace/mode-grok.js
@@ -0,0 +1,106 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+ace.define('ace/mode/grok_highlight_rules', function(require, exports, module) {
+
+    "use strict";
+
+    var oop = require("../lib/oop");
+    var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
+
+    var GrokHighlightRules = function() {
+
+        var escapeRe = /\\u[0-9a-fA-F]{4}|\\/;
+
+        this.$rules = {
+            "start" : [
+                {
+                    token : "paren.lparen",
+                    regex: "\\%{",
+                    next  : "key"
+                },{
+                    token : "comment",
+                    regex: "\\s*[-/]\\s*"
+                },{
+                    token : "comment",
+                    regex: "\\s*\\\\s*"
+                },{
+                    defaultToken: "invalid"
+                }
+            ],
+            "key" : [
+                {
+                    token: "variable",
+                    regex: "[a-zA-Z0-9]*",
+                    next  : "seperator"
+                },{
+                    defaultToken: "invalid"
+                }
+            ],"seperator" : [
+                {
+                    token: "seperator",
+                    regex: "\\s*:{1}",
+                    next  : "value"
+                },{
+                    defaultToken: "invalid"
+                }
+            ],"value" : [
+                {
+                    token: "string",
+                    regex: "\\s*[a-zA-Z0-9-_]*",
+                    next  : "end"
+                },{
+                    defaultToken: "invalid"
+                }
+            ],"end" : [
+                {
+                    token : "paren.rparen",
+                    regex : "\\}\\s*",
+                    next:   "start"
+                },{
+                    defaultToken: "invalid"
+                }
+            ]
+        };
+
+    };
+
+    oop.inherits(GrokHighlightRules, TextHighlightRules);
+
+    exports.GrokHighlightRules = GrokHighlightRules;
+
+});
+
+ace.define('ace/mode/grok', function(require, exports, module) {
+
+    var oop = require("ace/lib/oop");
+    var TextMode = require("ace/mode/text").Mode;
+    var GrokHighlightRules = require("ace/mode/grok_highlight_rules").GrokHighlightRules;
+
+    var Mode = function() {
+        this.HighlightRules = GrokHighlightRules;
+    };
+
+    oop.inherits(Mode, TextMode);
+
+    (function() {
+    }).call(Mode.prototype);
+
+    exports.Mode = Mode;
+});
+
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/ace/snippets/grok.js
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/ace/snippets/grok.js b/metron-interface/metron-config/src/assets/ace/snippets/grok.js
new file mode 100644
index 0000000..a48cedf
--- /dev/null
+++ b/metron-interface/metron-config/src/assets/ace/snippets/grok.js
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+ace.define("ace/snippets/grok",["require","exports","module"], function(require, exports, module) {
+    "use strict";
+
+    exports.snippetText =undefined;
+    exports.scope = "grok";
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/fonts/Roboto/LICENSE.txt
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/fonts/Roboto/LICENSE.txt b/metron-interface/metron-config/src/assets/fonts/Roboto/LICENSE.txt
new file mode 100755
index 0000000..d645695
--- /dev/null
+++ b/metron-interface/metron-config/src/assets/fonts/Roboto/LICENSE.txt
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Black.ttf
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Black.ttf b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Black.ttf
new file mode 100755
index 0000000..fbde625
Binary files /dev/null and b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Black.ttf differ

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-BlackItalic.ttf
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-BlackItalic.ttf b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-BlackItalic.ttf
new file mode 100755
index 0000000..60f7782
Binary files /dev/null and b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-BlackItalic.ttf differ

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Bold.ttf
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Bold.ttf b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Bold.ttf
new file mode 100755
index 0000000..a355c27
Binary files /dev/null and b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Bold.ttf differ

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-BoldItalic.ttf
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-BoldItalic.ttf b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-BoldItalic.ttf
new file mode 100755
index 0000000..3c9a7a3
Binary files /dev/null and b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-BoldItalic.ttf differ

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Italic.ttf
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Italic.ttf b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Italic.ttf
new file mode 100755
index 0000000..ff6046d
Binary files /dev/null and b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Italic.ttf differ

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Light.ttf
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Light.ttf b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Light.ttf
new file mode 100755
index 0000000..94c6bcc
Binary files /dev/null and b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Light.ttf differ

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-LightItalic.ttf
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-LightItalic.ttf b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-LightItalic.ttf
new file mode 100755
index 0000000..04cc002
Binary files /dev/null and b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-LightItalic.ttf differ

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Medium.ttf
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Medium.ttf b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Medium.ttf
new file mode 100755
index 0000000..39c63d7
Binary files /dev/null and b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Medium.ttf differ

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-MediumItalic.ttf
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-MediumItalic.ttf b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-MediumItalic.ttf
new file mode 100755
index 0000000..dc743f0
Binary files /dev/null and b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-MediumItalic.ttf differ

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Regular.ttf
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Regular.ttf b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Regular.ttf
new file mode 100755
index 0000000..8c082c8
Binary files /dev/null and b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Regular.ttf differ

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Thin.ttf
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Thin.ttf b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Thin.ttf
new file mode 100755
index 0000000..d695550
Binary files /dev/null and b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-Thin.ttf differ

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-ThinItalic.ttf
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-ThinItalic.ttf b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-ThinItalic.ttf
new file mode 100755
index 0000000..07172ff
Binary files /dev/null and b/metron-interface/metron-config/src/assets/fonts/Roboto/Roboto-ThinItalic.ttf differ

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/images/login.jpg
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/images/login.jpg b/metron-interface/metron-config/src/assets/images/login.jpg
new file mode 100644
index 0000000..a76a206
Binary files /dev/null and b/metron-interface/metron-config/src/assets/images/login.jpg differ

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/assets/images/logo.png
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/assets/images/logo.png b/metron-interface/metron-config/src/assets/images/logo.png
new file mode 100644
index 0000000..a0bc8cb
Binary files /dev/null and b/metron-interface/metron-config/src/assets/images/logo.png differ

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/environments/environment.prod.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/environments/environment.prod.ts b/metron-interface/metron-config/src/environments/environment.prod.ts
new file mode 100644
index 0000000..3612073
--- /dev/null
+++ b/metron-interface/metron-config/src/environments/environment.prod.ts
@@ -0,0 +1,3 @@
+export const environment = {
+  production: true
+};

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/environments/environment.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/environments/environment.ts b/metron-interface/metron-config/src/environments/environment.ts
new file mode 100644
index 0000000..00313f1
--- /dev/null
+++ b/metron-interface/metron-config/src/environments/environment.ts
@@ -0,0 +1,8 @@
+// The file contents for the current environment will overwrite these during build.
+// The build system defaults to the dev environment which uses `environment.ts`, but if you do
+// `ng build --env=prod` then `environment.prod.ts` will be used instead.
+// The list of which env maps to which file can be found in `angular-cli.json`.
+
+export const environment = {
+  production: false
+};

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/favicon.ico
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/favicon.ico b/metron-interface/metron-config/src/favicon.ico
new file mode 100644
index 0000000..a108a83
Binary files /dev/null and b/metron-interface/metron-config/src/favicon.ico differ

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/index.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/index.html b/metron-interface/metron-config/src/index.html
new file mode 100644
index 0000000..2c3f8c5
--- /dev/null
+++ b/metron-interface/metron-config/src/index.html
@@ -0,0 +1,38 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<!doctype html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Metron Configuration</title>
+  <base href="/">
+
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <link rel="icon" type="image/x-icon" href="favicon.ico">
+
+</head>
+<body>
+  <metron-config-root>
+    <div style="position: absolute;top: 45%; left: 50%">
+      <i class="fa fa-cog fa-spin fa-3x fa-fw" ></i>
+      <div>Loading ... </div>
+    </div>
+  </metron-config-root>
+
+
+
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/main.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/main.ts b/metron-interface/metron-config/src/main.ts
new file mode 100644
index 0000000..70335e5
--- /dev/null
+++ b/metron-interface/metron-config/src/main.ts
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './polyfills.ts';
+
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+import { enableProdMode } from '@angular/core';
+import { environment } from './environments/environment';
+import { AppModule } from './app/app.module';
+
+if (environment.production) {
+  enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(AppModule);

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/polyfills.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/polyfills.ts b/metron-interface/metron-config/src/polyfills.ts
new file mode 100644
index 0000000..640ac99
--- /dev/null
+++ b/metron-interface/metron-config/src/polyfills.ts
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// This file includes polyfills needed by Angular 2 and is loaded before
+// the app. You can add your own extra polyfills to this file.
+import 'core-js/es6/symbol';
+import 'core-js/es6/object';
+import 'core-js/es6/function';
+import 'core-js/es6/parse-int';
+import 'core-js/es6/parse-float';
+import 'core-js/es6/number';
+import 'core-js/es6/math';
+import 'core-js/es6/string';
+import 'core-js/es6/date';
+import 'core-js/es6/array';
+import 'core-js/es6/regexp';
+import 'core-js/es6/map';
+import 'core-js/es6/set';
+import 'core-js/es6/reflect';
+
+import 'core-js/es7/reflect';
+import 'zone.js/dist/zone';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/styles.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/styles.scss b/metron-interface/metron-config/src/styles.scss
new file mode 100644
index 0000000..db57e91
--- /dev/null
+++ b/metron-interface/metron-config/src/styles.scss
@@ -0,0 +1,739 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* You can add global styles to this file, and also import other style files */
+@import "app/_variables.scss";
+@import "app/_main.scss";
+
+$height: 60px;
+
+html,body
+{
+  height:100%;
+  background-color: $gray-dark;
+  color: $text-color-white;
+  font-family: "Roboto";
+}
+
+.header
+{
+  height: $height;
+}
+
+.body-fill
+{
+  height: -webkit-calc(100% - 60px);
+  height: calc(100% - 60px);
+  min-height: -webkit-calc(100% - 60px);
+  min-height: calc(100% - 60px);
+}
+
+.fill
+{
+  min-height: 100%;
+  height: 100%;
+}
+
+.metron-bg-inverse
+{
+  background-color: $gray-dark;
+}
+
+.btn
+{
+  font-size: 0.9em;
+}
+
+.metron-title
+{
+  font-family: Roboto-Medium;
+  font-size: 14px;
+  font-weight: 500;
+  color: $text-color-white;
+}
+
+table, .table
+{
+  margin-top: 0.5rem;
+
+  thead {
+    th {
+      font-family: Roboto-Regular;
+      font-size: 11px;
+      color: $text-color-white;
+      border-top: hidden !important;
+      border-bottom: 2px solid $tundora !important;
+    }
+  }
+  tbody {
+    tr
+    {
+      font-family: Roboto-Regular;
+      font-size: 12px;
+      letter-spacing: 0.1px;
+      color: $table-cell-text-color;
+      td:first-child
+      {
+        color: $text-color-white;
+      }
+      td
+      {
+        border-top: none;
+        border-bottom: 1px solid $tundora;
+      }
+      &:hover
+      {
+        background: $edit-background;
+        border-left: 1px solid $edit-background-border;
+        border-right: 1px solid $edit-background-border;
+      }
+    }
+  }
+  .active
+  {
+    background: $table-selection;
+    border-left: 1px solid $table-selection-lr-border;
+    border-right: 1px solid $table-selection-lr-border;
+    td
+    {
+      border-top: 1px solid  $table-selection-tb-border !important;
+      border-bottom: 1px solid $table-selection-tb-border;
+    }
+  }
+}
+
+input:-webkit-autofill {
+  -webkit-box-shadow: 0 0 0px 100px $field-background inset;
+  -webkit-text-fill-color: $form-field-text-color !important;
+  background-color: $field-background !important;
+}
+
+label
+{
+  margin-bottom: 0.17em;
+}
+
+.form-group
+{
+  margin-bottom: 0.2em;
+}
+
+
+.table
+{
+  thead
+  {
+    th
+    {
+      padding: 0.60em;
+    }
+  }
+  tr
+  {
+    td
+    {
+      padding: 0.60em;
+    }
+  }
+}
+
+.form-table {
+  background: $field-background;
+}
+
+.metron-add-button
+{
+  position: fixed;
+  right: 50px;
+  bottom: 37px;
+  background: none;
+  border:none;
+  cursor: pointer;
+
+  &:focus
+  {
+    outline: none;
+  }
+
+  i
+  {
+    color: #ffffff;
+  }
+}
+
+.metron-slider-pane
+{
+  background-color: $gray-light;
+  border: solid 1px $gray-border;
+  height: auto;
+  display: inline-block;
+  vertical-align:top;
+  float: right;
+  padding: 15px 20px 0px $slider-left-padding;
+  word-wrap: break-word;
+  position: relative;
+
+  .close-button
+  {
+    font-size: 26px;
+    cursor: pointer;
+  }
+}
+
+.flexbox-row-reverse
+{
+  min-height: 100%;
+  overflow: auto;
+  display: flex;
+  flex-wrap: wrap;
+  flex-direction: row-reverse;
+  justify-content: flex-start;
+}
+
+@media only screen and (min-width: 500px) {
+  .dialog1x {
+    width: $dialog-1x-width;
+  }
+  .dialog2x {
+    width: $dialog-2x-width;
+  }
+}
+
+@media only screen and (min-width: 2020px) {
+  .dialog1x {
+    width: $dialog-2x-width;
+  }
+  .dialog2x {
+    width: $dialog-4x-width;
+  }
+}
+
+.metron-slider-pane-edit
+{
+  @extend .metron-slider-pane;
+  @extend .metron-edit-pane-background;
+  padding-left: 0;
+  padding-right: 0;
+  min-height: 100%;
+}
+
+.metron-edit-pane-background
+{
+  background: $edit-child-background;
+  border: 1px solid $edit-background-border;
+}
+
+form
+{
+  label
+  {
+    font-family: Roboto-Medium;
+    font-size: 12px;
+    color: $form-label;
+  }
+
+  input
+  {
+    background: $form-input-background;
+  }
+}
+
+.form-label
+{
+  font-family: Roboto-Regular;
+  font-size: 12px;
+  color: $form-label
+}
+
+.form-value
+{
+  font-family: Roboto-Regular;
+  font-size: 14px;
+  color: $form-field-text-color;
+
+}
+
+.form-title
+{
+  font-family: Roboto-Medium;
+  font-size: 18px;
+  color: $form-field-text-color;
+  display: inline-block;
+}
+
+.form-title-subscript
+{
+  font-family: Roboto-Medium;
+  font-size: 12px;
+  color: $title-subscript-color;
+}
+
+.form-seperator
+{
+  border: solid 1px $form-field-separator-color;
+  margin: 10px 0px 10px 0px;
+}
+
+.form-seperator-edit
+{
+  border: solid 1px $edit-background-border;
+  margin: 10px 0px 10px 0px;
+}
+
+.form-edit-button
+{
+  color: $field-button-color;
+  padding-left: 5px;
+  cursor: pointer;
+}
+
+.form-enable-disable-button
+{
+  background-color: $gray-light;
+  border-color: $form-button-border;
+  color: $field-button-color;
+  font-size: 14px;
+  min-width: 90px;
+
+  &:hover
+  {
+
+  }
+
+  &:focus
+  {
+    outline: none;
+  }
+}
+
+
+.form-control, select
+{
+  border: solid 1px $gray-border;
+  background-color: $field-background;
+  font-family: Roboto;
+  font-size: 13px;
+  color: $form-field-text-color;
+  height: 35px;
+
+  &[readonly]
+  {
+    background: $form-field-separator-color;
+    border: 1px solid $gray-border;
+  }
+
+  &:focus
+  {
+    background-color: $field-background;
+    border-color: $gray-border;
+  }
+}
+
+.form-control, textarea
+{
+  border: solid 1px $gray-border;
+  background-color: $field-background;
+  font-family: Roboto;
+  font-size: 13px;
+  color: $form-field-text-color;
+
+  &[readonly]
+  {
+    background: $form-field-separator-color;
+    border: 1px solid $gray-border;
+  }
+
+  &:focus
+  {
+    background-color: $field-background;
+    border-color: $gray-border;
+  }
+}
+
+.input-group button
+{
+  border: solid 1px $gray-border;
+  background: $field-background;
+  border-left: none;
+  font-size: 14px;
+
+  &:focus
+  {
+    outline: none;
+  }
+
+  &[readonly]
+  {
+    background: $form-field-separator-color;
+    border: 1px solid $gray-border;
+  }
+}
+
+@mixin keyframes($animation-name, $start) {
+  @-webkit-keyframes #{$animation-name} {
+    0% {
+      --webkit-transform: translateX(#{$start});
+    }
+    100% {
+      -webkit-transform: translateX(0);
+    }
+  }
+  @-moz-keyframes #{$animation-name} {
+    0% {
+      -moz-transform: translateX(#{$start});
+    }
+    100% {
+      -moz-transform: translateX(0);
+    }
+  }
+  @-ms-keyframes #{$animation-name} {
+    0% {
+      -ms-transform: translateX(#{$start});
+    }
+    100% {
+      -ms-transform: translateX(0);
+    }
+  }
+  @-o-keyframes #{$animation-name} {
+    0% {
+      -o-transform: translateX(#{$start});
+    }
+    100% {
+      -o-transform: translateX(0);
+    }
+  }
+  @keyframes #{$animation-name} {
+    0% {
+      transform: translateX(#{$start});
+    }
+    100% {
+      transform: translateX(0);
+    }
+  }
+}
+
+@mixin keyframesWidth($animation-name) {
+  @-webkit-keyframes #{$animation-name} {
+    @content
+  }
+  @-moz-keyframes #{$animation-name} {
+    @content
+  }
+  @-ms-keyframes #{$animation-name} {
+    @content
+  }
+  @-o-keyframes #{$animation-name} {
+    @content
+  }
+  @keyframes #{$animation-name} {
+    @content
+  }
+}
+
+@mixin animation($name, $duration, $function)
+{
+  -moz-animation: #{$name} #{$duration} #{$function};
+  -webkit-animation: #{$name} #{$duration} #{$function};
+  animation: #{$name} #{$duration} #{$function};
+}
+
+@include keyframes("keyframe-dialog-rtl", "320px")
+
+.load-right-to-left{
+  @include animation("keyframe-dialog-rtl", "0.5s", "linear")
+}
+
+//@include keyframes("keyframe-dialog-ltr", "320px")
+
+@include keyframesWidth("keyframe-dialog-width")
+{
+  0%   { margin-right: -640px; }
+  100% { margin-right: 0px; }
+}
+
+.load-left-to-right {
+  @include animation("keyframe-dialog-width", "0.5s", "ease-in-out")
+}
+
+.btn-primary
+{
+  background-color: $form-button-border;
+  border-color: $form-button-border;
+  &:hover, &:active, &:focus
+  {
+    background-color: $form-button-border;
+    border-color: $form-button-border;
+  }
+}
+
+.open > .btn-primary.dropdown-toggle
+{
+  &:hover, &:active, &:focus
+  {
+    background-color: $form-button-border;
+    border-color: $form-button-border;
+  }
+}
+
+.fontawesome-checkbox
+{
+  display: none;
+}
+
+.fontawesome-checkbox ~label
+{
+  margin: 0px;
+  display: inline-block;
+  height: 13px;
+  line-height: 13px;
+  border: 1px solid $gray-border;
+  border-radius: 2px;
+}
+
+.fontawesome-checkbox ~label:before {
+  font-family: "FontAwesome";
+  font-style: normal;
+  font-size: 12px;
+  content: '\f0c8';
+  color: $gray-light;
+}
+
+.fontawesome-checkbox:checked ~ label:before {
+  content: '\f14a';
+  color: $nav-active-color;
+}
+
+.hexa-button
+{
+  display: inline-block;
+  text-align: center;
+  z-index: 0;
+  line-height: 0.7em;
+  font-size: 30px;
+  height: 28px;
+  width: 49px;
+  background: $form-button-border;
+
+  &::before, &::after
+  {
+    position: absolute;
+    content: "";
+    left: 0px;
+    top: 0;
+    z-index: -1;
+    height: 28px;
+    width: 49px;
+    background: $form-button-border;
+  }
+  &::before
+  {
+    @include transform(rotate(60deg));
+  }
+  &::after
+  {
+    @include transform(rotate(-60deg));
+  }
+}
+
+.warning-text
+{
+  color: $warning-color;
+}
+
+.success-text-color
+{
+  color: $ocean-green;
+}
+
+
+.icon-container
+{
+  width: 100px;
+  i
+  {
+    color: #33a6dd;
+    padding: 0px 2px;
+    cursor: pointer;
+  }
+}
+
+.icon-blue
+{
+  i
+  {
+    color: #33a6dd;
+    cursor: pointer;
+  }
+}
+
+.dropdown-item
+{
+  cursor: pointer;
+  color: #818a91;
+
+  &:focus, &:hover
+  {
+    background-color: $edit-background-border ;
+  }
+}
+
+a.blue-label
+{
+  color: #33a6dd;
+  font-size: 14px;
+  text-decoration: none ;
+}
+
+.readonly-view-inline-button {
+  display: inline-block;
+}
+
+.readonly-view-inline-button:not(:last-child) {
+  margin-right: 2px;
+}
+
+.input-placeholder {
+  font-size: 11px;
+  font-style: italic;
+  color:#999999;
+}
+
+.popover
+{
+  border: 1px solid #195d68;
+  //background-color: #0b4451;
+  background-color:#195d68;
+}
+
+.popover-title
+{
+  color: #bdbdbd;
+  background-color:#195d68;
+  border-color: #195d68;
+  border-top-left-radius: 4px;
+  border-top-right-radius: 4px;
+}
+
+.popover-content {
+  background-color: $tundora;
+  padding: 9px 14px;
+  color: #bdbdbd;
+  border-bottom-left-radius: 4px;
+  border-bottom-right-radius: 4px;
+  white-space:pre-wrap;
+}
+
+.metron-dialog.modal
+{
+  .modal-content
+  {
+    background-color: $gray-light;
+    border: solid 1px $gray-border;
+  }
+
+  .close
+  {
+    color: #BDBDBD;
+    text-shadow: 0 1px 0 $silver-color;
+    opacity: 1;
+    font-size: 28px;
+  }
+
+  .modal-title
+  {
+    color: $silver-color;
+    font-size: 18px;
+  }
+
+  .modal-body
+  {
+    color: $dusty-grey;
+  }
+
+  .modal-header
+  {
+    border-bottom: none;
+  }
+
+  .modal-footer
+  {
+    text-align: left;
+    border-top: none;
+  }
+}
+
+button
+{
+  i
+  {
+    color: $text-color-white;
+  }
+}
+
+.icon-button
+{
+  font-size: 16px;
+  cursor: pointer;
+  padding: 8px 10px;
+  border-radius: 4px;
+  border: 1px solid $gray-border;
+  color: $nav-active-text-color;
+  background: $form-field-separator-color;
+}
+
+.details-pane-padding
+{
+  padding: 0px 3px 0px 30px;
+}
+
+.metron-button-bar
+{
+  width: 100%;
+  bottom: 0;
+  position: absolute;
+  z-index: 10;
+  padding-left: 25px;
+  margin-left: ($slider-left-padding)* -1;
+  background: $edit-background;
+  border-top: solid 2px $edit-background-border;
+  border-right: solid 1px $edit-background-border;
+  border-left: solid 1px $edit-background-border;
+  max-height: $button-bar-height;
+}
+
+.tooltip
+{
+  font-family: "Roboto";
+}
+
+.card
+{
+  overflow: auto;
+}
+
+.form-control:focus
+{
+  color: #bdbdbd;
+}
+
+.ace_autocomplete.ace-monokai
+{
+  .ace_content {
+    background: #444;
+    color: #999;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/test.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/test.ts b/metron-interface/metron-config/src/test.ts
new file mode 100644
index 0000000..347d245
--- /dev/null
+++ b/metron-interface/metron-config/src/test.ts
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './polyfills.ts';
+
+import 'zone.js/dist/long-stack-trace-zone';
+import 'zone.js/dist/proxy.js';
+import 'zone.js/dist/sync-test';
+import 'zone.js/dist/jasmine-patch';
+import 'zone.js/dist/async-test';
+import 'zone.js/dist/fake-async-test';
+
+import 'jquery/dist/jquery';
+import 'tether/dist/js/tether';
+
+import * as $ from 'jquery';
+window['$'] = window['jQuery'] = $;
+
+import  * as Tether from 'tether';
+window['Tether'] = Tether;
+
+import 'ace-builds/src-noconflict/ace.js';
+import 'bootstrap/dist/js/bootstrap';
+import 'bootstrap';
+
+// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
+declare var __karma__: any;
+declare var require: any;
+
+// Prevent Karma from running prematurely.
+__karma__.loaded = function () {};
+
+
+Promise.all([
+  System.import('@angular/core/testing'),
+  System.import('@angular/platform-browser-dynamic/testing')
+])
+  // First, initialize the Angular testing environment.
+  .then(([testing, testingBrowser]) => {
+    testing.getTestBed().initTestEnvironment(
+      testingBrowser.BrowserDynamicTestingModule,
+      testingBrowser.platformBrowserDynamicTesting()
+    );
+  })
+  // Then we find all the tests.
+  .then(() => require.context('./', true, /\.spec\.ts/))
+  // And load the modules.
+  .then(context => context.keys().map(context))
+  // Finally, start Karma to run the tests.
+  .then(__karma__.start, __karma__.error);

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/tsconfig.json
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/tsconfig.json b/metron-interface/metron-config/src/tsconfig.json
new file mode 100644
index 0000000..65756e3
--- /dev/null
+++ b/metron-interface/metron-config/src/tsconfig.json
@@ -0,0 +1,24 @@
+{
+  "compilerOptions": {
+    "declaration": false,
+    "emitDecoratorMetadata": true,
+    "experimentalDecorators": true,
+    "lib": ["es6", "dom"],
+    "mapRoot": "./",
+    "module": "es6",
+    "moduleResolution": "node",
+    "outDir": "../dist/out-tsc",
+    "sourceMap": true,
+    "target": "es5",
+    "typeRoots": [
+      "../node_modules/@types"
+    ],
+    "types": [
+      "jasmine",
+      "jquery",
+      "bootstrap",
+      "ace"
+    ]
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/typings.d.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/typings.d.ts b/metron-interface/metron-config/src/typings.d.ts
new file mode 100644
index 0000000..c0aaf0c
--- /dev/null
+++ b/metron-interface/metron-config/src/typings.d.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Typings reference file, see links for more information
+// https://github.com/typings/typings
+// https://www.typescriptlang.org/docs/handbook/writing-declaration-files.html
+
+declare var System: any;
+declare var require: any;

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/tslint.json
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/tslint.json b/metron-interface/metron-config/tslint.json
new file mode 100644
index 0000000..93037ac
--- /dev/null
+++ b/metron-interface/metron-config/tslint.json
@@ -0,0 +1,112 @@
+{
+  "rulesDirectory": [
+    "node_modules/codelyzer"
+  ],
+  "rules": {
+    "class-name": true,
+    "comment-format": [
+      true,
+      "check-space"
+    ],
+    "curly": true,
+    "eofline": true,
+    "forin": true,
+    "indent": [
+      true,
+      "spaces"
+    ],
+    "label-position": true,
+    "label-undefined": true,
+    "max-line-length": [
+      true,
+      140
+    ],
+    "member-access": false,
+    "member-ordering": [
+      true,
+      "static-before-instance",
+      "variables-before-functions"
+    ],
+    "no-arg": true,
+    "no-bitwise": true,
+    "no-console": [
+      true,
+      "debug",
+      "info",
+      "time",
+      "timeEnd",
+      "trace"
+    ],
+    "no-construct": true,
+    "no-debugger": true,
+    "no-duplicate-key": true,
+    "no-duplicate-variable": true,
+    "no-empty": false,
+    "no-eval": true,
+    "no-inferrable-types": true,
+    "no-shadowed-variable": true,
+    "no-string-literal": false,
+    "no-switch-case-fall-through": true,
+    "no-trailing-whitespace": true,
+    "no-unused-expression": true,
+    "no-unused-variable": true,
+    "no-unreachable": true,
+    "no-use-before-declare": true,
+    "no-var-keyword": true,
+    "object-literal-sort-keys": false,
+    "one-line": [
+      true,
+      "check-open-brace",
+      "check-catch",
+      "check-else",
+      "check-whitespace"
+    ],
+    "quotemark": [
+      true,
+      "single"
+    ],
+    "radix": true,
+    "semicolon": [
+      "always"
+    ],
+    "triple-equals": [
+      true,
+      "allow-null-check"
+    ],
+    "typedef-whitespace": [
+      true,
+      {
+        "call-signature": "nospace",
+        "index-signature": "nospace",
+        "parameter": "nospace",
+        "property-declaration": "nospace",
+        "variable-declaration": "nospace"
+      }
+    ],
+    "variable-name": false,
+    "whitespace": [
+      true,
+      "check-branch",
+      "check-decl",
+      "check-operator",
+      "check-separator",
+      "check-type"
+    ],
+
+    "directive-selector-prefix": [true, "metron-config"],
+    "component-selector-prefix": [true, "metron-config"],
+    "directive-selector-name": [true, "camelCase"],
+    "component-selector-name": [true, "kebab-case"],
+    "directive-selector-type": [true, "attribute"],
+    "component-selector-type": [true, "element"],
+    "use-input-property-decorator": true,
+    "use-output-property-decorator": true,
+    "use-host-property-decorator": true,
+    "no-input-rename": true,
+    "no-output-rename": true,
+    "use-life-cycle-interface": true,
+    "use-pipe-transform-interface": true,
+    "component-class-suffix": true,
+    "directive-class-suffix": true
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/model/TopologyStatus.java
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/model/TopologyStatus.java b/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/model/TopologyStatus.java
index 6fc1c01..a057d1b 100644
--- a/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/model/TopologyStatus.java
+++ b/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/model/TopologyStatus.java
@@ -28,6 +28,8 @@ public class TopologyStatus {
   private Map<String, Object>[] topologyStats;
   private Double latency = 0.0;
   private Double throughput = 0.0;
+  private Integer emitted = 0;
+  private Integer acked = 0;
 
   public String getId() {
     return id;
@@ -61,14 +63,24 @@ public class TopologyStatus {
     return throughput;
   }
 
+  public Integer getEmitted() {
+    return emitted;
+  }
+
+  public long getAcked() {
+    return acked;
+  }
+
   public void setTopologyStats(List<Map<String, Object>> topologyStats) {
     for(Map<String, Object> topologyStatsItem: topologyStats) {
       if ("600".equals(topologyStatsItem.get("window"))) {
         latency = Double.parseDouble((String) topologyStatsItem.get("completeLatency"));
-        int acked = 0;
         if (topologyStatsItem.get("acked") != null) {
           acked = (int) topologyStatsItem.get("acked");
         }
+        if (topologyStatsItem.get("emitted") != null) {
+          emitted= (int) topologyStatsItem.get("emitted");
+        }
         throughput = acked / 600.00;
       }
     }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest/README.md
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest/README.md b/metron-interface/metron-rest/README.md
index 8de1982..c52b21d 100644
--- a/metron-interface/metron-rest/README.md
+++ b/metron-interface/metron-rest/README.md
@@ -87,6 +87,7 @@ Request and Response objects are JSON formatted.  The JSON schemas are available
 | [ `GET /api/v1/global/config`](#get-apiv1globalconfig)|
 | [ `DELETE /api/v1/global/config`](#delete-apiv1globalconfig)|
 | [ `POST /api/v1/global/config`](#post-apiv1globalconfig)|
+| [ `GET /api/v1/grok/get/statement`](#get-apiv1grokgetstatement)|
 | [ `GET /api/v1/grok/list`](#get-apiv1groklist)|
 | [ `POST /api/v1/grok/validate`](#post-apiv1grokvalidate)|
 | [ `POST /api/v1/hdfs`](#post-apiv1hdfs)|
@@ -158,6 +159,13 @@ Request and Response objects are JSON formatted.  The JSON schemas are available
     * 200 - Global Config updated. Returns saved Global Config JSON
     * 201 - Global Config created. Returns saved Global Config JSON
 
+### `GET /api/v1/grok/get/statement`
+  * Description: Retrieves a Grok statement from the classpath
+  * Input:
+    * path - Path to classpath resource
+  * Returns:
+    * 200 - Grok statement
+
 ### `GET /api/v1/grok/list`
   * Description: Lists the common Grok statements available in Metron
   * Returns:

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/GrokController.java
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/GrokController.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/GrokController.java
index d561897..5ce8b83 100644
--- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/GrokController.java
+++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/GrokController.java
@@ -29,6 +29,7 @@ import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 import java.util.Map;
@@ -53,4 +54,11 @@ public class GrokController {
     ResponseEntity<Map<String, String>> list() throws RestException {
         return new ResponseEntity<>(grokService.getCommonGrokPatterns(), HttpStatus.OK);
     }
+
+    @ApiOperation(value = "Retrieves a Grok statement from the classpath")
+    @ApiResponse(message = "Grok statement", code = 200)
+    @RequestMapping(value = "/get/statement", method = RequestMethod.GET)
+    ResponseEntity<String> get(@ApiParam(name = "path", value = "Path to classpath resource", required = true) @RequestParam String path) throws RestException {
+      return new ResponseEntity<>(grokService.getStatementFromClasspath(path), HttpStatus.OK);
+    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/HdfsController.java
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/HdfsController.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/HdfsController.java
index bf7ae84..c8ff0b6 100644
--- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/HdfsController.java
+++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/HdfsController.java
@@ -64,7 +64,7 @@ public class HdfsController {
 
   }
 
-  @ApiOperation(value = "Writes contents to an HDFS file.  Warning: this will overwite the contents of a file if it already exists.")
+  @ApiOperation(value = "Writes contents to an HDFS file.  Warning: this will overwrite the contents of a file if it already exists.")
   @ApiResponse(message = "Contents were written", code = 200)
   @RequestMapping(method = RequestMethod.POST)
   ResponseEntity<Void> write(@ApiParam(name="path", value="Path to HDFS file", required=true) @RequestParam String path,

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/KafkaController.java
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/KafkaController.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/KafkaController.java
index 876558b..0cd4d54 100644
--- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/KafkaController.java
+++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/KafkaController.java
@@ -69,7 +69,7 @@ public class KafkaController {
         return new ResponseEntity<>(kafkaService.listTopics(), HttpStatus.OK);
     }
 
-    @ApiOperation(value = "Delets a Kafka topic")
+    @ApiOperation(value = "Deletes a Kafka topic")
     @ApiResponses(value = { @ApiResponse(message = "Kafka topic was deleted", code = 200),
             @ApiResponse(message = "Kafka topic is missing", code = 404) })
     @RequestMapping(value = "/topic/{name}", method = RequestMethod.DELETE)

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/SensorEnrichmentConfigController.java
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/SensorEnrichmentConfigController.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/SensorEnrichmentConfigController.java
index 546384a..826be98 100644
--- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/SensorEnrichmentConfigController.java
+++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/SensorEnrichmentConfigController.java
@@ -21,6 +21,7 @@ import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
 import io.swagger.annotations.ApiResponse;
 import io.swagger.annotations.ApiResponses;
+import org.apache.metron.common.aggregator.Aggregators;
 import org.apache.metron.common.configuration.enrichment.SensorEnrichmentConfig;
 import org.apache.metron.rest.RestException;
 import org.apache.metron.rest.service.SensorEnrichmentConfigService;
@@ -90,8 +91,15 @@ public class SensorEnrichmentConfigController {
 
   @ApiOperation(value = "Lists the available enrichments")
   @ApiResponse(message = "Returns a list of available enrichments", code = 200)
-  @RequestMapping(value = "/list/available", method = RequestMethod.GET)
-  ResponseEntity<List<String>> getAvailable() throws RestException {
+  @RequestMapping(value = "/list/available/enrichments", method = RequestMethod.GET)
+  ResponseEntity<List<String>> getAvailableEnrichments() throws RestException {
     return new ResponseEntity<>(sensorEnrichmentConfigService.getAvailableEnrichments(), HttpStatus.OK);
   }
+
+  @ApiOperation(value = "Lists the available threat triage aggregators")
+  @ApiResponse(message = "Returns a list of available threat triage aggregators", code = 200)
+  @RequestMapping(value = "/list/available/threat/triage/aggregators", method = RequestMethod.GET)
+  ResponseEntity<List<String>> getAvailableThreatTriageAggregators() throws RestException {
+    return new ResponseEntity<>(sensorEnrichmentConfigService.getAvailableThreatTriageAggregators(), HttpStatus.OK);
+  }
 }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/GrokService.java
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/GrokService.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/GrokService.java
index 268d396..adeb1ed 100644
--- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/GrokService.java
+++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/GrokService.java
@@ -31,4 +31,6 @@ public interface GrokService {
 
     File saveTemporary(String statement, String name) throws RestException;
 
+    String getStatementFromClasspath(String path) throws RestException;
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/SensorEnrichmentConfigService.java
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/SensorEnrichmentConfigService.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/SensorEnrichmentConfigService.java
index fe84802..817a57d 100644
--- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/SensorEnrichmentConfigService.java
+++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/SensorEnrichmentConfigService.java
@@ -17,6 +17,7 @@
  */
 package org.apache.metron.rest.service;
 
+import org.apache.metron.common.aggregator.Aggregators;
 import org.apache.metron.common.configuration.enrichment.SensorEnrichmentConfig;
 import org.apache.metron.rest.RestException;
 
@@ -37,4 +38,6 @@ public interface SensorEnrichmentConfigService {
 
     List<String> getAvailableEnrichments();
 
+    List<String> getAvailableThreatTriageAggregators();
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/GrokServiceImpl.java
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/GrokServiceImpl.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/GrokServiceImpl.java
index 3f2de2f..edae13b 100644
--- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/GrokServiceImpl.java
+++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/GrokServiceImpl.java
@@ -19,6 +19,7 @@ package org.apache.metron.rest.service.impl;
 
 import oi.thekraken.grok.api.Grok;
 import oi.thekraken.grok.api.Match;
+import org.apache.commons.io.IOUtils;
 import org.apache.directory.api.util.Strings;
 import org.apache.hadoop.fs.Path;
 import org.apache.metron.rest.RestException;
@@ -110,4 +111,12 @@ public class GrokServiceImpl implements GrokService {
       return new Path(grokTempPath, authentication.getName()).toString();
     }
 
+    public String getStatementFromClasspath(String path) throws RestException {
+      try {
+        return IOUtils.toString(getClass().getResourceAsStream(path));
+      } catch (Exception e) {
+        throw new RestException("Could not find a statement at path " + path);
+      }
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/SensorEnrichmentConfigServiceImpl.java
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/SensorEnrichmentConfigServiceImpl.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/SensorEnrichmentConfigServiceImpl.java
index 2bfef89..d4438a4 100644
--- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/SensorEnrichmentConfigServiceImpl.java
+++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/SensorEnrichmentConfigServiceImpl.java
@@ -19,6 +19,7 @@ package org.apache.metron.rest.service.impl;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.curator.framework.CuratorFramework;
+import org.apache.metron.common.aggregator.Aggregators;
 import org.apache.metron.common.configuration.ConfigurationType;
 import org.apache.metron.common.configuration.ConfigurationsUtils;
 import org.apache.metron.common.configuration.enrichment.SensorEnrichmentConfig;
@@ -29,9 +30,11 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 @Service
 public class SensorEnrichmentConfigServiceImpl implements SensorEnrichmentConfigService {
@@ -113,4 +116,8 @@ public class SensorEnrichmentConfigServiceImpl implements SensorEnrichmentConfig
         }};
     }
 
+    @Override
+    public List<String> getAvailableThreatTriageAggregators() {
+      return Arrays.asList(Aggregators.values()).stream().map(Enum::toString).collect(Collectors.toList());
+    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/SensorParserConfigServiceImpl.java
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/SensorParserConfigServiceImpl.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/SensorParserConfigServiceImpl.java
index eddfc8d..37d59d0 100644
--- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/SensorParserConfigServiceImpl.java
+++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/SensorParserConfigServiceImpl.java
@@ -164,6 +164,7 @@ public class SensorParserConfigServiceImpl implements SensorParserConfigService
         sensorParserConfig.getParserConfig().put(MetronRestConstants.GROK_PATH_KEY, temporaryGrokFile.toString());
       }
       parser.configure(sensorParserConfig.getParserConfig());
+      parser.init();
       JSONObject results = parser.parse(parseMessageRequest.getSampleData().getBytes()).get(0);
       if (isGrokConfig(sensorParserConfig) && temporaryGrokFile != null) {
         temporaryGrokFile.delete();

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/GrokControllerIntegrationTest.java
----------------------------------------------------------------------
diff --git a/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/GrokControllerIntegrationTest.java b/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/GrokControllerIntegrationTest.java
index e0a9c5b..8888eb0 100644
--- a/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/GrokControllerIntegrationTest.java
+++ b/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/GrokControllerIntegrationTest.java
@@ -18,6 +18,7 @@
 package org.apache.metron.rest.controller;
 
 import org.adrianwalker.multilinestring.Multiline;
+import org.apache.commons.io.FileUtils;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -30,6 +31,8 @@ import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.setup.MockMvcBuilders;
 import org.springframework.web.context.WebApplicationContext;
 
+import java.io.File;
+
 import static org.apache.metron.rest.MetronRestConstants.TEST_PROFILE;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
@@ -127,5 +130,16 @@ public class GrokControllerIntegrationTest {
                 .andExpect(status().isOk())
                 .andExpect(content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8")))
                 .andExpect(jsonPath("$").isNotEmpty());
+
+        String statement = FileUtils.readFileToString(new File("../../metron-platform/metron-parsers/src/main/resources/patterns/squid"));
+        this.mockMvc.perform(get(grokUrl + "/get/statement?path=/patterns/squid").with(httpBasic(user,password)))
+                .andExpect(status().isOk())
+                .andExpect(content().contentType(MediaType.parseMediaType("text/plain;charset=UTF-8")))
+                .andExpect(content().bytes(statement.getBytes()));
+
+        this.mockMvc.perform(get(grokUrl + "/get/statement?path=/bad/path").with(httpBasic(user,password)))
+                .andExpect(status().isInternalServerError())
+                .andExpect(jsonPath("$.responseCode").value(500))
+                .andExpect(jsonPath("$.message").value("Could not find a statement at path /bad/path"));
     }
 }



[10/12] incubator-metron git commit: METRON-623 Management UI [contributed by Raghu Mitra Kandikonda and Ryan Merriman] closes apache/incubator-metron#489

Posted by rm...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/global.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/global.scss b/metron-interface/metron-config/src/app/global.scss
new file mode 100644
index 0000000..8b64cab
--- /dev/null
+++ b/metron-interface/metron-config/src/app/global.scss
@@ -0,0 +1,594 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "_variables.scss";
+@import "_main.scss";
+
+$height: 60px;
+
+html,body
+{
+  height:100%;
+  background-color: $gray-dark;
+  color: $text-color-white;
+  font-family: "Roboto";
+}
+
+.header
+{
+  height: $height;
+}
+
+.body-fill
+{
+  height: -webkit-calc(100% - 60px);
+  height: calc(100% - 60px);
+  min-height: -webkit-calc(100% - 60px);
+  min-height: calc(100% - 60px);
+}
+
+.fill
+{
+  min-height: 100%;
+  height: 100%;
+}
+
+.metron-bg-inverse
+{
+  background-color: $gray-dark;
+}
+
+.btn
+{
+  font-size: 0.9em;
+}
+
+.metron-title
+{
+  font-family: Roboto-Medium;
+  font-size: 14px;
+  font-weight: 500;
+  color: $text-color-white;
+}
+
+table, .table
+{
+  margin-top: 0.5rem;
+
+  thead {
+    th {
+      font-family: Roboto-Regular;
+      font-size: 11px;
+      color: $text-color-white;
+      border-top: hidden !important;
+      border-bottom: 2px solid #404040 !important;
+    }
+  }
+  tr
+  {
+    font-family: Roboto-Regular;
+    font-size: 12px;
+    letter-spacing: 0.1px;
+    color: $table-cell-text-color;
+    td:first-child
+    {
+      color: $text-color-white;
+    }
+    td
+    {
+      border-bottom: 1px solid #404040 !important;
+    }
+  }
+  .active
+  {
+    background: $edit-background;
+    border-left: 1px solid $edit-background-border;
+    border-right: 1px solid $edit-background-border;
+    td
+    {
+      border-top: 1px solid  $edit-background-border !important;
+      border-bottom: 1px solid $edit-background-border;
+    }
+  }
+}
+
+input:-webkit-autofill {
+   -webkit-box-shadow: 0 0 0px 100px $field-background inset;
+   -webkit-text-fill-color: $form-field-text-color !important;
+   background-color: $field-background !important;
+ }
+
+.table
+{
+  thead
+  {
+    th
+    {
+      padding: 0.60em;
+    }
+  }
+  tr
+  {
+    td
+    {
+      padding: 0.60em;
+    }
+  }
+}
+
+.form-table {
+  background: $field-background;
+}
+
+.metron-add-button
+{
+  position: fixed;
+  right: 50px;
+  bottom: 37px;
+  background: none;
+  border:none;
+  cursor: pointer;
+
+  &:focus
+  {
+    outline: none;
+  }
+
+  i
+  {
+    color: #ffffff;
+  }
+}
+
+.metron-slider-pane
+{
+  background-color: $gray-light;
+  border: solid 1px $gray-border;
+  height: auto;
+  display: inline-block;
+  vertical-align:top;
+  float: right;
+  padding: 15px 20px 0px 25px;
+  word-wrap: break-word;
+
+  .close-button
+  {
+    font-size: 26px;
+    cursor: pointer;
+  }
+}
+
+.dialog1x
+{
+  width: 320px;
+}
+
+.dialog2x
+{
+  width :640px
+}
+
+//.metron-slider-pane-edit
+//{
+//  @extend .metron-slider-pane;
+//  background: $edit-background;
+//  border: 1px solid $edit-background-border;
+//}
+//
+//.metron-slider-pane-edit-child
+//{
+//  @extend .metron-slider-pane-edit;
+//  background-color: #083b44;
+//}
+
+form
+{
+  label
+  {
+    font-family: Roboto-Medium;
+    font-size: 12px;
+    //letter-spacing: -0.8px;
+    color: $form-label;
+  }
+
+  input
+  {
+    background: $form-input-background;
+  }
+}
+
+.form-label
+{
+  font-family: Roboto-Medium;
+  font-size: 12px;
+  //letter-spacing: -0.8px;
+  color: $form-label
+}
+
+.form-value
+{
+  font-family: Roboto;
+  font-size: 14px;
+  color: $form-field-text-color;
+
+}
+
+.form-title
+{
+  font-family: Roboto-Medium;
+  font-size: 18px;
+  color: $form-field-text-color;
+  display: inline-block;
+}
+
+.form-title-subscript
+{
+  font-family: Roboto-Medium;
+  font-size: 12px;
+  color: $title-subscript-color;
+}
+
+.form-seperator
+{
+  border: solid 1px $form-field-separator-color;
+  margin: 10px 0px 10px 0px;
+}
+
+.form-seperator-edit
+{
+  border: solid 1px $edit-background-border;
+  margin: 10px 0px 10px 0px;
+}
+
+.form-edit-button
+{
+  color: $field-button-color;
+  padding-left: 5px;
+  cursor: pointer;
+}
+
+.form-enable-disable-button
+{
+  background-color: $gray-light;
+  border-color: $form-button-border;
+  color: $field-button-color;
+  font-size: 14px;
+
+  &:hover
+  {
+
+  }
+
+  &:focus
+  {
+    outline: none;
+  }
+}
+
+
+.form-control, select
+{
+  border: solid 1px $gray-border;
+  background-color: $field-background;
+  font-family: Roboto;
+  font-size: 13px;
+  color: $form-field-text-color;
+  height: 35px;
+
+  &[readonly]
+  {
+    background: $form-field-separator-color;
+    border: 1px solid $gray-border;
+  }
+
+  &:focus
+  {
+    background-color: $field-background;
+    border-color: $gray-border;
+  }
+}
+
+.input-group button
+{
+  border: solid 1px $gray-border;
+  background: $field-background;
+  border-left: none;
+  font-size: 14px;
+
+  &:focus
+  {
+    outline: none;
+  }
+
+  &[readonly]
+  {
+    background: $form-field-separator-color;
+    border: 1px solid $gray-border;
+  }
+}
+
+@mixin keyframes($animation-name, $start) {
+  @-webkit-keyframes #{$animation-name} {
+    0% {
+      --webkit-transform: translateX(#{$start});
+    }
+    100% {
+      -webkit-transform: translateX(0);
+    }
+  }
+  @-moz-keyframes #{$animation-name} {
+    0% {
+      -moz-transform: translateX(#{$start});
+    }
+    100% {
+      -moz-transform: translateX(0);
+    }
+  }
+  @-ms-keyframes #{$animation-name} {
+    0% {
+      -ms-transform: translateX(#{$start});
+    }
+    100% {
+      -ms-transform: translateX(0);
+    }
+  }
+  @-o-keyframes #{$animation-name} {
+    0% {
+      -o-transform: translateX(#{$start});
+    }
+    100% {
+      -o-transform: translateX(0);
+    }
+  }
+  @keyframes #{$animation-name} {
+    0% {
+      transform: translateX(#{$start});
+    }
+    100% {
+      transform: translateX(0);
+    }
+  }
+}
+
+@mixin keyframesWidth($animation-name) {
+  @-webkit-keyframes #{$animation-name} {
+    @content
+  }
+  @-moz-keyframes #{$animation-name} {
+    @content
+  }
+  @-ms-keyframes #{$animation-name} {
+    @content
+  }
+  @-o-keyframes #{$animation-name} {
+    @content
+  }
+  @keyframes #{$animation-name} {
+    @content
+  }
+}
+
+@mixin animation($name, $duration, $function)
+{
+  -moz-animation: #{$name} #{$duration} #{$function};
+  -webkit-animation: #{$name} #{$duration} #{$function};
+  animation: #{$name} #{$duration} #{$function};
+}
+
+@include keyframes("keyframe-dialog-rtl", "320px")
+
+.load-right-to-left{
+  @include animation("keyframe-dialog-rtl", "0.5s", "linear")
+}
+
+//@include keyframes("keyframe-dialog-ltr", "320px")
+
+@include keyframesWidth("keyframe-dialog-width")
+{
+  0%   { margin-right: -640px; }
+  100% { margin-right: 0px; }
+}
+
+.load-left-to-right {
+  @include animation("keyframe-dialog-width", "0.5s", "ease-in-out")
+}
+
+.btn-primary
+{
+  background-color: $form-button-border;
+  border-color: $form-button-border;
+  &:hover, &:active, &:focus
+  {
+    background-color: $form-button-border;
+    border-color: $form-button-border;
+  }
+}
+
+.open > .btn-primary.dropdown-toggle
+{
+  &:hover, &:active, &:focus
+  {
+    background-color: $form-button-border;
+    border-color: $form-button-border;
+  }
+}
+
+.fontawesome-checkbox
+{
+  display: none;
+}
+
+.fontawesome-checkbox ~label
+{
+  margin: 0px;
+  display: inline-block;
+  height: 13px;
+  line-height: 13px;
+  border: 1px solid $gray-border;
+  border-radius: 2px;
+}
+
+.fontawesome-checkbox ~label:before {
+  font-family: "FontAwesome";
+  font-style: normal;
+  font-size: 12px;
+  content: '\f0c8';
+  color: $gray-light;
+}
+
+.fontawesome-checkbox:checked ~ label:before {
+  content: '\f14a';
+  color: $nav-active-color;
+}
+
+.hexa-button
+{
+  display: inline-block;
+  text-align: center;
+  z-index: 0;
+  line-height: 0.7em;
+  font-size: 30px;
+  height: 28px;
+  width: 49px;
+  background: $form-button-border;
+
+  &::before, &::after
+  {
+    position: absolute;
+    content: "";
+    left: 0px;
+    top: 0;
+    z-index: -1;
+    height: 28px;
+    width: 49px;
+    background: $form-button-border;
+  }
+  &::before
+  {
+    @include transform(rotate(60deg));
+  }
+  &::after
+  {
+    @include transform(rotate(-60deg));
+  }
+}
+
+.warning-text
+{
+  color: $warning-color;
+}
+
+.icon-container
+{
+  width: 100px;
+  i
+  {
+    color: #33a6dd;
+    padding: 0px 2px;
+    cursor: pointer;
+  }
+}
+
+.dropdown-item
+{
+  cursor: pointer;
+  color: #818a91;
+
+  &:focus, &:hover
+  {
+    background-color: $edit-background-border ;
+  }
+}
+
+a.blue-label
+{
+  color: #33a6dd;
+  font-size: 14px;
+  text-decoration: none ;
+}
+
+.input-placeholder {
+  font-size: 11px;
+  font-style: italic;
+  color:#999999;
+}
+
+.popover
+{
+  border: 1px solid #195d68;
+  //background-color: #0b4451;
+  background-color:#195d68;
+}
+
+.popover-title
+{
+  color: #bdbdbd;
+  background-color:#195d68;
+  border-color: #195d68;
+  border-top-left-radius: 4px;
+  border-top-right-radius: 4px;
+}
+
+.popover-content {
+  background-color: #404040;
+  padding: 9px 14px;
+  color: #bdbdbd;
+  border-bottom-left-radius: 4px;
+  border-bottom-right-radius: 4px;
+  white-space:pre-wrap;
+}
+
+.metron-dialog.modal {
+  .modal-content {
+    background-color: $gray-light;
+    border: solid 1px $gray-border;
+  }
+
+  .close {
+    color: #BDBDBD;
+    text-shadow: 0 1px 0 $silver-color;
+    opacity: 1;
+    font-size: 28px;
+  }
+
+  .modal-title {
+    color: $silver-color;
+    font-size: 18px;
+  }
+
+  .modal-body {
+    color: $dusty-grey;
+  }
+
+  .modal-header {
+    border-bottom: none;
+  }
+
+  .modal-footer
+  {
+    text-align: left;
+    border-top: none;
+  }
+}
+
+button
+{
+  i
+  {
+    color: $text-color-white;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/index.ts b/metron-interface/metron-config/src/app/index.ts
new file mode 100644
index 0000000..1639c33
--- /dev/null
+++ b/metron-interface/metron-config/src/app/index.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export * from './environment';
+export * from './app.component';
+export * from './app.routes';
+export * from './app.config';
+export * from './app.config.interface';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/login/login.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/login/login.component.html b/metron-interface/metron-config/src/app/login/login.component.html
new file mode 100644
index 0000000..dcd5c34
--- /dev/null
+++ b/metron-interface/metron-config/src/app/login/login.component.html
@@ -0,0 +1,28 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<div class="fill login-pane" style="">
+  <form (ngSubmit)="login()" class="login-card">
+    <img class="logo-img-card" src="assets/images/logo.png"/>
+    <label class="label"> USERNAME </label>
+    <input class="form-control" name="user" [(ngModel)]="user" required autofocus>
+    <label class="label"> PASSWORD </label>
+    <input type="password" name="password" class="form-control" [(ngModel)]="password" required>
+    <div class="my-1" style="color:#a94442">
+      {{loginFailure}}
+      <button class="btn btn-primary pull-right" type="submit" (click)="login()">LOG IN</button>
+    </div>
+  </form>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/login/login.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/login/login.component.scss b/metron-interface/metron-config/src/app/login/login.component.scss
new file mode 100644
index 0000000..8289a2f
--- /dev/null
+++ b/metron-interface/metron-config/src/app/login/login.component.scss
@@ -0,0 +1,55 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../_variables.scss";
+
+.login-pane {
+  background: url(/assets/images/login.jpg);
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+}
+
+.label
+{
+  font-size: 0.8em;
+  color: $login-label;
+}
+
+.login-card
+{
+  top: 25%;
+  height: 380px;
+  width: 480px;
+  opacity: 0.8;
+  margin: 0 auto;
+  border: none;
+  padding: 20px 25px 30px;
+  background-color: $black;
+  display: block;
+  position: relative;
+}
+
+.logo-img-card
+{
+  margin: 0 auto 10px;
+  display: block;
+  width: 390px;
+  height: 120px;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/login/login.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/login/login.component.spec.ts b/metron-interface/metron-config/src/app/login/login.component.spec.ts
new file mode 100644
index 0000000..0cf3554
--- /dev/null
+++ b/metron-interface/metron-config/src/app/login/login.component.spec.ts
@@ -0,0 +1,65 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {AuthenticationService} from '../service/authentication.service';
+import {LoginComponent} from './login.component';
+
+class MockAuthenticationService {
+
+  public login(username: string, password: string, onError): void {
+    if (username === 'success') {
+      onError({status: 200});
+    }
+
+    if (username === 'failure') {
+      onError({status: 401});
+    }
+  }
+}
+
+describe('LoginComponent', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      providers: [
+        LoginComponent,
+        {provide: AuthenticationService, useClass: MockAuthenticationService}
+      ]
+    })
+      .compileComponents();
+
+  }));
+
+  it('can instantiate login component', inject([LoginComponent], (loginComponent: LoginComponent) => {
+      expect(loginComponent instanceof LoginComponent).toBe(true);
+  }));
+
+  it('can instantiate login component', inject([LoginComponent], (loginComponent: LoginComponent) => {
+    loginComponent.user = 'success';
+    loginComponent.password = 'success';
+    loginComponent.login();
+    expect(loginComponent.loginFailure).toEqual('');
+
+    loginComponent.user = 'failure';
+    loginComponent.password = 'failure';
+    loginComponent.login();
+    expect(loginComponent.loginFailure).toEqual('Login failed for failure');
+
+  }));
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/login/login.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/login/login.component.ts b/metron-interface/metron-config/src/app/login/login.component.ts
new file mode 100644
index 0000000..cdd672a
--- /dev/null
+++ b/metron-interface/metron-config/src/app/login/login.component.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component } from '@angular/core';
+import { AuthenticationService } from '../service/authentication.service';
+
+@Component({
+  selector: 'metron-config-login',
+  templateUrl: 'login.component.html',
+  styleUrls: ['login.component.scss'],
+})
+export class LoginComponent {
+
+  user: string;
+  password: string;
+  loginFailure: string = '';
+
+  constructor(private authenticationService: AuthenticationService) {
+  }
+
+  login(): void {
+    this.authenticationService.login(this.user, this.password, error => {
+      if (error.status === 401) {
+        this.loginFailure = 'Login failed for ' + this.user;
+      }
+    });
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/login/login.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/login/login.module.ts b/metron-interface/metron-config/src/app/login/login.module.ts
new file mode 100644
index 0000000..6efd125
--- /dev/null
+++ b/metron-interface/metron-config/src/app/login/login.module.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import {routing} from "./login.routing";
+
+@NgModule ({
+  imports: [ routing ]
+})
+export class LoginModule { }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/login/login.routing.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/login/login.routing.ts b/metron-interface/metron-config/src/app/login/login.routing.ts
new file mode 100644
index 0000000..7d2d22e
--- /dev/null
+++ b/metron-interface/metron-config/src/app/login/login.routing.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { ModuleWithProviders }  from '@angular/core';
+import { RouterModule } from '@angular/router';
+import {LoginComponent} from "./login.component";
+
+
+export const routing: ModuleWithProviders = RouterModule.forChild([
+  { path: '', component: LoginComponent}
+]);

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/autocomplete-option.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/autocomplete-option.ts b/metron-interface/metron-config/src/app/model/autocomplete-option.ts
new file mode 100644
index 0000000..8006696
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/autocomplete-option.ts
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export class AutocompleteOption {
+  name: string;
+  title: string;
+  description: string;
+  isFunction: boolean;
+  isActive: boolean;
+
+  constructor(name?: string, title?: string, description?: string) {
+    this.name = name;
+    this.title = title;
+    this.description = description;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/field-transformation.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/field-transformation.ts b/metron-interface/metron-config/src/app/model/field-transformation.ts
new file mode 100644
index 0000000..f53a447
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/field-transformation.ts
@@ -0,0 +1,20 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export class FieldTransformation {
+  name: string;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/field-transformer.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/field-transformer.ts b/metron-interface/metron-config/src/app/model/field-transformer.ts
new file mode 100644
index 0000000..ace68c7
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/field-transformer.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export class FieldTransformer {
+  input: string[];
+  output: string[];
+  transformation: string;
+  config: {};
+  initialized: boolean;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/grok-validation.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/grok-validation.ts b/metron-interface/metron-config/src/app/model/grok-validation.ts
new file mode 100644
index 0000000..52e4026
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/grok-validation.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export class GrokValidation {
+  patternLabel: string;
+  statement: string;
+  sampleData: string;
+  results: {};
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/kafka-topic.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/kafka-topic.ts b/metron-interface/metron-config/src/app/model/kafka-topic.ts
new file mode 100644
index 0000000..799488f
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/kafka-topic.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export class KafkaTopic {
+  name: string;
+  numPartitions: number;
+  replicationFactor: number;
+  properties: any = {};
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/parse-message-request.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/parse-message-request.ts b/metron-interface/metron-config/src/app/model/parse-message-request.ts
new file mode 100644
index 0000000..053d7ad
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/parse-message-request.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {SensorParserConfig} from './sensor-parser-config';
+export class ParseMessageRequest {
+  sensorParserConfig: SensorParserConfig;
+  grokStatement: string;
+  sampleData: string;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/rest-error.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/rest-error.ts b/metron-interface/metron-config/src/app/model/rest-error.ts
new file mode 100644
index 0000000..5daee25
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/rest-error.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export class RestError {
+  responseCode: number;
+  message: string;
+  fullMessage: string;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/risk-level-rule.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/risk-level-rule.ts b/metron-interface/metron-config/src/app/model/risk-level-rule.ts
new file mode 100644
index 0000000..1cd4e8a
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/risk-level-rule.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export class RiskLevelRule {
+  name: string = '';
+  comment: string = '';
+  rule: string = '';
+  score: number = 0;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/sensor-enrichment-config.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/sensor-enrichment-config.ts b/metron-interface/metron-config/src/app/model/sensor-enrichment-config.ts
new file mode 100644
index 0000000..3a3ac8b
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/sensor-enrichment-config.ts
@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {ThreatTriageConfig} from './threat-triage-config';
+export class SensorEnrichmentConfig {
+  enrichment: EnrichmentConfig;
+  threatIntel: ThreatIntelConfig;
+  configuration: {};
+
+  constructor() {
+    this.enrichment = new EnrichmentConfig();
+    this.threatIntel = new ThreatIntelConfig();
+  }
+}
+
+export class EnrichmentConfig {
+  fieldMap = {};
+  fieldToTypeMap = {};
+  config = {};
+}
+
+export class ThreatIntelConfig {
+  fieldMap = {};
+  fieldToTypeMap = {};
+  config = {};
+  triageConfig: ThreatTriageConfig;
+  constructor() {
+    this.triageConfig = new ThreatTriageConfig();
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/sensor-enrichments.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/sensor-enrichments.ts b/metron-interface/metron-config/src/app/model/sensor-enrichments.ts
new file mode 100644
index 0000000..92f303f
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/sensor-enrichments.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {ThreatTriageConfig} from './threat-triage-config';
+export class SensorEnrichments {
+  sensorName: string;
+  fieldEnrichments: {};
+  fieldThreatIntels: {};
+  threatTriageConfig: ThreatTriageConfig;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/sensor-indexing-config.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/sensor-indexing-config.ts b/metron-interface/metron-config/src/app/model/sensor-indexing-config.ts
new file mode 100644
index 0000000..4b945de
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/sensor-indexing-config.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export class SensorIndexingConfig {
+  index: string;
+  batchSize: number = 1;
+  enabled: boolean = true;
+}
+
+export class IndexingConfigurations {
+  hdfs: SensorIndexingConfig = new SensorIndexingConfig();
+  elasticsearch: SensorIndexingConfig = new SensorIndexingConfig();
+  solr: SensorIndexingConfig = new SensorIndexingConfig();
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/sensor-parser-config-history.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/sensor-parser-config-history.ts b/metron-interface/metron-config/src/app/model/sensor-parser-config-history.ts
new file mode 100644
index 0000000..0aed6fe
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/sensor-parser-config-history.ts
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {SensorParserConfig} from './sensor-parser-config';
+export class SensorParserConfigHistory {
+  createdBy: string;
+  modifiedBy: string;
+  createdDate: string;
+  modifiedByDate: string;
+  config: SensorParserConfig;
+
+  constructor() {
+    this.config = new SensorParserConfig();
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/sensor-parser-config.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/sensor-parser-config.ts b/metron-interface/metron-config/src/app/model/sensor-parser-config.ts
new file mode 100644
index 0000000..c0d907d
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/sensor-parser-config.ts
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {FieldTransformer} from './field-transformer';
+export class SensorParserConfig {
+  parserClassName: string;
+  filterClassName: string;
+  sensorTopic: string;
+  writerClassName: string;
+  errorWriterClassName: string;
+  invalidWriterClassName: string;
+  parserConfig: {};
+  fieldTransformations: FieldTransformer[];
+
+  constructor() {
+    this.parserConfig = {};
+    this.fieldTransformations = [];
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/sensor-parser-context.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/sensor-parser-context.ts b/metron-interface/metron-config/src/app/model/sensor-parser-context.ts
new file mode 100644
index 0000000..29217fe
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/sensor-parser-context.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {SensorParserConfig} from './sensor-parser-config';
+export class SensorParserContext {
+  sampleData: any;
+  sensorParserConfig: SensorParserConfig;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/sensor-parser-info.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/sensor-parser-info.ts b/metron-interface/metron-config/src/app/model/sensor-parser-info.ts
new file mode 100644
index 0000000..330b59c
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/sensor-parser-info.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {SensorParserConfig} from './sensor-parser-config';
+export class SensorParserInfo {
+  createdBy: string;
+  modifiedBy: string;
+  createdDate: string;
+  modifiedByDate: string;
+  config: SensorParserConfig;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/sensor-parser-response.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/sensor-parser-response.ts b/metron-interface/metron-config/src/app/model/sensor-parser-response.ts
new file mode 100644
index 0000000..92af2d3
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/sensor-parser-response.ts
@@ -0,0 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export class SensorParserResponse {
+  status: string;
+  message: string;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/sensor-parser-status.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/sensor-parser-status.ts b/metron-interface/metron-config/src/app/model/sensor-parser-status.ts
new file mode 100644
index 0000000..c0cab0c
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/sensor-parser-status.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export class SensorParserStatus {
+  id: string;
+  name: string;
+  status: string;
+  latency: string;
+  throughput: string;
+  state: string;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/stellar-function-description.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/stellar-function-description.ts b/metron-interface/metron-config/src/app/model/stellar-function-description.ts
new file mode 100644
index 0000000..5dd7f9a
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/stellar-function-description.ts
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export class StellarFunctionDescription {
+  name: string;
+  description: string;
+  params: string[];
+  returns: string;
+
+  constructor(name: string,  description: string,  params: string[], returns?: string) {
+    this.name = name;
+    this.description = description;
+    this.params = params;
+    this.returns = returns;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/threat-triage-config.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/threat-triage-config.ts b/metron-interface/metron-config/src/app/model/threat-triage-config.ts
new file mode 100644
index 0000000..2dc7524
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/threat-triage-config.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {RiskLevelRule} from './risk-level-rule';
+export class ThreatTriageConfig {
+  riskLevelRules: RiskLevelRule[] = [];
+  aggregator: string = 'MAX';
+  aggregationConfig = {};
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/topology-response.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/topology-response.ts b/metron-interface/metron-config/src/app/model/topology-response.ts
new file mode 100644
index 0000000..97a5c84
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/topology-response.ts
@@ -0,0 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export class TopologyResponse {
+  status: string;
+  message: string;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/model/topology-status.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/model/topology-status.ts b/metron-interface/metron-config/src/app/model/topology-status.ts
new file mode 100644
index 0000000..a8fad1c
--- /dev/null
+++ b/metron-interface/metron-config/src/app/model/topology-status.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export class TopologyStatus {
+  id: string;
+  name: string;
+  status: string;
+  latency: number;
+  throughput: number;
+  emitted: number;
+  acked: number;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/navbar/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/navbar/index.ts b/metron-interface/metron-config/src/app/navbar/index.ts
new file mode 100644
index 0000000..ab58b3e
--- /dev/null
+++ b/metron-interface/metron-config/src/app/navbar/index.ts
@@ -0,0 +1,18 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export * from './navbar.component';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/navbar/navbar.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/navbar/navbar.component.scss b/metron-interface/metron-config/src/app/navbar/navbar.component.scss
new file mode 100644
index 0000000..5a0b0be
--- /dev/null
+++ b/metron-interface/metron-config/src/app/navbar/navbar.component.scss
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../_variables.scss";
+
+.logout {
+  padding-left: 10px;
+  font-family: Roboto-Regular;
+}
+
+.logout-link{
+  color: $form-button-border;
+  cursor: pointer;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/navbar/navbar.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/navbar/navbar.component.spec.ts b/metron-interface/metron-config/src/app/navbar/navbar.component.spec.ts
new file mode 100644
index 0000000..d0bd4e0
--- /dev/null
+++ b/metron-interface/metron-config/src/app/navbar/navbar.component.spec.ts
@@ -0,0 +1,48 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, TestBed, ComponentFixture} from '@angular/core/testing';
+import {NavbarComponent} from './navbar.component';
+import {AuthenticationService} from '../service/authentication.service';
+
+class MockAuthenticationService {
+}
+
+describe('NavbarComponent', () => {
+  let fixture: ComponentFixture<NavbarComponent>;
+  let navbarComponent: NavbarComponent;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ NavbarComponent],
+      providers: [
+        NavbarComponent,
+        {provide: AuthenticationService, useClass: MockAuthenticationService}
+      ]
+    });
+
+    fixture = TestBed.createComponent(NavbarComponent);
+    navbarComponent = fixture.componentInstance;
+
+  }));
+
+  it('can instantiate SampleDataComponent', async(() => {
+    expect(navbarComponent instanceof NavbarComponent).toBe(true);
+    fixture.destroy();
+  }));
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/navbar/navbar.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/navbar/navbar.component.ts b/metron-interface/metron-config/src/app/navbar/navbar.component.ts
new file mode 100644
index 0000000..45c15ef
--- /dev/null
+++ b/metron-interface/metron-config/src/app/navbar/navbar.component.ts
@@ -0,0 +1,31 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Component} from '@angular/core';
+import {AuthenticationService} from '../service/authentication.service';
+
+@Component({
+    selector: 'metron-config-navbar',
+    templateUrl: 'navbar.html',
+    styleUrls: ['navbar.component.scss']
+})
+
+export class NavbarComponent {
+
+  constructor(private authenticationService: AuthenticationService) {
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/navbar/navbar.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/navbar/navbar.html b/metron-interface/metron-config/src/app/navbar/navbar.html
new file mode 100644
index 0000000..3af1c7c
--- /dev/null
+++ b/metron-interface/metron-config/src/app/navbar/navbar.html
@@ -0,0 +1,29 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<nav class="metron-nav navbar navbar-dark">
+  <button class="navbar-toggler hidden-sm-up" type="button" data-toggle="collapse" data-target="#exCollapsingNavbar">
+    &#9776;
+  </button>
+  <div class="collapse navbar-toggleable-xs pull-left" id="exCollapsingNavbar">
+    <a class="navbar-brand" href="#"><img src="assets/images/logo.png" alt="Logo" width=110></a>
+  </div>
+
+  <div class="form-inline pull-right">
+    <i class="fa fa-user " style="padding-left: 11px" aria-hidden="true"></i>
+    <div *ngIf="authenticationService.currentUser != null" class="fa logout">Logged in as {{authenticationService.currentUser}} - <span class="logout-link" (click)="authenticationService.logout()">Logout</span></div>
+  </div>
+
+</nav>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/rxjs-operators.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/rxjs-operators.ts b/metron-interface/metron-config/src/app/rxjs-operators.ts
new file mode 100644
index 0000000..11c86fb
--- /dev/null
+++ b/metron-interface/metron-config/src/app/rxjs-operators.ts
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// import 'rxjs/Rx'; // adds ALL RxJS statics & operators to Observable
+
+// See node_module/rxjs/Rxjs.js
+// Import just the rxjs statics and operators we need for THIS app.
+
+// Statics
+import 'rxjs/add/observable/throw';
+
+// Operators
+import 'rxjs/add/operator/catch';
+import 'rxjs/add/operator/debounceTime';
+import 'rxjs/add/operator/distinctUntilChanged';
+import 'rxjs/add/operator/map';
+import 'rxjs/add/operator/switchMap';
+import 'rxjs/add/operator/toPromise';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-field-schema/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-field-schema/index.ts b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/index.ts
new file mode 100644
index 0000000..621e31a
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/index.ts
@@ -0,0 +1,18 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export * from './sensor-field-schema.component';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.html b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.html
new file mode 100644
index 0000000..18b4b82
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.html
@@ -0,0 +1,113 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+  <div class="metron-slider-pane-edit fill load-left-to-right dialog2x">
+
+  <div class="form-title">Schema</div>
+  <i class="fa fa-times pull-right close-button" aria-hidden="true" (click)="onCancel()"></i>
+
+
+  <form role="form" class="fieldschema-form">
+      <metron-config-sample-data [topic]="sensorParserConfig.sensorTopic" (onSampleDataChanged)="onSampleDataChanged($event)" (onSampleDataNotAvailable)="onSampleDataNotAvailable($event)"></metron-config-sample-data>
+
+      <br><br>
+
+      <div class="metron-bg-inverse">
+        <div class="p-2">
+
+          <div class="row placeholder" *ngIf="fieldSchemaRows.length===0 || (parserResult === {})">
+            A data sample is need to view/configure the complete schema.
+          </div>
+
+          <div class="row mx-0" *ngIf="fieldSchemaRows.length>0">
+            <div class="col-md-3 title text-grey"> Field </div>
+            <div class="col-md-9 title text-grey"> Changes </div>
+          </div>
+
+          <div *ngFor="let fieldSchemaRow of fieldSchemaRows" class="row pb-1 mx-0">
+              <!-- Readonly Field Schema Row -->
+              <div class="field-schema-row" *ngIf="!fieldSchemaRow.showConfig" [class.field-suppressed]="fieldSchemaRow.isRemoved">
+                <div class="col-md-3 field-schema-cell-title"> {{ fieldSchemaRow.outputFieldName }} </div>
+                <div class="col-md-7 field-schema-cell-value" [innerHtml]="getChanges(fieldSchemaRow)"> </div>
+                <div class="col-md-2 field-schema-cell icon-blue" style="font-size: 16px">
+                  <i *ngIf="!fieldSchemaRow.isParserGenerated" class="fa fa-trash pull-right" aria-hidden="true" (click)="onDelete(fieldSchemaRow)"></i>
+                  <i class="fa fa-pencil pull-right" aria-hidden="true" (click)="fieldSchemaRow.showConfig=true"></i>
+                  <i *ngIf="!fieldSchemaRow.isRemoved" class="fa fa-ban pull-right" aria-hidden="true" (click)="onRemove(fieldSchemaRow)"></i>
+                  <i *ngIf="fieldSchemaRow.isRemoved" class="fa fa-check-circle-o pull-right" style="color:lightgreen" aria-hidden="true" (click)="onEnable(fieldSchemaRow)"></i>
+
+                </div>
+              </div>
+
+              <!-- Editable Enrichments Pane -->
+              <div class="config container" *ngIf="fieldSchemaRow.showConfig">
+
+                <!--Title-->
+                <div class="row py-1">
+                  <div class="col-md-10 enrichments-edit-title"> {{ fieldSchemaRow.outputFieldName }} </div>
+                  <div class="col-md-2  enrichment-cell">
+                    <i class="fa fa-times-circle pull-right small-close-button" aria-hidden="true" (click)="onCancelChange(fieldSchemaRow)"></i>
+                  </div>
+                </div>
+
+                <!--Input Field Name-->
+                <div class="form-group  px-1 col-md-10" *ngIf="fieldSchemaRow.isNew">
+                  <label>INPUT FIELD</label>
+                  <select [(ngModel)]="fieldSchemaRow.inputFieldName" [ngModelOptions]="{standalone: true}" (ngModelChange)="fieldSchemaRow.outputFieldName=fieldSchemaRow.inputFieldName+'_copy'">
+                    <option [disabled] [selected]>  </option>
+                    <option *ngFor="let fieldSchemaRow of fieldSchemaRows" [attr.hidden]="fieldSchemaRow.isNew"> {{ fieldSchemaRow.inputFieldName }} </option>
+                  </select>
+                </div>
+
+                <!-- Output Field Name-->
+                <div class="form-group  px-1 col-md-10" *ngIf="fieldSchemaRow.isNew  || !fieldSchemaRow.isParserGenerated">
+                  <label>NAME</label>
+                  <input type="text" class="form-control" [name]="fieldSchemaRow.outputFieldName" [(ngModel)]="fieldSchemaRow.outputFieldName" [ngModelOptions]="{standalone: true}">
+                </div>
+
+                <!-- Transforms -->
+                <div class="form-group  px-1 col-md-12">
+                  <label>TRANSFORMATIONS</label>
+                  <metron-config-multiple-input [type]="'select'" [availableItems]="transformOptions" [(configuredItems)]="fieldSchemaRow.transformConfigured" (onConfigChange)="onTransformsChange(fieldSchemaRow)"> </metron-config-multiple-input>
+                  <div class="edit-pane-readonly rounded col-md-10"> {{ fieldSchemaRow.preview }} </div>
+                </div>
+
+                <!-- Enrichmnets -->
+                <div class="form-group  px-1 col-md-12">
+                  <label>ENRICHMENTS</label>
+                  <metron-config-multiple-input [availableItems]="enrichmentOptions" [(configuredItems)]="fieldSchemaRow.enrichmentConfigured" [allowDuplicates]="false"> </metron-config-multiple-input>
+                </div>
+
+                <!-- Threat Intel -->
+                <div class="form-group  px-1 col-md-12">
+                  <label>THREAT INTEL </label>
+                  <metron-config-multiple-input [availableItems]="threatIntelOptions" [(configuredItems)]="fieldSchemaRow.threatIntelConfigured" [allowDuplicates]="false"> </metron-config-multiple-input>
+                </div>
+
+                <div class="form-group  p-1 col-md-12">
+                  <button type="submit" class="btn form-enable-disable-button pull-left" (click)="onSaveChange(fieldSchemaRow)">SAVE</button>
+                </div>
+
+              </div>
+          </div>
+
+          <div class="row mx-0 add-button" *ngIf="fieldSchemaRows.length > 0">
+            <button type="submit" class="btn form-enable-disable-button" (click)="addNewRule()"><i class="fa fa-plus" aria-hidden="true"></i></button>
+          </div>
+
+        </div>
+      </div>
+
+  </form>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.scss b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.scss
new file mode 100644
index 0000000..f248a70
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.scss
@@ -0,0 +1,168 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../../_variables.scss";
+
+.form-title
+{
+  padding-left: 25px;
+}
+
+.title
+{
+  font-size: 12px;
+}
+
+.close-button
+{
+  padding-right: 20px;
+}
+
+.field-schema-row
+{
+  display: table;
+  width: 100%;
+  padding: 10px 0px 10px 0px;
+  background: #383838;
+}
+
+.field-schema-edit-title
+{
+  font-size: 15px;
+  font-weight: bold;
+}
+
+.field-schema-cell
+{
+  float: none;
+  font-size: 13px;
+  height: 15px;
+  line-height: 15px;
+  display: table-cell;
+  vertical-align: middle;
+  word-break: break-all;
+  font-family: Roboto-Regular;
+}
+
+.field-schema-cell-title
+{
+  @extend .field-schema-cell;
+  color: $silver-color;
+  font-size: 14px;
+}
+
+.field-schema-cell-value
+{
+  @extend .field-schema-cell;
+  font-size: 13px;
+}
+
+.small-close-button {
+  font-size:20px;
+  color:$silver-color;
+  cursor: pointer;
+}
+
+.config
+{
+  background: $edit-background;
+  border: 1px solid $edit-background-border;
+}
+
+.enrichment-cell
+{
+  display: table-cell;
+  vertical-align: middle;
+  float: none;
+  min-height: 35px;
+  line-height: 20px;
+}
+
+.enrichments-edit-title
+{
+  font-size: 15px;
+  font-weight: bold;
+}
+
+.transform-info-row
+{
+  display: table;
+  width: 100%;
+  min-height: 35px;
+}
+
+.edit-pane-readonly
+{
+  background: $edit-child-background;
+  font-size: 12px;
+  margin-bottom: 0.5em;
+  font-style: italic;
+  padding: 0.5rem 0.75rem;
+  border: none;
+  line-height: 1.5em;
+  height: 35px;
+}
+
+.field-suppressed
+{
+  background-color: #262626;
+  color: #737373;
+  .field-schema-cell-title
+  {
+    color: #737373;
+  }
+}
+
+.metron-bg-inverse
+{
+  background: #2e2e2e;
+  margin-bottom: 70px;
+}
+
+select
+{
+  width: 100%;
+  display: block;
+}
+
+.placeholder
+{
+  text-align: center;
+  font-size: 13px;
+  color: $text-color-white;
+}
+
+.add-button
+{
+  button
+  {
+    width: 100%;
+    height: 35px;
+    padding: 0px;
+  }
+  i
+  {
+    font-size: 1.5em;
+    color: $field-button-color;
+  }
+}
+
+.fieldschema-form
+{
+  padding-left: 25px;
+  padding-right: 20px;
+}


[04/12] incubator-metron git commit: METRON-623 Management UI [contributed by Raghu Mitra Kandikonda and Ryan Merriman] closes apache/incubator-metron#489

Posted by rm...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.ts b/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.ts
new file mode 100644
index 0000000..958b27d
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.ts
@@ -0,0 +1,58 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, Inject} from '@angular/core';
+import {Http, Headers, RequestOptions, Response} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {IndexingConfigurations} from '../model/sensor-indexing-config';
+import {HttpUtil} from '../util/httpUtil';
+import {IAppConfig} from '../app.config.interface';
+import {APP_CONFIG} from '../app.config';
+
+@Injectable()
+export class SensorIndexingConfigService {
+  url = this.config.apiEndpoint + '/sensor/indexing/config';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+
+  constructor(private http: Http, @Inject(APP_CONFIG) private config: IAppConfig) {
+  }
+
+  public post(name: string, sensorIndexingConfig: IndexingConfigurations): Observable<IndexingConfigurations> {
+    return this.http.post(this.url + '/' + name, JSON.stringify(sensorIndexingConfig),
+                          new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public get(name: string): Observable<IndexingConfigurations> {
+    return this.http.get(this.url + '/' + name, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public getAll(): Observable<IndexingConfigurations[]> {
+    return this.http.get(this.url, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public deleteSensorIndexingConfig(name: string): Observable<Response> {
+    return this.http.delete(this.url + '/' + name, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .catch(HttpUtil.handleError);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/sensor-parser-config-history.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/sensor-parser-config-history.service.spec.ts b/metron-interface/metron-config/src/app/service/sensor-parser-config-history.service.spec.ts
new file mode 100644
index 0000000..3da4065
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/sensor-parser-config-history.service.spec.ts
@@ -0,0 +1,97 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {SensorParserConfig} from '../model/sensor-parser-config';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {METRON_REST_CONFIG, APP_CONFIG} from '../app.config';
+import {SensorParserConfigHistoryService} from './sensor-parser-config-history.service';
+import {IAppConfig} from '../app.config.interface';
+import {SensorParserConfigHistory} from '../model/sensor-parser-config-history';
+
+describe('SensorParserConfigHistoryService', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpModule],
+      providers: [
+        SensorParserConfigHistoryService,
+        {provide: XHRBackend, useClass: MockBackend},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    })
+        .compileComponents();
+  }));
+
+  it('can instantiate service when inject service',
+      inject([SensorParserConfigHistoryService], (service: SensorParserConfigHistoryService) => {
+        expect(service instanceof SensorParserConfigHistoryService).toBe(true);
+      }));
+
+  it('can instantiate service with "new"', inject([Http, APP_CONFIG], (http: Http, config: IAppConfig) => {
+    expect(http).not.toBeNull('http should be provided');
+    let service = new SensorParserConfigHistoryService(http, config);
+    expect(service instanceof SensorParserConfigHistoryService).toBe(true, 'new service should be ok');
+  }));
+
+
+  it('can provide the mockBackend as XHRBackend',
+      inject([XHRBackend], (backend: MockBackend) => {
+        expect(backend).not.toBeNull('backend should be provided');
+      }));
+
+  describe('when service functions', () => {
+    let sensorParserConfigHistoryService: SensorParserConfigHistoryService;
+    let mockBackend: MockBackend;
+    let sensorParserConfigHistory = new SensorParserConfigHistory();
+    let sensorParserConfig = new SensorParserConfig();
+    sensorParserConfig.sensorTopic = 'bro';
+    sensorParserConfigHistory.config = sensorParserConfig;
+    let sensorParserConfigHistoryResponse: Response;
+    let allSensorParserConfigHistoryResponse: Response;
+
+    beforeEach(inject([Http, XHRBackend, APP_CONFIG], (http: Http, be: MockBackend, config: IAppConfig) => {
+      mockBackend = be;
+      sensorParserConfigHistoryService = new SensorParserConfigHistoryService(http, config);
+      sensorParserConfigHistoryResponse = new Response(new ResponseOptions({status: 200, body: sensorParserConfig}));
+      allSensorParserConfigHistoryResponse = new Response(new ResponseOptions({status: 200, body: [sensorParserConfig]}));
+    }));
+
+    it('get', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sensorParserConfigHistoryResponse));
+
+      sensorParserConfigHistoryService.get('bro').subscribe(
+          result => {
+            expect(result).toEqual(sensorParserConfigHistory);
+          }, error => console.log(error));
+    })));
+
+    it('getAll', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(allSensorParserConfigHistoryResponse));
+
+      sensorParserConfigHistoryService.getAll().subscribe(
+          result => {
+            expect(result).toEqual([sensorParserConfigHistory]);
+          }, error => console.log(error));
+    })));
+  });
+
+});
+
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/sensor-parser-config-history.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/sensor-parser-config-history.service.ts b/metron-interface/metron-config/src/app/service/sensor-parser-config-history.service.ts
new file mode 100644
index 0000000..bd5cbc5
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/sensor-parser-config-history.service.ts
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, Inject} from '@angular/core';
+import {Http, Headers, RequestOptions, Response} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {HttpUtil} from '../util/httpUtil';
+import {IAppConfig} from '../app.config.interface';
+import {SensorParserConfigHistory} from '../model/sensor-parser-config-history';
+import {APP_CONFIG} from '../app.config';
+import {SensorParserConfig} from '../model/sensor-parser-config';
+
+@Injectable()
+export class SensorParserConfigHistoryService {
+  url =  this.config.apiEndpoint + '/sensor/parser/config';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+
+  constructor(private http: Http, @Inject(APP_CONFIG) private config: IAppConfig) {
+
+  }
+
+  public get(name: string): Observable<SensorParserConfigHistory> {
+    return this.http.get(this.url + '/' + name, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map((response: Response) => {
+        let sensorParserConfigHistory = new SensorParserConfigHistory();
+        sensorParserConfigHistory.config = response.json();
+        return sensorParserConfigHistory;
+      })
+      .catch(HttpUtil.handleError);
+  }
+
+  public getAll(): Observable<SensorParserConfigHistory[]> {
+    return this.http.get(this.url, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map((response: Response) => {
+        let sensorParserConfigHistoryArray = [];
+        let sensorParserConfigs: SensorParserConfig[] = response.json();
+        for (let sensorParserConfig of sensorParserConfigs) {
+          let sensorParserConfigHistory = new SensorParserConfigHistory();
+          sensorParserConfigHistory.config = sensorParserConfig;
+          sensorParserConfigHistoryArray.push(sensorParserConfigHistory);
+        }
+        return sensorParserConfigHistoryArray;
+      })
+      .catch(HttpUtil.handleError);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/sensor-parser-config.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/sensor-parser-config.service.spec.ts b/metron-interface/metron-config/src/app/service/sensor-parser-config.service.spec.ts
new file mode 100644
index 0000000..bae4656
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/sensor-parser-config.service.spec.ts
@@ -0,0 +1,149 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {SensorParserConfigService} from './sensor-parser-config.service';
+import {SensorParserConfig} from '../model/sensor-parser-config';
+import {ParseMessageRequest} from '../model/parse-message-request';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+
+describe('SensorParserConfigService', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpModule],
+      providers: [
+        SensorParserConfigService,
+        {provide: XHRBackend, useClass: MockBackend},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    })
+      .compileComponents();
+  }));
+
+  it('can instantiate service when inject service',
+    inject([SensorParserConfigService], (service: SensorParserConfigService) => {
+      expect(service instanceof SensorParserConfigService).toBe(true);
+    }));
+
+  it('can instantiate service with "new"', inject([Http, APP_CONFIG], (http: Http, config: IAppConfig) => {
+    expect(http).not.toBeNull('http should be provided');
+    let service = new SensorParserConfigService(http, config);
+    expect(service instanceof SensorParserConfigService).toBe(true, 'new service should be ok');
+  }));
+
+
+  it('can provide the mockBackend as XHRBackend',
+    inject([XHRBackend], (backend: MockBackend) => {
+      expect(backend).not.toBeNull('backend should be provided');
+    }));
+
+  describe('when service functions', () => {
+    let sensorParserConfigService: SensorParserConfigService;
+    let mockBackend: MockBackend;
+    let sensorParserConfig = new SensorParserConfig();
+    sensorParserConfig.sensorTopic = 'bro';
+    sensorParserConfig.parserClassName = 'parserClass';
+    sensorParserConfig.parserConfig = {field: 'value'};
+    let availableParsers = [{ 'Grok': 'org.apache.metron.parsers.GrokParser'}];
+    let parseMessageRequest = new ParseMessageRequest();
+    parseMessageRequest.sensorParserConfig = new SensorParserConfig();
+    parseMessageRequest.sensorParserConfig.sensorTopic = 'bro';
+    parseMessageRequest.sampleData = 'sampleData';
+    let parsedMessage = { 'field': 'value'};
+    let sensorParserConfig1 = new SensorParserConfig();
+    sensorParserConfig1.sensorTopic = 'bro1';
+    let sensorParserConfig2 = new SensorParserConfig();
+    sensorParserConfig2.sensorTopic = 'bro2';
+    let deleteResult = {success: ['bro1', 'bro2']};
+    let sensorParserConfigResponse: Response;
+    let sensorParserConfigsResponse: Response;
+    let availableParserResponse: Response;
+    let parseMessageResponse: Response;
+    let deleteResponse: Response;
+
+    beforeEach(inject([Http, XHRBackend, APP_CONFIG], (http: Http, be: MockBackend, config: IAppConfig) => {
+      mockBackend = be;
+      sensorParserConfigService = new SensorParserConfigService(http, config);
+      sensorParserConfigResponse = new Response(new ResponseOptions({status: 200, body: sensorParserConfig}));
+      sensorParserConfigsResponse = new Response(new ResponseOptions({status: 200, body: [sensorParserConfig]}));
+      availableParserResponse = new Response(new ResponseOptions({status: 200, body: availableParsers}));
+      parseMessageResponse = new Response(new ResponseOptions({status: 200, body: parsedMessage}));
+      deleteResponse = new Response(new ResponseOptions({status: 200, body: deleteResult}));
+    }));
+
+    it('post', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sensorParserConfigResponse));
+
+      sensorParserConfigService.post(sensorParserConfig).subscribe(
+      result => {
+        expect(result).toEqual(sensorParserConfig);
+      }, error => console.log(error));
+    })));
+
+    it('get', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sensorParserConfigResponse));
+
+      sensorParserConfigService.get('bro').subscribe(
+        result => {
+          expect(result).toEqual(sensorParserConfig);
+        }, error => console.log(error));
+    })));
+
+    it('getAll', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sensorParserConfigsResponse));
+
+      sensorParserConfigService.getAll().subscribe(
+        results => {
+          expect(results).toEqual([sensorParserConfig]);
+        }, error => console.log(error));
+    })));
+
+    it('getAvailableParsers', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(availableParserResponse));
+
+      sensorParserConfigService.getAvailableParsers().subscribe(
+        results => {
+          expect(results).toEqual(availableParsers);
+        }, error => console.log(error));
+    })));
+
+    it('parseMessage', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(parseMessageResponse));
+
+      sensorParserConfigService.parseMessage(parseMessageRequest).subscribe(
+        results => {
+          expect(results).toEqual(parsedMessage);
+        }, error => console.log(error));
+    })));
+
+    it('deleteSensorParserConfigs', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(deleteResponse));
+
+      sensorParserConfigService.deleteSensorParserConfigs([sensorParserConfig1, sensorParserConfig2]).subscribe(result => {
+        expect(result.success.length).toEqual(2);
+      });
+    })));
+  });
+
+});
+
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/sensor-parser-config.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/sensor-parser-config.service.ts b/metron-interface/metron-config/src/app/service/sensor-parser-config.service.ts
new file mode 100644
index 0000000..25cd833
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/sensor-parser-config.service.ts
@@ -0,0 +1,116 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, Inject}     from '@angular/core';
+import {Http, Headers, RequestOptions, Response} from '@angular/http';
+import {Observable}     from 'rxjs/Observable';
+import {SensorParserConfig} from '../model/sensor-parser-config';
+import {HttpUtil} from '../util/httpUtil';
+import {Subject}    from 'rxjs/Subject';
+import {ParseMessageRequest} from '../model/parse-message-request';
+import {IAppConfig} from '../app.config.interface';
+import {APP_CONFIG} from '../app.config';
+
+@Injectable()
+export class SensorParserConfigService {
+  url = this.config.apiEndpoint + '/sensor/parser/config';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+  selectedSensorParserConfig: SensorParserConfig;
+
+  dataChangedSource = new Subject<SensorParserConfig[]>();
+  dataChanged$ = this.dataChangedSource.asObservable();
+
+  constructor(private http: Http, @Inject(APP_CONFIG) private config: IAppConfig) {
+
+  }
+
+  public post(sensorParserConfig: SensorParserConfig): Observable<SensorParserConfig> {
+    return this.http.post(this.url, JSON.stringify(sensorParserConfig), new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public get(name: string): Observable<SensorParserConfig> {
+    return this.http.get(this.url + '/' + name, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public getAll(): Observable<SensorParserConfig[]> {
+    return this.http.get(this.url, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public deleteSensorParserConfig(name: string): Observable<Response> {
+    return this.http.delete(this.url + '/' + name, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .catch(HttpUtil.handleError);
+  }
+
+  public getAvailableParsers(): Observable<{}> {
+    return this.http.get(this.url + '/list/available', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public parseMessage(parseMessageRequest: ParseMessageRequest): Observable<{}> {
+    return this.http.post(this.url + '/parseMessage', parseMessageRequest, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public deleteSensorParserConfigs(sensors: SensorParserConfig[]): Observable<{success: Array<string>, failure: Array<string>}> {
+    let result: {success: Array<string>, failure: Array<string>} = {success: [], failure: []};
+    let observable = Observable.create((observer => {
+
+      let completed = () => {
+        if (observer) {
+          observer.next(result);
+          observer.complete();
+        }
+
+        this.dataChangedSource.next(sensors);
+      };
+
+      for (let i = 0; i < sensors.length; i++) {
+        this.deleteSensorParserConfig(sensors[i].sensorTopic).subscribe(results => {
+          result.success.push(sensors[i].sensorTopic);
+          if (result.success.length + result.failure.length === sensors.length) {
+            completed();
+          }
+        }, error => {
+          result.failure.push(sensors[i].sensorTopic);
+          if (result.success.length + result.failure.length === sensors.length) {
+            completed();
+          }
+        });
+      }
+
+    }));
+
+    return observable;
+  }
+
+  public setSeletedSensor(sensor: SensorParserConfig): void {
+    this.selectedSensorParserConfig = sensor;
+  }
+
+  public getSelectedSensor(): SensorParserConfig {
+    return this.selectedSensorParserConfig;
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/stellar.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/stellar.service.spec.ts b/metron-interface/metron-config/src/app/service/stellar.service.spec.ts
new file mode 100644
index 0000000..163eefb
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/stellar.service.spec.ts
@@ -0,0 +1,142 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {StellarService} from './stellar.service';
+import {SensorParserContext} from '../model/sensor-parser-context';
+import {SensorParserConfig} from '../model/sensor-parser-config';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+
+describe('StellarService', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpModule],
+      providers: [
+        StellarService,
+        {provide: XHRBackend, useClass: MockBackend},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    })
+      .compileComponents();
+  }));
+
+  it('can instantiate service when inject service',
+    inject([StellarService], (service: StellarService) => {
+      expect(service instanceof StellarService).toBe(true);
+    }));
+
+  it('can instantiate service with "new"', inject([Http, APP_CONFIG], (http: Http, config: IAppConfig) => {
+    expect(http).not.toBeNull('http should be provided');
+    let service = new StellarService(http, config);
+    expect(service instanceof StellarService).toBe(true, 'new service should be ok');
+  }));
+
+
+  it('can provide the mockBackend as XHRBackend',
+    inject([XHRBackend], (backend: MockBackend) => {
+      expect(backend).not.toBeNull('backend should be provided');
+    }));
+
+  describe('when service functions', () => {
+    let transformationValidationService: StellarService;
+    let mockBackend: MockBackend;
+    let transformationRules = ['rule1', 'rule2'];
+    let transformationRulesValidation = {rule1: true, rule2: false};
+    let transformationValidation = new SensorParserContext();
+    transformationValidation.sampleData = {'data': 'data'};
+    transformationValidation.sensorParserConfig = new SensorParserConfig();
+    transformationValidation.sensorParserConfig.sensorTopic = 'test';
+    let transformations = ['STELLAR', 'REMOVE'];
+    let transformFunctions = [{'function1': 'desc1'}, {'function2': 'desc2'}];
+    let simpleTransformFunctions = [{'simplefunction1': 'simpledesc1'}, {'simplefunction2': 'simpledesc2'}];
+    let transformationRulesValidationResponse: Response;
+    let transformationValidationResponse: Response;
+    let transformationListResponse: Response;
+    let transformationListFunctionsResponse: Response;
+    let transformationListSimpleFunctionsResponse: Response;
+
+    beforeEach(inject([Http, XHRBackend, APP_CONFIG], (http: Http, be: MockBackend, config: IAppConfig) => {
+      mockBackend = be;
+      transformationValidationService = new StellarService(http, config);
+      transformationRulesValidationResponse = new Response(new ResponseOptions({
+        status: 200,
+        body: transformationRulesValidation
+      }));
+      transformationValidationResponse = new Response(new ResponseOptions({
+        status: 200,
+        body: transformationValidation
+      }));
+      transformationListResponse = new Response(new ResponseOptions({status: 200, body: transformations}));
+      transformationListFunctionsResponse = new Response(new ResponseOptions({status: 200, body: transformFunctions}));
+      transformationListSimpleFunctionsResponse = new Response(new ResponseOptions({status: 200, body: simpleTransformFunctions}));
+    }));
+
+    it('validateRules', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(transformationRulesValidationResponse));
+
+      transformationValidationService.validateRules(transformationRules).subscribe(
+        result => {
+          expect(result).toEqual(transformationRulesValidation);
+        }, error => console.log(error));
+    })));
+
+    it('validate', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(transformationValidationResponse));
+
+      transformationValidationService.validate(transformationValidation).subscribe(
+        result => {
+          expect(result).toEqual(transformationValidation);
+        }, error => console.log(error));
+    })));
+
+    it('list', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(transformationListResponse));
+
+      transformationValidationService.list().subscribe(
+        result => {
+          expect(result).toEqual(transformations);
+        }, error => console.log(error));
+    })));
+
+    it('listFunctions', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(transformationListFunctionsResponse));
+
+      transformationValidationService.listFunctions().subscribe(
+        result => {
+          expect(result).toEqual(transformFunctions);
+        }, error => console.log(error));
+    })));
+
+    it('listSimpleFunctions', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(transformationListSimpleFunctionsResponse));
+
+      transformationValidationService.listSimpleFunctions().subscribe(
+          result => {
+            expect(result).toEqual(simpleTransformFunctions);
+          }, error => console.log(error));
+    })));
+  });
+
+});
+
+
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/stellar.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/stellar.service.ts b/metron-interface/metron-config/src/app/service/stellar.service.ts
new file mode 100644
index 0000000..be04906
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/stellar.service.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, Inject} from '@angular/core';
+import {Http, Headers, RequestOptions} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {SensorParserContext} from '../model/sensor-parser-context';
+import {HttpUtil} from '../util/httpUtil';
+import {StellarFunctionDescription} from '../model/stellar-function-description';
+import {IAppConfig} from '../app.config.interface';
+import {APP_CONFIG} from '../app.config';
+
+@Injectable()
+export class StellarService {
+  url = this.config.apiEndpoint + '/stellar';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+
+  constructor(private http: Http, @Inject(APP_CONFIG) private config: IAppConfig) {
+
+  }
+
+  public validateRules(rules: string[]): Observable<{}> {
+    return this.http.post(this.url + '/validate/rules', JSON.stringify(rules),
+      new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public validate(transformationValidation: SensorParserContext): Observable<{}> {
+    return this.http.post(this.url + '/validate', JSON.stringify(transformationValidation),
+      new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public list(): Observable<string[]> {
+    return this.http.get(this.url + '/list', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public listFunctions(): Observable<StellarFunctionDescription[]> {
+    return this.http.get(this.url + '/list/functions', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public listSimpleFunctions(): Observable<StellarFunctionDescription[]> {
+    return this.http.get(this.url + '/list/simple/functions', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/storm.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/storm.service.spec.ts b/metron-interface/metron-config/src/app/service/storm.service.spec.ts
new file mode 100644
index 0000000..528c1c3
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/storm.service.spec.ts
@@ -0,0 +1,252 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {TopologyStatus} from '../model/topology-status';
+import {TopologyResponse} from '../model/topology-response';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+import {StormService} from './storm.service';
+
+describe('StormService', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpModule],
+      providers: [
+        StormService,
+        {provide: XHRBackend, useClass: MockBackend},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    })
+        .compileComponents();
+  }));
+
+  it('can instantiate service when inject service',
+      inject([StormService], (service: StormService) => {
+        expect(service instanceof StormService).toBe(true);
+      }));
+
+  it('can instantiate service with "new"', inject([Http, APP_CONFIG], (http: Http, config: IAppConfig) => {
+    expect(http).not.toBeNull('http should be provided');
+    let service = new StormService(http, config);
+    expect(service instanceof StormService).toBe(true, 'new service should be ok');
+  }));
+
+
+  it('can provide the mockBackend as XHRBackend',
+      inject([XHRBackend], (backend: MockBackend) => {
+        expect(backend).not.toBeNull('backend should be provided');
+      }));
+
+  describe('when service functions', () => {
+    let stormService: StormService;
+    let mockBackend: MockBackend;
+    let allStatuses: TopologyStatus[] = [];
+    let broStatus = new TopologyStatus();
+    broStatus.name = 'bro';
+    broStatus.id = 'broid';
+    broStatus.status = 'ACTIVE';
+    allStatuses.push(broStatus);
+    let enrichmentStatus = new TopologyStatus();
+    enrichmentStatus.name = 'enrichment';
+    enrichmentStatus.id = 'enrichmentid';
+    enrichmentStatus.status = 'ACTIVE';
+    allStatuses.push(enrichmentStatus);
+    let indexingStatus = new TopologyStatus();
+    indexingStatus.name = 'indexing';
+    indexingStatus.id = 'indexingid';
+    indexingStatus.status = 'ACTIVE';
+    allStatuses.push(indexingStatus);
+    let startMessage: TopologyResponse = {status: 'success', message: 'STARTED'};
+    let stopMessage: TopologyResponse = {status: 'success', message: 'STOPPED'};
+    let activateMessage: TopologyResponse = {status: 'success', message: 'ACTIVE'};
+    let deactivateMessage: TopologyResponse = {status: 'success', message: 'INACTIVE'};
+    let allStatusesResponse: Response;
+    let enrichmentStatusResponse: Response;
+    let indexingStatusResponse: Response;
+    let broStatusResponse: Response;
+    let startResponse: Response;
+    let stopResponse: Response;
+    let activateResponse: Response;
+    let deactivateResponse: Response;
+
+    beforeEach(inject([Http, XHRBackend, APP_CONFIG], (http: Http, be: MockBackend, config: IAppConfig) => {
+      mockBackend = be;
+      stormService = new StormService(http, config);
+      allStatusesResponse = new Response(new ResponseOptions({status: 200, body: allStatuses}));
+      enrichmentStatusResponse = new Response(new ResponseOptions({status: 200, body: enrichmentStatus}));
+      indexingStatusResponse = new Response(new ResponseOptions({status: 200, body: indexingStatus}));
+      broStatusResponse = new Response(new ResponseOptions({status: 200, body: broStatus}));
+      startResponse = new Response(new ResponseOptions({status: 200, body: startMessage}));
+      stopResponse = new Response(new ResponseOptions({status: 200, body: stopMessage}));
+      activateResponse = new Response(new ResponseOptions({status: 200, body: activateMessage}));
+      deactivateResponse = new Response(new ResponseOptions({status: 200, body: deactivateMessage}));
+    }));
+
+    it('getAll', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(allStatusesResponse));
+
+      stormService.getAll().subscribe(
+          result => {
+            expect(result).toEqual(allStatuses);
+          }, error => console.log(error));
+    })));
+
+    it('getEnrichmentStatus', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(enrichmentStatusResponse));
+
+      stormService.getEnrichmentStatus().subscribe(
+          result => {
+            expect(result).toEqual(enrichmentStatus);
+          }, error => console.log(error));
+    })));
+
+    it('activateEnrichment', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(activateResponse));
+
+      stormService.activateEnrichment().subscribe(
+          result => {
+            expect(result).toEqual(activateMessage);
+          }, error => console.log(error));
+    })));
+
+    it('deactivateEnrichment', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(deactivateResponse));
+
+      stormService.deactivateEnrichment().subscribe(
+          result => {
+            expect(result).toEqual(deactivateMessage);
+          }, error => console.log(error));
+    })));
+
+    it('startEnrichment', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(startResponse));
+
+      stormService.startEnrichment().subscribe(
+          result => {
+            expect(result).toEqual(startMessage);
+          }, error => console.log(error));
+    })));
+
+    it('stopEnrichment', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(stopResponse));
+
+      stormService.stopEnrichment().subscribe(
+          result => {
+            expect(result).toEqual(stopMessage);
+          }, error => console.log(error));
+    })));
+
+    it('getIndexingStatus', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(indexingStatusResponse));
+
+      stormService.getIndexingStatus().subscribe(
+          result => {
+            expect(result).toEqual(indexingStatus);
+          }, error => console.log(error));
+    })));
+
+    it('activateIndexing', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(activateResponse));
+
+      stormService.activateIndexing().subscribe(
+          result => {
+            expect(result).toEqual(activateMessage);
+          }, error => console.log(error));
+    })));
+
+    it('deactivateIndexing', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(deactivateResponse));
+
+      stormService.deactivateIndexing().subscribe(
+          result => {
+            expect(result).toEqual(deactivateMessage);
+          }, error => console.log(error));
+    })));
+
+    it('startIndexing', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(startResponse));
+
+      stormService.startIndexing().subscribe(
+          result => {
+            expect(result).toEqual(startMessage);
+          }, error => console.log(error));
+    })));
+
+    it('stopIndexing', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(stopResponse));
+
+      stormService.stopIndexing().subscribe(
+          result => {
+            expect(result).toEqual(stopMessage);
+          }, error => console.log(error));
+    })));
+
+    it('getStatus', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(broStatusResponse));
+
+      stormService.getStatus('bro').subscribe(
+          result => {
+            expect(result).toEqual(broStatus);
+          }, error => console.log(error));
+    })));
+
+    it('activateParser', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(activateResponse));
+
+      stormService.activateParser('bro').subscribe(
+          result => {
+            expect(result).toEqual(activateMessage);
+          }, error => console.log(error));
+    })));
+
+    it('deactivateParser', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(deactivateResponse));
+
+      stormService.deactivateParser('bro').subscribe(
+          result => {
+            expect(result).toEqual(deactivateMessage);
+          }, error => console.log(error));
+    })));
+
+    it('startParser', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(startResponse));
+
+      stormService.startParser('bro').subscribe(
+          result => {
+            expect(result).toEqual(startMessage);
+          }, error => console.log(error));
+    })));
+
+    it('stopParser', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(stopResponse));
+
+      stormService.stopParser('bro').subscribe(
+          result => {
+            expect(result).toEqual(stopMessage);
+          }, error => console.log(error));
+    })));
+
+
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/storm.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/storm.service.ts b/metron-interface/metron-config/src/app/service/storm.service.ts
new file mode 100644
index 0000000..7a79e1f
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/storm.service.ts
@@ -0,0 +1,144 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, Inject} from '@angular/core';
+import {Http, Headers, RequestOptions} from '@angular/http';
+import {HttpUtil} from '../util/httpUtil';
+import {TopologyStatus} from '../model/topology-status';
+import {TopologyResponse} from '../model/topology-response';
+import {APP_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+import {Observable} from 'rxjs/Observable';
+import 'rxjs/add/observable/interval';
+import 'rxjs/add/operator/switchMap';
+import 'rxjs/add/operator/onErrorResumeNext';
+
+@Injectable()
+export class StormService {
+  url = this.config.apiEndpoint + '/storm';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+
+  constructor(private http: Http, @Inject(APP_CONFIG) private config: IAppConfig) {
+
+  }
+
+  public pollGetAll(): Observable<TopologyStatus[]> {
+    return Observable.interval(8000).switchMap(() => {
+      return this.http.get(this.url, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+          .map(HttpUtil.extractData)
+          .catch(HttpUtil.handleError)
+          .onErrorResumeNext();
+    });
+  }
+
+  public getAll(): Observable<TopologyStatus[]> {
+    return this.http.get(this.url, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public getEnrichmentStatus(): Observable<TopologyStatus> {
+    return this.http.get(this.url + '/enrichment', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public activateEnrichment(): Observable<TopologyResponse> {
+    return this.http.get(this.url + '/enrichment/activate', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public deactivateEnrichment(): Observable<TopologyResponse> {
+    return this.http.get(this.url + '/enrichment/deactivate', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public startEnrichment(): Observable<TopologyResponse> {
+    return this.http.get(this.url + '/enrichment/start', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public stopEnrichment(): Observable<TopologyResponse> {
+    return this.http.get(this.url + '/enrichment/stop', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public getIndexingStatus(): Observable<TopologyStatus> {
+    return this.http.get(this.url + '/indexing', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public activateIndexing(): Observable<TopologyResponse> {
+    return this.http.get(this.url + '/indexing/activate', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public deactivateIndexing(): Observable<TopologyResponse> {
+    return this.http.get(this.url + '/indexing/deactivate', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public startIndexing(): Observable<TopologyResponse> {
+    return this.http.get(this.url + '/indexing/start', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public stopIndexing(): Observable<TopologyResponse> {
+    return this.http.get(this.url + '/indexing/stop', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public getStatus(name: string): Observable<TopologyStatus> {
+    return this.http.get(this.url + '/' + name, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public activateParser(name: string): Observable<TopologyResponse> {
+    return this.http.get(this.url + '/parser/activate/' + name, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public deactivateParser(name: string): Observable<TopologyResponse> {
+    return this.http.get(this.url + '/parser/deactivate/' + name, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public startParser(name: string): Observable<TopologyResponse> {
+    return this.http.get(this.url + '/parser/start/' + name, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public stopParser(name: string): Observable<TopologyResponse> {
+    return this.http.get(this.url + '/parser/stop/' + name, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.html b/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.html
new file mode 100644
index 0000000..72489ed
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.html
@@ -0,0 +1,16 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<div #aceEditor class="editor"></div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.scss b/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.scss
new file mode 100644
index 0000000..3dbb13d
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.scss
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../../_variables.scss";
+.editor {
+  border-radius: 4px;
+  border: 1px solid #4d4d4d;
+}
+* /deep/ {
+  .ace_emptyMessage
+  {
+    @include place-holder-text;
+  }
+
+  .ace_gutter {
+    background: #272822 !important;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.spec.ts b/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.spec.ts
new file mode 100644
index 0000000..89f6205
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.spec.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AceEditorComponent } from './ace-editor.component';
+
+describe('Component: AceEditor', () => {
+  it('should create an instance', () => {
+    let component = new AceEditorComponent();
+    expect(component).toBeTruthy();
+  });
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.ts b/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.ts
new file mode 100644
index 0000000..d5edd83
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.ts
@@ -0,0 +1,182 @@
+import { Component, AfterViewInit, ViewChild, ElementRef, forwardRef, Input} from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import Editor = AceAjax.Editor;
+import {AutocompleteOption} from '../../model/autocomplete-option';
+
+declare var ace: any;
+
+@Component({
+  selector: 'metron-config-ace-editor',
+  templateUrl: 'ace-editor.component.html',
+  styleUrls: ['ace-editor.component.scss'],
+  providers: [
+    {
+      provide: NG_VALUE_ACCESSOR,
+      useExisting: forwardRef(() => AceEditorComponent),
+      multi: true
+    }
+  ]
+})
+export class AceEditorComponent implements AfterViewInit, ControlValueAccessor {
+
+  inputJson: any = '';
+  aceConfigEditor: Editor;
+  @Input() type: string = 'JSON';
+  @Input() placeHolder: string = 'Enter text here';
+  @Input() options: AutocompleteOption[] = [];
+  @ViewChild('aceEditor') aceEditorEle: ElementRef;
+
+  private onTouchedCallback;
+  private onChangeCallback;
+
+  constructor() {
+    ace.config.set('basePath', '/assets/ace');
+  }
+
+  ngAfterViewInit() {
+    ace.config.loadModule('ace/ext/language_tools',  () => { this.initializeEditor(); });
+  }
+
+  writeValue(obj: any) {
+    this.inputJson = obj;
+    this.setInput();
+  }
+
+  registerOnChange(fn: any) {
+    this.onChangeCallback = fn;
+  }
+
+  registerOnTouched(fn: any) {
+    this.onTouchedCallback = fn;
+  }
+
+  setDisabledState(isDisabled: boolean) {
+    // TODO set readonly
+  }
+
+  initializeEditor() {
+    this.aceConfigEditor = this.createEditor(this.aceEditorEle.nativeElement);
+    this.addPlaceHolder();
+    this.setInput();
+  }
+
+  updatePlaceHolderText() {
+    let shouldShow = !this.aceConfigEditor.session.getValue().length;
+    let node = this.aceConfigEditor.renderer['emptyMessageNode'];
+    if (!shouldShow && node) {
+      this.aceConfigEditor.renderer.scroller.removeChild(this.aceConfigEditor.renderer['emptyMessageNode']);
+      this.aceConfigEditor.renderer['emptyMessageNode'] = null;
+    } else if (shouldShow && !node) {
+      node = this.aceConfigEditor.renderer['emptyMessageNode'] = document.createElement('div');
+      node.textContent = this.placeHolder;
+      node.className = 'ace_invisible ace_emptyMessage';
+      this.aceConfigEditor.renderer.scroller.appendChild(node);
+    }
+  }
+
+  addPlaceHolder() {
+    this.aceConfigEditor.on('input', () => { this.updatePlaceHolderText(); });
+    setTimeout(() => { this.updatePlaceHolderText(); }, 100);
+  }
+
+  private createEditor(element: ElementRef) {
+    let parserConfigEditor = ace.edit(element);
+    parserConfigEditor.getSession().setMode(this.getEditorType());
+    parserConfigEditor.getSession().setTabSize(2);
+    parserConfigEditor.getSession().setUseWrapMode(true);
+    parserConfigEditor.getSession().setWrapLimitRange(72, 72);
+
+    parserConfigEditor.$blockScrolling = Infinity;
+    parserConfigEditor.setTheme('ace/theme/monokai');
+    parserConfigEditor.setOptions({
+      minLines: 10,
+      highlightActiveLine: false,
+      maxLines: Infinity,
+      enableBasicAutocompletion: true,
+      enableSnippets: true,
+      enableLiveAutocompletion: true
+    });
+    parserConfigEditor.on('change', (e: any) => {
+      this.inputJson = this.aceConfigEditor.getValue();
+      this.onChangeCallback(this.aceConfigEditor.getValue());
+    });
+
+    if (this.type === 'GROK') {
+      parserConfigEditor.completers = [this.getGrokCompletion()];
+    }
+
+    return parserConfigEditor;
+  }
+
+  private getGrokCompletion() {
+    let _this = this;
+    return {
+      getCompletions: function(editor, session, pos, prefix, callback) {
+        let autoCompletePrefix = '';
+        let autoCompleteSuffix = '';
+        let options = _this.options;
+
+        let currentToken = editor.getSession().getTokenAt(pos.row, pos.column);
+
+        // No value or user typed just a char
+        if (currentToken === null || currentToken.type === 'comment') {
+          autoCompletePrefix = '%{';
+          autoCompleteSuffix = ':$0}';
+        } else {
+          // }any<here>
+          if (currentToken.type === 'invalid') {
+            let lastToken = editor.getSession().getTokenAt(pos.row, (pos.column - currentToken.value.length));
+            autoCompletePrefix = lastToken.value.endsWith('}') ? ' %{' : '%{';
+            autoCompleteSuffix = ':$0}';
+          }
+
+          // In %{<here>}
+          if (currentToken.type === 'paren.rparen') {
+            autoCompletePrefix = currentToken.value.endsWith(' ') ? '%{' : ' %{';
+            autoCompleteSuffix = ':$0}';
+          }
+
+          // %{NUM<here>:}
+          if (currentToken.type === 'paren.lparen' || currentToken.type === 'variable') {
+            let nextToken = editor.getSession().getTokenAt(pos.row, pos.column + 1);
+            autoCompletePrefix = '';
+            autoCompleteSuffix = (nextToken && nextToken.value.indexOf(':') > -1) ? '' : ':$0}';
+          }
+
+          // %{NUMBER:<here>}
+          if (currentToken.type === 'seperator' || currentToken.type === 'string') {
+            let autocompleteVal = currentToken.value.replace(/:/g, '');
+            let autocompletes = autocompleteVal.length === 0 ? 'variable' : '';
+            options = [new AutocompleteOption(autocompletes)];
+          }
+        }
+
+        callback(null, options.map(function(autocompleteOption) {
+          return {
+            caption: autocompleteOption.name,
+            snippet: autoCompletePrefix + autocompleteOption.name + autoCompleteSuffix,
+            meta: 'grok-pattern',
+            score: Number.MAX_VALUE
+          };
+        }));
+
+      }
+    };
+  }
+
+  private getEditorType() {
+      if (this.type === 'GROK') {
+        return 'ace/mode/grok';
+      }
+
+      return 'ace/mode/json';
+  }
+
+  private setInput() {
+      if (this.aceConfigEditor && this.inputJson) {
+        this.aceConfigEditor.getSession().setValue(this.inputJson);
+        this.aceConfigEditor.resize(true);
+      }
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.module.ts b/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.module.ts
new file mode 100644
index 0000000..56817da
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.module.ts
@@ -0,0 +1,12 @@
+import {NgModule} from '@angular/core';
+
+import {AceEditorComponent}   from './ace-editor.component';
+
+@NgModule({
+    imports: [],
+    exports: [AceEditorComponent],
+    declarations: [AceEditorComponent],
+    providers: [],
+})
+export class AceEditorModule {
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/ace-editor/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/ace-editor/index.ts b/metron-interface/metron-config/src/app/shared/ace-editor/index.ts
new file mode 100644
index 0000000..a8ba0f0
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/ace-editor/index.ts
@@ -0,0 +1,18 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export * from './ace-editor.component'

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.component.html b/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.component.html
new file mode 100644
index 0000000..1a30ee7
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.component.html
@@ -0,0 +1,39 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+  http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<form role="form" [formGroup]="configForm">
+    <div *ngFor="let key of configKeys">
+        <div class="row mx-0 configkey-row">
+            <div class="col-md-10 advanced-input"><input type="text" class="form-control" name="advanced1" value="{{key}}" readonly></div>
+            <div class="col-md-2">
+            </div>
+        </div>
+        <div class="row  mx-0">
+            <div class="col-md-10 advanced-input"><input type="text" class="form-control" formControlName="{{key}}" [(ngModel)]="config[key]"></div>
+            <div class="col-md-2" (click)="removeConfig(key)">
+                <i class="fa fa-minus fa-4 icon-button" aria-hidden="true" ></i>
+            </div>
+        </div>
+    </div>
+    <div class="row  mx-0 new-configkey-row">
+        <div class="col-md-10 advanced-input"><input type="text" class="form-control"  (focus)="clearKeyPlaceholder()" (blur)="saveNewConfig()" [ngClass]="{'input-placeholder': newConfigKey == 'enter field'}" formControlName="newConfigKey" [(ngModel)]="newConfigKey"></div>
+        <div class="col-md-2">
+        </div>
+    </div>
+    <div class="row  mx-0">
+        <div class="col-md-10 advanced-input"><input type="text" class="form-control" (focus)="clearValuePlaceholder()" (blur)="saveNewConfig()" [ngClass]="{'input-placeholder': newConfigValue == 'enter value'}" formControlName="newConfigValue" [(ngModel)]="newConfigValue"></div>
+        <div class="col-md-2" (click)="addConfig()">
+            <i class="fa fa-plus fa-4 icon-button" aria-hidden="true" ></i>
+        </div>
+    </div>
+</form>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.component.scss b/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.component.scss
new file mode 100644
index 0000000..c43f0b1
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.component.scss
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../../_variables.scss";
+
+.advanced {
+  padding-left: 10px;
+  cursor: pointer;
+}
+
+.advanced-input {
+  padding-left: 0;
+  padding-right: 5px
+}
+
+.advanced-link {
+  color: $field-button-color;
+  font-size: 14px;
+}
+
+.advanced-title {
+  font-size: 16px;
+  color: $form-field-text-color;
+  display: inline-block;
+}
+
+.small-close-button {
+  font-size: 16px;
+  padding-right: 10px;
+  cursor: pointer;
+}
+
+.input-placeholder {
+  font-size: 11px;
+  font-style: italic;
+  color:#999999;
+}
+
+.new-configkey-row {
+  margin-bottom: 5px;
+  padding-top: 10px
+}
+
+.configkey-row {
+  margin-bottom: 5px;
+  margin-top: 10px
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.component.spec.ts b/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.component.spec.ts
new file mode 100644
index 0000000..a8f0ed0
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.component.spec.ts
@@ -0,0 +1,151 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {AdvancedConfigFormComponent} from './advanced-config-form.component';
+
+describe('Component: AdvancedConfigFormComponent', () => {
+
+  let comp: AdvancedConfigFormComponent;
+  let fixture: ComponentFixture<AdvancedConfigFormComponent>;
+
+  beforeEach(async(() => {
+
+    TestBed.configureTestingModule({
+      imports: [FormsModule, ReactiveFormsModule],
+      declarations: [AdvancedConfigFormComponent]
+    }).compileComponents()
+      .then(() => {
+        fixture = TestBed.createComponent(AdvancedConfigFormComponent);
+        comp = fixture.componentInstance;
+      });
+
+  }));
+
+  it('should create new forms for AdvancedConfigFormComponent',  async(() => {
+      let component: AdvancedConfigFormComponent =  fixture.componentInstance;
+      component.config = {'field1': 'value1', 'field2': 'value2'};
+      component.ngOnInit();
+
+      expect(Object.keys(component.configForm.controls).length).toEqual(4);
+      expect(component.configForm.controls['newConfigKey'].value).toEqual('enter field');
+      expect(component.configForm.controls['newConfigValue'].value).toEqual('enter value');
+      expect(component.configForm.controls['field1'].value).toEqual('value1');
+      expect(component.configForm.controls['field2'].value).toEqual('value2');
+  }));
+
+  it('OnChanges should recreate the form',  async(() => {
+    let component: AdvancedConfigFormComponent =  fixture.componentInstance;
+    component.config = {'field1': 'value1', 'field2': 'value2'};
+    let changes = {'field1': {'currentValue' : 'value1', 'previousValue': 'value1'}};
+    spyOn(component, 'createForm');
+
+    component.ngOnChanges(changes);
+
+    expect(component.createForm).not.toHaveBeenCalled();
+    expect(component.configKeys).toEqual([]);
+
+    changes = {'field1': {'currentValue' : 'value1', 'previousValue': 'value-1-1'}};
+    component.ngOnChanges(changes);
+
+    expect(component.createForm).toHaveBeenCalled();
+    expect(component.configKeys).toEqual(['field1', 'field2']);
+
+  }));
+
+  it('verify form interactions AdvancedConfigFormComponent',  async(() => {
+      let component: AdvancedConfigFormComponent =  fixture.componentInstance;
+      component.config = {'field1': 'value1', 'field2': 'value2'};
+      component.ngOnInit();
+
+
+      expect(component.newConfigKey).toEqual('enter field');
+      expect(component.newConfigValue).toEqual('enter value');
+      expect(component.configKeys).toEqual(['field1', 'field2']);
+
+      component.clearKeyPlaceholder();
+      expect(component.newConfigKey).toEqual('');
+      component.clearValuePlaceholder();
+      expect(component.newConfigValue).toEqual('');
+
+      component.newConfigKey = '';
+      component.newConfigValue = '';
+      component.saveNewConfig();
+      expect(Object.keys(component.config).length).toEqual(2);
+      expect(component.config['field1']).toEqual('value1');
+      expect(component.config['field2']).toEqual('value2');
+      expect(component.newConfigKey).toEqual('enter field');
+      expect(component.newConfigValue).toEqual('enter value');
+      component.addConfig();
+      expect(component.configKeys).toEqual(['field1', 'field2']);
+      expect(Object.keys(component.configForm.controls).length).toEqual(4);
+      expect(component.configForm.controls['newConfigKey'].value).toEqual('enter field');
+      expect(component.configForm.controls['newConfigValue'].value).toEqual('enter value');
+      expect(component.configForm.controls['field1'].value).toEqual('value1');
+      expect(component.configForm.controls['field2'].value).toEqual('value2');
+
+
+      component.newConfigKey = 'field3';
+      component.newConfigValue = 'value3';
+      component.saveNewConfig();
+      expect(Object.keys(component.config).length).toEqual(3);
+      expect(component.config['field1']).toEqual('value1');
+      expect(component.config['field2']).toEqual('value2');
+      expect(component.config['field3']).toEqual('value3');
+      expect(component.newConfigKey).toEqual('field3');
+      expect(component.newConfigValue).toEqual('value3');
+      component.addConfig();
+      expect(component.configKeys).toEqual(['field1', 'field2', 'field3']);
+      expect(Object.keys(component.configForm.controls).length).toEqual(5);
+      expect(component.configForm.controls['newConfigKey'].value).toEqual('enter field');
+      expect(component.configForm.controls['newConfigValue'].value).toEqual('enter value');
+      expect(component.configForm.controls['field1'].value).toEqual('value1');
+      expect(component.configForm.controls['field2'].value).toEqual('value2');
+      expect(component.configForm.controls['field3'].value).toEqual('value3');
+
+      component.newConfigKey = 'field1';
+      component.newConfigValue = 'newValue1';
+      component.saveNewConfig();
+      expect(Object.keys(component.config).length).toEqual(3);
+      expect(component.config['field1']).toEqual('newValue1');
+      expect(component.config['field2']).toEqual('value2');
+      expect(component.config['field3']).toEqual('value3');
+      expect(component.newConfigKey).toEqual('enter field');
+      expect(component.newConfigValue).toEqual('enter value');
+      component.addConfig();
+      expect(component.configKeys).toEqual(['field1', 'field2', 'field3']);
+      expect(Object.keys(component.configForm.controls).length).toEqual(5);
+      expect(component.configForm.controls['newConfigKey'].value).toEqual('enter field');
+      expect(component.configForm.controls['newConfigValue'].value).toEqual('enter value');
+      expect(component.configForm.controls['field1'].value).toEqual('value1');
+      expect(component.configForm.controls['field2'].value).toEqual('value2');
+      expect(component.configForm.controls['field3'].value).toEqual('value3');
+
+      component.removeConfig('field1');
+      expect(Object.keys(component.config).length).toEqual(2);
+      expect(component.config['field2']).toEqual('value2');
+      expect(component.config['field3']).toEqual('value3');
+      expect(component.configKeys).toEqual(['field2', 'field3']);
+      expect(Object.keys(component.configForm.controls).length).toEqual(4);
+      expect(component.configForm.controls['newConfigKey'].value).toEqual('enter field');
+      expect(component.configForm.controls['newConfigValue'].value).toEqual('enter value');
+      expect(component.configForm.controls['field2'].value).toEqual('value2');
+      expect(component.configForm.controls['field3'].value).toEqual('value3');
+  }));
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.component.ts b/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.component.ts
new file mode 100644
index 0000000..f363391
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.component.ts
@@ -0,0 +1,110 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Component, OnInit, OnChanges, Input} from '@angular/core';
+import {FormGroup, Validators, FormControl} from '@angular/forms';
+
+@Component({
+  selector: 'metron-config-advanced-form',
+  templateUrl: 'advanced-config-form.component.html',
+  styleUrls: ['advanced-config-form.component.scss']
+})
+export class AdvancedConfigFormComponent implements OnInit, OnChanges {
+
+  @Input() config: {};
+  configKeys: string[] = [];
+  newConfigKey: string = 'enter field';
+  newConfigValue: string = 'enter value';
+  configForm: FormGroup;
+
+  constructor() {
+
+  }
+
+  ngOnInit() {
+    this.configKeys = Object.keys(this.config);
+    this.configForm = this.createForm();
+  }
+
+  ngOnChanges(changes: {[propertyName: string]: any}) {
+    for (let propName of Object.keys(changes)) {
+      let chng = changes[propName];
+      let cur = JSON.stringify(chng.currentValue);
+      let prev = JSON.stringify(chng.previousValue);
+      if (cur !== prev) {
+        this.configKeys = Object.keys(this.config);
+        this.configForm = this.createForm();
+      }
+    }
+  }
+
+  createForm(): FormGroup {
+    let group: any = {};
+    for (let key of this.configKeys) {
+      group[key] = new FormControl(this.config[key], Validators.required);
+    }
+    group['newConfigKey'] = new FormControl(this.newConfigKey, Validators.required);
+    group['newConfigValue'] = new FormControl(this.newConfigValue, Validators.required);
+    return new FormGroup(group);
+  }
+
+  clearKeyPlaceholder() {
+    if (this.newConfigKey === 'enter field') {
+      this.newConfigKey = '';
+    }
+  }
+
+  clearValuePlaceholder() {
+    if (this.newConfigValue === 'enter value') {
+      this.newConfigValue = '';
+    }
+  }
+
+  saveNewConfig() {
+    if (this.newConfigKey === '') {
+      this.newConfigKey = 'enter field';
+    }
+    if (this.newConfigValue === '') {
+      this.newConfigValue = 'enter value';
+    }
+    if (this.newConfigKey !== 'enter field' && this.newConfigValue !== 'enter value') {
+      let keyExists = this.config[this.newConfigKey] !== undefined;
+      this.config[this.newConfigKey] = this.newConfigValue;
+      if (keyExists) {
+        this.newConfigKey = 'enter field';
+        this.newConfigValue = 'enter value';
+      }
+
+    }
+  }
+
+  addConfig() {
+    if (this.newConfigKey !== 'enter field' && this.newConfigValue !== 'enter value') {
+      this.configKeys.push(this.newConfigKey);
+      this.configForm.addControl(this.newConfigKey, new FormControl(this.newConfigValue, Validators.required));
+      this.newConfigKey = 'enter field';
+      this.newConfigValue = 'enter value';
+    }
+  }
+
+  removeConfig(key: string) {
+    delete this.config[key];
+    this.configKeys = Object.keys(this.config);
+    this.configForm.removeControl(key);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.module.ts b/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.module.ts
new file mode 100644
index 0000000..d29a369
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/advanced-config-form/advanced-config-form.module.ts
@@ -0,0 +1,14 @@
+import {NgModule} from '@angular/core';
+
+import {AdvancedConfigFormComponent}   from './advanced-config-form.component';
+import {SharedModule} from '../shared.module';
+import {ReactiveFormsModule} from '@angular/forms';
+
+@NgModule({
+    imports: [ ReactiveFormsModule, SharedModule ],
+    exports: [ AdvancedConfigFormComponent ],
+    declarations: [ AdvancedConfigFormComponent ],
+    providers: [],
+})
+export class AdvancedConfigFormModule {
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/auth-guard.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/auth-guard.spec.ts b/metron-interface/metron-config/src/app/shared/auth-guard.spec.ts
new file mode 100644
index 0000000..78c246a
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/auth-guard.spec.ts
@@ -0,0 +1,92 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {EventEmitter}     from '@angular/core';
+import {AuthGuard} from './auth-guard';
+import {AuthenticationService} from '../service/authentication.service';
+import {Router} from '@angular/router';
+
+class MockAuthenticationService {
+  _isAuthenticationChecked: boolean;
+  _isAuthenticated: boolean;
+  onLoginEvent: EventEmitter<boolean> = new EventEmitter<boolean>();
+
+  public isAuthenticationChecked(): boolean {
+    return this._isAuthenticationChecked;
+  }
+
+  public isAuthenticated(): boolean {
+    return this._isAuthenticated;
+  }
+}
+
+class MockRouter {
+  navigateByUrl(): any {}
+}
+
+describe('AuthGuard', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      providers: [
+        AuthGuard,
+        {provide: AuthenticationService, useClass: MockAuthenticationService},
+        {provide: Router, useClass: MockRouter}
+      ]
+    })
+      .compileComponents();
+
+  }));
+
+  it('can instantiate auth guard',
+    inject([AuthGuard], (authGuard: AuthGuard) => {
+      expect(authGuard instanceof AuthGuard).toBe(true);
+    }));
+
+  it('test when authentication is checked',
+    inject([AuthGuard, AuthenticationService], (authGuard: AuthGuard, authenticationService: MockAuthenticationService) => {
+      authenticationService._isAuthenticationChecked = true;
+      authenticationService._isAuthenticated = true;
+      expect(authGuard.canActivate(null, null)).toBe(true);
+
+      authenticationService._isAuthenticationChecked = true;
+      authenticationService._isAuthenticated = false;
+      expect(authGuard.canActivate(null, null)).toBe(false);
+    }));
+
+  it('test when authentication is not checked',
+    inject([AuthGuard, AuthenticationService, Router], (authGuard: AuthGuard,
+                                                        authenticationService: MockAuthenticationService,
+                                                        router: MockRouter) => {
+      authenticationService._isAuthenticationChecked = false;
+      authGuard.canActivate(null, null).subscribe(isUserValid => {
+        expect(isUserValid).toBe(true);
+      });
+      authenticationService.onLoginEvent.emit(true);
+
+
+      spyOn(router, 'navigateByUrl');
+      authenticationService._isAuthenticationChecked = false;
+      authGuard.canActivate(null, null).subscribe(isUserValid => {
+        expect(isUserValid).toBe(false);
+      });
+      authenticationService.onLoginEvent.emit(false);
+      expect(router.navigateByUrl).toHaveBeenCalledWith('/login');
+
+    }));
+});


[03/12] incubator-metron git commit: METRON-623 Management UI [contributed by Raghu Mitra Kandikonda and Ryan Merriman] closes apache/incubator-metron#489

Posted by rm...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/auth-guard.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/auth-guard.ts b/metron-interface/metron-config/src/app/shared/auth-guard.ts
new file mode 100644
index 0000000..66c27cf
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/auth-guard.ts
@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Injectable } from '@angular/core';
+import {
+  CanActivate,
+  Router,
+  ActivatedRouteSnapshot,
+  RouterStateSnapshot
+} from '@angular/router';
+import { Observable } from 'rxjs/Observable';
+import { AuthenticationService } from '../service/authentication.service';
+
+@Injectable()
+export class AuthGuard implements CanActivate {
+
+  constructor(private authService: AuthenticationService, private router: Router) {}
+
+  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
+    if (!this.authService.isAuthenticationChecked()) {
+      return Observable.create(observer => {
+        this.authService.onLoginEvent.subscribe(isLoggedIn => {
+          if (isLoggedIn) {
+            observer.next(true);
+            observer.complete();
+          } else {
+            observer.next(false);
+            observer.complete();
+            this.router.navigateByUrl('/login');
+          }
+        });
+      });
+    }
+    return this.authService.isAuthenticated();
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/index.ts b/metron-interface/metron-config/src/app/shared/index.ts
new file mode 100644
index 0000000..be1e874
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/index.ts
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export * from './metron-dialog-box';
+export * from './metron-modal';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/login-guard.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/login-guard.spec.ts b/metron-interface/metron-config/src/app/shared/login-guard.spec.ts
new file mode 100644
index 0000000..a49b124
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/login-guard.spec.ts
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {AuthenticationService} from '../service/authentication.service';
+import {Router} from '@angular/router';
+import {LoginGuard} from './login-guard';
+
+class MockAuthenticationService {
+  public logout(): void {}
+}
+
+class MockRouter {
+  navigateByUrl(): any {}
+}
+
+describe('LoginGuard', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      providers: [
+        LoginGuard,
+        {provide: AuthenticationService, useClass: MockAuthenticationService},
+        {provide: Router, useClass: MockRouter}
+      ]
+    })
+      .compileComponents();
+
+  }));
+
+  it('can instantiate auth guard',
+    inject([LoginGuard], (loginGaurd: LoginGuard) => {
+      expect(loginGaurd instanceof LoginGuard).toBe(true);
+  }));
+
+  it('test when login is checked',
+    inject([LoginGuard, AuthenticationService], (loginGuard: LoginGuard, authenticationService: MockAuthenticationService) => {
+
+      spyOn(authenticationService, 'logout');
+
+      expect(loginGuard.canActivate(null, null)).toBe(true);
+
+      expect(authenticationService.logout).toHaveBeenCalled();
+
+  }));
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/login-guard.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/login-guard.ts b/metron-interface/metron-config/src/app/shared/login-guard.ts
new file mode 100644
index 0000000..d8b8f0d
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/login-guard.ts
@@ -0,0 +1,40 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Injectable } from '@angular/core';
+import {
+  CanActivate,
+  Router,
+  ActivatedRouteSnapshot,
+  RouterStateSnapshot
+} from '@angular/router';
+import { AuthenticationService } from '../service/authentication.service';
+
+// This guard will ensure that logout is called even if you call /login manually
+@Injectable()
+export class LoginGuard implements CanActivate {
+
+  constructor(private authService: AuthenticationService, private router: Router) {}
+
+  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
+
+    this.authService.logout();
+
+    return true;
+
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-alerts.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-alerts.spec.ts b/metron-interface/metron-config/src/app/shared/metron-alerts.spec.ts
new file mode 100644
index 0000000..5efe063
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-alerts.spec.ts
@@ -0,0 +1,75 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* tslint:disable:no-unused-variable */
+
+import {MetronAlerts} from './metron-alerts';
+
+describe('MetronAlerts', () => {
+
+  beforeEach(function() {
+    MetronAlerts.SUCESS_MESSAGE_DISPALY_TIME = 500;
+  });
+
+  afterEach(function() {
+    MetronAlerts.SUCESS_MESSAGE_DISPALY_TIME = 5000;
+  });
+
+  it('should create an instance', () => {
+    expect(new MetronAlerts()).toBeTruthy();
+  });
+
+  it('should close success message after timeout', (done) => {
+    new MetronAlerts().showSuccessMessage('test message');
+
+    expect($(document).find('.alert.alert-success span').text()).toEqual('test message');
+
+    setTimeout(() => {
+      expect($(document).find('.alert .alert-success').length).toEqual(0);
+      done();
+    }, MetronAlerts.SUCESS_MESSAGE_DISPALY_TIME);
+  });
+
+  it('should close success message on click of close', (done) => {
+    new MetronAlerts().showSuccessMessage('test message');
+
+    expect($(document).find('.alert.alert-success span').text()).toEqual('test message');
+
+    $(document).find('.alert.alert-success .close').click();
+
+    expect($(document).find('.alert .alert-success').length).toEqual(0);
+
+    setTimeout(() => {
+      expect($(document).find('.alert .alert-success').length).toEqual(0);
+      done();
+    }, MetronAlerts.SUCESS_MESSAGE_DISPALY_TIME);
+
+  });
+
+  // it('should close error message on click of close', () => {
+  //   new MetronAlerts().showErrorMessage('test message');
+  //
+  //   expect($(document).find('.alert.alert-danger span').text()).toEqual('test message');
+  //
+  //   $(document).find('.alert.alert-danger .close').click();
+  //
+  //   expect($(document).find('.alert .alert-danger').length).toEqual(0);
+  //
+  // });
+
+});
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-alerts.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-alerts.ts b/metron-interface/metron-config/src/app/shared/metron-alerts.ts
new file mode 100644
index 0000000..5b307e4
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-alerts.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export class MetronAlerts {
+
+  public static SUCESS_MESSAGE_DISPALY_TIME = 5000;
+
+  private createMessage(message: string, type: string): Node {
+    let element = document.createElement('div');
+    element.id = 'alert_placeholder';
+    element.style.zIndex = '1000';
+    element.style.position = 'fixed';
+    element.style.display = 'inline-block';
+    element.style.minWidth = '250px';
+    element.style.cssFloat = 'right';
+    element.style.top = '5px';
+    element.style.right = '35px';
+
+    element.innerHTML = '<div id="alertdiv" class="alert ' + type + '"><a class="close" data-dismiss="alert">�</a><span>' +
+      message + '</span></div>';
+    document.body.appendChild(element);
+
+    return element;
+  }
+
+  showErrorMessage(message: string): void {
+    this.createMessage(message, 'alert-danger');
+  }
+
+  showSuccessMessage(message: string): void {
+    let element = this.createMessage(message, 'alert-success');
+    setTimeout(function () {
+
+      document.body.removeChild(element);
+
+    }, MetronAlerts.SUCESS_MESSAGE_DISPALY_TIME);
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-dialog-box.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-dialog-box.spec.ts b/metron-interface/metron-config/src/app/shared/metron-dialog-box.spec.ts
new file mode 100644
index 0000000..3c4acb7
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-dialog-box.spec.ts
@@ -0,0 +1,63 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* tslint:disable:no-unused-variable */
+
+import {MetronDialogBox} from './metron-dialog-box';
+
+describe('MetronDialogBox', () => {
+
+  it('should create an instance', () => {
+    expect(new MetronDialogBox()).toBeTruthy();
+  });
+
+  it('should return true', () => {
+    let showConfirmationMessage = new MetronDialogBox().showConfirmationMessage('test message');
+    let subscription = showConfirmationMessage.subscribe(result => {
+      expect(result).toEqual(true);
+      subscription.unsubscribe();
+    });
+    spyOn(showConfirmationMessage, 'subscribe');
+
+    expect($(document).find('.metron-dialog .modal-title').text()).toEqual('Confirmation');
+    expect($(document).find('.metron-dialog .modal-body p').text()).toEqual('test message');
+
+    $(document).find('.metron-dialog .btn-primary').click();
+
+    $(document).find('.metron-dialog').trigger('hidden.bs.modal');
+
+    expect($(document).find('.metron-dialog').length).toEqual(0);
+
+  });
+
+  it('should return false', () => {
+    let showConfirmationMessage = new MetronDialogBox().showConfirmationMessage('test message');
+    let subscription = showConfirmationMessage.subscribe(result => {
+      expect(result).toEqual(false);
+      subscription.unsubscribe();
+    });
+    spyOn(showConfirmationMessage, 'subscribe');
+
+    $(document).find('.metron-dialog .form-enable-disable-button').click();
+
+    $(document).find('.metron-dialog').trigger('hidden.bs.modal');
+
+    expect($(document).find('.metron-dialog').length).toEqual(0);
+
+  });
+});
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-dialog-box.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-dialog-box.ts b/metron-interface/metron-config/src/app/shared/metron-dialog-box.ts
new file mode 100644
index 0000000..5e0e32b
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-dialog-box.ts
@@ -0,0 +1,75 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {EventEmitter}     from '@angular/core';
+
+export class MetronDialogBox {
+
+  private createDialogBox(message: string, title: string) {
+
+    let html = `<div class="metron-dialog modal fade"  data-backdrop="static" >
+                  <div class="modal-dialog modal-sm" role="document">
+                    <div class="modal-content">
+                      <div class="modal-header">
+                        <button type="button" class="close" data-dismiss="modal" aria-label="Close"> 
+                            <span aria-hidden="true">&times;</span> 
+                        </button>
+                        <span class="modal-title"><b>` + title + `</b></span>
+                      </div>
+                      <div class="modal-body">
+                        <p>` +  message + `</p>
+                      </div>
+                      <div class="modal-footer">
+                        <button type="button" class="btn btn-primary">OK</button>
+                        <button type="button" class="btn form-enable-disable-button" data-dismiss="modal">Cancel</button>
+                      </div>
+                    </div>
+                  </div>
+                </div>`;
+
+    let element = document.createElement('div');
+    element.innerHTML = html;
+
+    document.body.appendChild(element);
+
+    return element;
+  }
+
+  public showConfirmationMessage(message: string): EventEmitter<boolean> {
+    let eventEmitter = new EventEmitter<boolean>();
+    let element = this.createDialogBox(message, 'Confirmation');
+
+    $(element).find('.metron-dialog').modal('show');
+
+    $(element).find('.btn-primary').on('click', function (e) {
+      $(element).find('.metron-dialog').modal('hide');
+      eventEmitter.emit(true);
+    });
+
+    $(element).find('.form-enable-disable-button').on('click', function (e) {
+      $(element).find('.metron-dialog').modal('hide');
+      eventEmitter.emit(false);
+    });
+
+    $(element).find('.metron-dialog').on('hidden.bs.modal', function (e) {
+      $(element).remove();
+    });
+
+    return eventEmitter;
+
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-modal/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-modal/index.ts b/metron-interface/metron-config/src/app/shared/metron-modal/index.ts
new file mode 100644
index 0000000..ec19caf
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-modal/index.ts
@@ -0,0 +1 @@
+export * from './metron-modal.component';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-modal/metron-modal.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-modal/metron-modal.component.html b/metron-interface/metron-config/src/app/shared/metron-modal/metron-modal.component.html
new file mode 100644
index 0000000..3a0b18e
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-modal/metron-modal.component.html
@@ -0,0 +1,20 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<div [ngClass]="{'metron-modal': backgroundMasked, 'metron-modal-clickable': !backgroundMasked}"  data-backdrop="static" data-keyboard="false">
+  <div class="dialog-pane">
+    <ng-content></ng-content>
+  </div>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-modal/metron-modal.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-modal/metron-modal.component.scss b/metron-interface/metron-config/src/app/shared/metron-modal/metron-modal.component.scss
new file mode 100644
index 0000000..b47468f
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-modal/metron-modal.component.scss
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../../../styles.scss";
+
+.metron-modal {
+  position: fixed;
+  z-index: 1;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgb(0,0,0);
+  background-color: rgba(0,0,0,0.4);
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.metron-modal-clickable {
+  position: fixed;
+  z-index: 1;
+  right: 0;
+  top: 0;
+  height: 100%;
+  padding-left: 15px;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+.dialog-pane {
+  @extend .flexbox-row-reverse;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-modal/metron-modal.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-modal/metron-modal.component.spec.ts b/metron-interface/metron-config/src/app/shared/metron-modal/metron-modal.component.spec.ts
new file mode 100644
index 0000000..74f6c1e
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-modal/metron-modal.component.spec.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, TestBed, ComponentFixture} from '@angular/core/testing';
+import {SharedModule} from '../shared.module';
+import { MetronModalComponent } from './metron-modal.component';
+
+describe('MetronModalComponent', () => {
+
+  let fixture: ComponentFixture<MetronModalComponent>;
+  let component: MetronModalComponent;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [SharedModule],
+      providers: [
+        MetronModalComponent
+      ]
+    });
+
+    fixture = TestBed.createComponent(MetronModalComponent);
+    component = fixture.componentInstance;
+
+  }));
+
+  it('can instantiate MetronModalComponent', async(() => {
+    expect(component instanceof MetronModalComponent).toBe(true);
+  }));
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-modal/metron-modal.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-modal/metron-modal.component.ts b/metron-interface/metron-config/src/app/shared/metron-modal/metron-modal.component.ts
new file mode 100644
index 0000000..22a95d6
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-modal/metron-modal.component.ts
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, Input } from '@angular/core';
+
+@Component({
+  selector: 'metron-config-metron-modal',
+  templateUrl: 'metron-modal.component.html',
+  styleUrls: ['metron-modal.component.scss']
+})
+export class MetronModalComponent {
+  @Input() backgroundMasked = true;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/index.ts b/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/index.ts
new file mode 100644
index 0000000..325568a
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/index.ts
@@ -0,0 +1 @@
+export * from './metron-sorter.component';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/metron-sorter.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/metron-sorter.component.html b/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/metron-sorter.component.html
new file mode 100644
index 0000000..988bec1
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/metron-sorter.component.html
@@ -0,0 +1,21 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<a style="cursor: pointer"  class="text-nowrap" (click)="sort()">
+  <ng-content></ng-content>
+  <i class="fa fa-sort" aria-hidden="true" *ngIf="!sortAsc && !sortDesc"></i>
+  <i class="fa fa-sort-asc" aria-hidden="true" *ngIf="sortAsc"></i>
+  <i class="fa fa-sort-desc" aria-hidden="true" *ngIf="sortDesc"></i>
+</a>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/metron-sorter.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/metron-sorter.component.scss b/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/metron-sorter.component.scss
new file mode 100644
index 0000000..5d5f1e3
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/metron-sorter.component.scss
@@ -0,0 +1,17 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/metron-sorter.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/metron-sorter.component.spec.ts b/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/metron-sorter.component.spec.ts
new file mode 100644
index 0000000..cac8667
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/metron-sorter.component.spec.ts
@@ -0,0 +1,75 @@
+/* tslint:disable:no-unused-variable */
+// directiveSelectorNameRule
+
+import {MetronSorterComponent} from './metron-sorter.component';
+import {MetronTableDirective} from '../metron-table.directive';
+
+describe('Component: MetronSorter', () => {
+
+  it('should create an instance', () => {
+    let metronTable = new MetronTableDirective();
+    let component = new MetronSorterComponent(metronTable);
+    expect(component).toBeTruthy();
+  });
+
+  it('should set the variables according to sorter', () => {
+    let metronTable = new MetronTableDirective();
+    let sorter1 = new MetronSorterComponent(metronTable);
+    let sorter2 = new MetronSorterComponent(metronTable);
+    let sorter3 = new MetronSorterComponent(metronTable);
+
+    sorter1.sortBy = 'col1';
+    sorter2.sortBy = 'col2';
+    sorter3.sortBy = 'col3';
+
+    sorter1.sort();
+    expect(sorter1.sortAsc).toEqual(true);
+    expect(sorter1.sortDesc).toEqual(false);
+    expect(sorter2.sortAsc).toEqual(false);
+    expect(sorter2.sortDesc).toEqual(false);
+    expect(sorter3.sortAsc).toEqual(false);
+    expect(sorter3.sortDesc).toEqual(false);
+
+    sorter1.sort();
+    expect(sorter1.sortAsc).toEqual(false);
+    expect(sorter1.sortDesc).toEqual(true);
+    expect(sorter2.sortAsc).toEqual(false);
+    expect(sorter2.sortDesc).toEqual(false);
+    expect(sorter3.sortAsc).toEqual(false);
+    expect(sorter3.sortDesc).toEqual(false);
+
+    sorter2.sort();
+    expect(sorter1.sortAsc).toEqual(false);
+    expect(sorter1.sortDesc).toEqual(false);
+    expect(sorter2.sortAsc).toEqual(true);
+    expect(sorter2.sortDesc).toEqual(false);
+    expect(sorter3.sortAsc).toEqual(false);
+    expect(sorter3.sortDesc).toEqual(false);
+
+    sorter2.sort();
+    expect(sorter1.sortAsc).toEqual(false);
+    expect(sorter1.sortDesc).toEqual(false);
+    expect(sorter2.sortAsc).toEqual(false);
+    expect(sorter2.sortDesc).toEqual(true);
+    expect(sorter3.sortAsc).toEqual(false);
+    expect(sorter3.sortDesc).toEqual(false);
+
+    sorter3.sort();
+    expect(sorter1.sortAsc).toEqual(false);
+    expect(sorter1.sortDesc).toEqual(false);
+    expect(sorter2.sortAsc).toEqual(false);
+    expect(sorter2.sortDesc).toEqual(false);
+    expect(sorter3.sortAsc).toEqual(true);
+    expect(sorter3.sortDesc).toEqual(false);
+
+    sorter3.sort();
+    expect(sorter1.sortAsc).toEqual(false);
+    expect(sorter1.sortDesc).toEqual(false);
+    expect(sorter2.sortAsc).toEqual(false);
+    expect(sorter2.sortDesc).toEqual(false);
+    expect(sorter3.sortAsc).toEqual(false);
+    expect(sorter3.sortDesc).toEqual(true);
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/metron-sorter.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/metron-sorter.component.ts b/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/metron-sorter.component.ts
new file mode 100644
index 0000000..11ada7d
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-table/metron-sorter/metron-sorter.component.ts
@@ -0,0 +1,28 @@
+import { Component, Input } from '@angular/core';
+import {Sort} from '../../../util/enums';
+import {MetronTableDirective, SortEvent} from '../metron-table.directive';
+
+@Component({
+  selector: 'metron-config-sorter',
+  templateUrl: './metron-sorter.component.html',
+  styleUrls: ['./metron-sorter.component.scss']
+})
+export class MetronSorterComponent {
+
+  @Input() sortBy: string;
+
+  sortAsc: boolean = false;
+  sortDesc: boolean = false;
+
+  constructor(private metronTable: MetronTableDirective ) {
+    this.metronTable.onSortColumnChange.subscribe((event: SortEvent) => {
+      this.sortAsc = (event.sortBy === this.sortBy && event.sortOrder === Sort.ASC);
+      this.sortDesc = (event.sortBy === this.sortBy && event.sortOrder === Sort.DSC);
+    });
+  }
+
+  sort() {
+    let order = this.sortAsc ? Sort.DSC : Sort.ASC;
+    this.metronTable.setSort({sortBy: this.sortBy, sortOrder: order});
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-table/metron-table.directive.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-table/metron-table.directive.spec.ts b/metron-interface/metron-config/src/app/shared/metron-table/metron-table.directive.spec.ts
new file mode 100644
index 0000000..b469d46
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-table/metron-table.directive.spec.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* tslint:disable:no-unused-variable */
+
+import {Sort} from '../../util/enums';
+import {MetronTableDirective, SortEvent} from './metron-table.directive';
+
+describe('Directive: MetronTable', () => {
+
+  it('should create an instance', () => {
+    let directive = new MetronTableDirective();
+    expect(directive).toBeTruthy();
+  });
+
+  it('should emmit listeners ', () => {
+    let eventToTest: SortEvent = null;
+    let directive = new MetronTableDirective();
+
+    let event1 = { sortBy: 'col1', sortOrder: Sort.ASC };
+    let event2 = { sortBy: 'col2', sortOrder: Sort.DSC };
+
+    directive.onSort.subscribe((sortEvent: SortEvent) => {
+        expect(sortEvent).toEqual(eventToTest);
+    });
+    directive.onSortColumnChange.subscribe((sortEvent: SortEvent) => {
+        expect(sortEvent).toEqual(eventToTest);
+    });
+
+    eventToTest = event1;
+    directive.setSort(eventToTest);
+
+    eventToTest = event2;
+    directive.setSort(eventToTest);
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-table/metron-table.directive.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-table/metron-table.directive.ts b/metron-interface/metron-config/src/app/shared/metron-table/metron-table.directive.ts
new file mode 100644
index 0000000..682fecc
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-table/metron-table.directive.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* tslint:disable:directive-selector-name */
+import {Directive, Output, EventEmitter} from '@angular/core';
+import {Sort} from '../../util/enums';
+
+export interface SortEvent {
+  sortBy: string;
+  sortOrder: Sort;
+}
+
+@Directive({
+  selector: '[metron-config-table]'
+})
+
+export class MetronTableDirective {
+
+  @Output() onSort = new EventEmitter<SortEvent>();
+
+  onSortColumnChange = new EventEmitter<SortEvent>();
+
+  public setSort(sortEvent: SortEvent): void {
+    this.onSortColumnChange.emit(sortEvent);
+    this.onSort.emit(sortEvent);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/metron-table/metron-table.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/metron-table/metron-table.module.ts b/metron-interface/metron-config/src/app/shared/metron-table/metron-table.module.ts
new file mode 100644
index 0000000..57369df
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/metron-table/metron-table.module.ts
@@ -0,0 +1,14 @@
+import {NgModule} from '@angular/core';
+
+import {MetronSorterComponent} from './metron-sorter/metron-sorter.component';
+import {MetronTableDirective} from './metron-table.directive';
+import {SharedModule} from '../shared.module';
+
+@NgModule({
+    imports: [ SharedModule ],
+    exports: [ MetronSorterComponent, MetronTableDirective ],
+    declarations: [ MetronSorterComponent, MetronTableDirective ],
+    providers: [],
+})
+export class MetronTableModule {
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/multiple-input/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/multiple-input/index.ts b/metron-interface/metron-config/src/app/shared/multiple-input/index.ts
new file mode 100644
index 0000000..3697be3
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/multiple-input/index.ts
@@ -0,0 +1 @@
+export * from './multiple-input.component';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.component.html b/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.component.html
new file mode 100644
index 0000000..f070dd7
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.component.html
@@ -0,0 +1,39 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<!--Configured-->
+<div *ngFor="let configuredItem of configuredItems; let i = index" class="row mx-0">
+  <div class="col-md-10 px-0">
+    <input *ngIf="type==='text'" class="form-control disabled" readonly [ngModel]="configuredItem.name">
+    <select *ngIf="type==='select'" #select class="form-control" [ngModel]="configuredItem.name" (change)="onUpdate(configuredItem,select.value)">
+      <option *ngFor="let availableFunction of getAvailableFunctions()" [disabled]="availableFunction===''" [selected]="availableFunction===''"> {{ availableFunction.name }} </option>
+    </select>
+  </div>
+  <div class="col-md-2 px-0">
+    <i class="fa fa-minus fa-border icon-button" aria-hidden="true" style="margin-left: 0.3rem;" (click)="onRemove(configuredItem)"></i>
+  </div>
+</div>
+
+<!--new-->
+<div class="row mx-0" *ngIf="(this.configuredItems.length !== this.availableItems.length) || configuredItems.length === 0 ">
+  <div class="col-md-10 px-0">
+    <select #select class="form-control" (change)="onAdd(select)">
+      <option [disabled]="true" [selected]="true"></option>
+      <option *ngFor="let availableFunction of getAvailableFunctions()"> {{ availableFunction.name }} </option>
+    </select>
+  </div>
+  <div class="col-md-3 px-0"> </div>
+</div>
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.component.scss b/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.component.scss
new file mode 100644
index 0000000..611a44a
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.component.scss
@@ -0,0 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.form-control
+{
+  margin-bottom: 4px;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.component.spec.ts b/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.component.spec.ts
new file mode 100644
index 0000000..0bf9b76
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.component.spec.ts
@@ -0,0 +1,147 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* tslint:disable:no-unused-variable */
+
+import {async, TestBed, ComponentFixture} from '@angular/core/testing';
+import { MultipleInputComponent } from './multiple-input.component';
+import {SharedModule} from '../shared.module';
+import {AutocompleteOption} from '../../model/autocomplete-option';
+
+describe('Component: MultipleInput', () => {
+
+  let fixture: ComponentFixture<MultipleInputComponent>;
+  let component: MultipleInputComponent;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [SharedModule],
+      declarations: [ MultipleInputComponent ],
+      providers: [
+        MultipleInputComponent
+      ]
+    });
+
+    fixture = TestBed.createComponent(MultipleInputComponent);
+    component = fixture.componentInstance;
+
+  }));
+
+  it('should create an instance', () => {
+    expect(component).toBeDefined();
+  });
+
+  it('should get AvailableFunctions', () => {
+    let availableItems: AutocompleteOption[] = [];
+    availableItems.push(new AutocompleteOption('option1'));
+    availableItems.push(new AutocompleteOption('option2'));
+    availableItems.push(new AutocompleteOption('option3'));
+    availableItems.push(new AutocompleteOption('option4'));
+
+    let configuredItems: AutocompleteOption[] = [];
+    configuredItems.push(new AutocompleteOption('option1'));
+    configuredItems.push(new AutocompleteOption('option3'));
+
+    component.allowDuplicates = false;
+
+    component.availableItems = availableItems;
+    expect(component.getAvailableFunctions()).toEqual(availableItems);
+
+    component.configuredItems = configuredItems;
+    expect(component.getAvailableFunctions()).toEqual([new AutocompleteOption('option2'), new AutocompleteOption('option4')]);
+
+    component.allowDuplicates = true;
+    expect(component.getAvailableFunctions()).toEqual(availableItems);
+
+    fixture.destroy();
+  });
+
+  it('should get item by name', () => {
+    let availableItems: AutocompleteOption[] = [];
+    availableItems.push(new AutocompleteOption('option1'));
+    availableItems.push(new AutocompleteOption('option2'));
+    availableItems.push(new AutocompleteOption('option3'));
+
+    component.availableItems = availableItems;
+    expect(component.getAvailableItemByName('option1')).toEqual(new AutocompleteOption('option1'));
+    expect(component.getAvailableItemByName('option4')).toEqual(new AutocompleteOption());
+
+    fixture.destroy();
+  });
+
+  it('should add', () => {
+    spyOn(component.onConfigChange, 'emit');
+
+    let availableItems: AutocompleteOption[] = [];
+    availableItems.push(new AutocompleteOption('option1'));
+    availableItems.push(new AutocompleteOption('option2'));
+    availableItems.push(new AutocompleteOption('option3'));
+
+    let select = {'value': 'option1'};
+    component.availableItems = availableItems;
+    component.onAdd(select);
+    expect(component.configuredItems.length).toEqual(1);
+    expect(component.configuredItems[0].name).toEqual('option1');
+    expect(component.showAddNew).toEqual(false);
+    expect(component.onConfigChange.emit).toHaveBeenCalled();
+    expect(select.value).toEqual('');
+
+    fixture.destroy();
+  });
+
+  it('should remove', () => {
+    spyOn(component.onConfigChange, 'emit');
+
+    let configuredItems: AutocompleteOption[] = [];
+    configuredItems.push(new AutocompleteOption('option1'));
+    configuredItems.push(new AutocompleteOption('option2'));
+    configuredItems.push(new AutocompleteOption('option3'));
+
+    component.configuredItems = configuredItems;
+    component.onRemove(new AutocompleteOption('option1'));
+    expect(component.configuredItems.length).toEqual(2);
+    expect(component.onConfigChange.emit).toHaveBeenCalled();
+
+    fixture.destroy();
+  });
+
+  it('should update', () => {
+    spyOn(component.onConfigChange, 'emit');
+
+    let availableItems: AutocompleteOption[] = [];
+    availableItems.push(new AutocompleteOption('option1'));
+    availableItems.push(new AutocompleteOption('option2'));
+    availableItems.push(new AutocompleteOption('option3'));
+    availableItems.push(new AutocompleteOption('option4'));
+
+    let configuredItems: AutocompleteOption[] = [];
+    let option1 = new AutocompleteOption('option1');
+    configuredItems.push(option1);
+    configuredItems.push(new AutocompleteOption('option3'));
+
+    component.availableItems = availableItems;
+    component.configuredItems = configuredItems;
+
+    component.onUpdate(option1, 'option4');
+    expect(component.configuredItems.length).toEqual(2);
+    expect(component.configuredItems[0].name).toEqual('option4');
+    expect(component.onConfigChange.emit).toHaveBeenCalled();
+
+    fixture.destroy();
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.component.ts b/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.component.ts
new file mode 100644
index 0000000..eb76f40
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.component.ts
@@ -0,0 +1,83 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, Input, Output, EventEmitter } from '@angular/core';
+import {AutocompleteOption} from '../../model/autocomplete-option';
+
+@Component({
+  selector: 'metron-config-multiple-input',
+  templateUrl: './multiple-input.component.html',
+  styleUrls: ['./multiple-input.component.scss']
+})
+export class MultipleInputComponent {
+
+  @Input() type: string = 'text';
+  @Input() allowDuplicates: boolean = true;
+  @Input() configuredItems: AutocompleteOption[] = [];
+  @Input() availableItems: AutocompleteOption[] = [];
+
+  @Output() onConfigChange: EventEmitter<void> = new EventEmitter<void>();
+
+  showAddNew: boolean = false;
+
+  constructor() { }
+
+  getAvailableFunctions(): AutocompleteOption[] {
+    if (this.allowDuplicates) {
+      return this.availableItems;
+    }
+
+    let tAvailable: AutocompleteOption[] = [];
+    let configuredFunctionNames: string[] = [];
+    for (let item of this.configuredItems) {
+      configuredFunctionNames.push(item.name);
+    }
+    for (let item of this.availableItems) {
+      if (configuredFunctionNames.indexOf(item.name) === -1) {
+        tAvailable.push(item);
+      }
+    }
+
+    return tAvailable;
+  }
+
+  getAvailableItemByName(name: string): AutocompleteOption {
+    for (let item of this.availableItems) {
+      if (name === item.name) {
+        return item;
+      }
+    }
+
+    return new AutocompleteOption();
+  }
+
+  onAdd(select: any) {
+    this.configuredItems.push(this.getAvailableItemByName(select.value));
+    select.value = '';
+    this.onConfigChange.emit();
+  }
+
+  onRemove(configuredItem: AutocompleteOption) {
+    this.configuredItems.splice(this.configuredItems.indexOf(configuredItem), 1);
+    this.onConfigChange.emit();
+  }
+
+  onUpdate(configuredItem: AutocompleteOption, value: string) {
+    this.configuredItems.splice(this.configuredItems.indexOf(configuredItem), 1, this.getAvailableItemByName(value));
+    this.onConfigChange.emit();
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.module.ts b/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.module.ts
new file mode 100644
index 0000000..39e0290
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/multiple-input/multiple-input.module.ts
@@ -0,0 +1,13 @@
+import {NgModule} from '@angular/core';
+
+import {MultipleInputComponent}   from './multiple-input.component';
+import {SharedModule} from '../shared.module';
+
+@NgModule({
+    imports: [ SharedModule ],
+    exports: [ MultipleInputComponent ],
+    declarations: [ MultipleInputComponent ],
+    providers: [],
+})
+export class MultipleInputModule {
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/number-spinner/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/number-spinner/index.ts b/metron-interface/metron-config/src/app/shared/number-spinner/index.ts
new file mode 100644
index 0000000..b37c764
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/number-spinner/index.ts
@@ -0,0 +1 @@
+export * from './number-spinner.component';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.component.html b/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.component.html
new file mode 100644
index 0000000..b63908f
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.component.html
@@ -0,0 +1,22 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<div class="input-group spinner">
+  <input type="text" class="form-control" [(ngModel)]="value">
+  <div class="input-group-btn-vertical">
+    <button class="btn btn-default" type="button" (click)="value=value+1" [disabled]="value >= max"><i class="fa fa-caret-up"></i></button>
+    <button class="btn btn-default" type="button" (click)="value=value-1" [disabled]="value <= min"><i class="fa fa-caret-down"></i></button>
+  </div>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.component.scss b/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.component.scss
new file mode 100644
index 0000000..b4352c4
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.component.scss
@@ -0,0 +1,55 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.input-group-btn-vertical
+{
+  width: 2%;
+  position: relative;
+  white-space: nowrap;
+  vertical-align: middle;
+  display: table-cell;
+}
+
+.input-group-btn-vertical > .btn
+{
+  display: block;
+  float: none;
+  width: 100%;
+  max-width: 100%;
+  padding: 8px;
+  margin-left: -1px;
+  position: relative;
+  border-radius: 0;
+}
+
+.input-group-btn-vertical > .btn:first-child
+{
+  border-top-right-radius: 0.25em;
+}
+
+.input-group-btn-vertical > .btn:last-child
+{
+  margin-top: -1px;
+  border-bottom-right-radius: 0.25em;
+}
+
+.input-group-btn-vertical i
+{
+  position: absolute;
+  top: 0;
+  left: 4px;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.component.spec.ts b/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.component.spec.ts
new file mode 100644
index 0000000..ca3042d
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.component.spec.ts
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* tslint:disable:no-unused-variable */
+
+import {NumberSpinnerComponent} from './number-spinner.component';
+
+describe('NumberSpinnerComponent', () => {
+
+  it('should create an instance', () => {
+    expect(new NumberSpinnerComponent()).toBeTruthy();
+  });
+
+  it('spec for all inherited functions', () => {
+    let numberSpinnerComponent = new NumberSpinnerComponent();
+
+    numberSpinnerComponent.writeValue(10);
+    expect(numberSpinnerComponent.innerValue).toEqual(10);
+    expect(numberSpinnerComponent.value).toEqual(10);
+
+    numberSpinnerComponent.registerOnChange((_: any) => {});
+    numberSpinnerComponent.value = 11;
+    expect(numberSpinnerComponent.innerValue).toEqual(11);
+    expect(numberSpinnerComponent.value).toEqual(11);
+
+    numberSpinnerComponent.value = 'abc';
+    expect(numberSpinnerComponent.innerValue).toEqual(11);
+    expect(numberSpinnerComponent.value).toEqual(11);
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.component.ts b/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.component.ts
new file mode 100644
index 0000000..1fd1521
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.component.ts
@@ -0,0 +1,70 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, Input, forwardRef } from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+
+@Component({
+  selector: 'metron-config-number-spinner',
+  templateUrl: './number-spinner.component.html',
+  styleUrls: ['./number-spinner.component.scss'],
+  providers: [
+    {
+      provide: NG_VALUE_ACCESSOR,
+      useExisting: forwardRef(() => NumberSpinnerComponent),
+      multi: true
+    }
+  ]
+})
+export class NumberSpinnerComponent implements ControlValueAccessor {
+
+  @Input() min: number;
+  @Input() max: number;
+
+  innerValue: number = 0;
+
+  private onTouchedCallback ;
+  private onChangeCallback ;
+
+  writeValue(val: any) {
+    this.innerValue = val;
+  }
+
+  registerOnChange(fn: any) {
+    this.onChangeCallback = fn;
+  }
+
+  registerOnTouched(fn: any) {
+    this.onTouchedCallback = fn;
+  }
+
+  get value(): any {
+    if (typeof this.innerValue === 'string') {
+      this.innerValue = parseInt(this.innerValue, 10);
+    }
+    return this.innerValue;
+  };
+
+  set value(v: any) {
+    v = Number(v);
+    if (!isNaN(v) && v !== this.innerValue) {
+      this.innerValue = v;
+      this.onChangeCallback(v);
+    }
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.module.ts b/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.module.ts
new file mode 100644
index 0000000..db622b6
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/number-spinner/number-spinner.module.ts
@@ -0,0 +1,13 @@
+import {NgModule} from '@angular/core';
+
+import {NumberSpinnerComponent}   from './number-spinner.component';
+import {SharedModule} from '../shared.module';
+
+@NgModule({
+    imports: [ SharedModule ],
+    exports: [ NumberSpinnerComponent ],
+    declarations: [ NumberSpinnerComponent ],
+    providers: [],
+})
+export class NumberSpinnerModule {
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/sample-data/sample-data.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/sample-data/sample-data.component.html b/metron-interface/metron-config/src/app/shared/sample-data/sample-data.component.html
new file mode 100644
index 0000000..887211c
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/sample-data/sample-data.component.html
@@ -0,0 +1,22 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<label attr.for="sampleData">SAMPLE&nbsp;( {{sampleDataIndex + 1}} of {{sampleData.length}} )</label>
+<div>
+  <i class="fa fa-caret-left sample-iterator sample-unavailable" aria-hidden="true" [class.sample-available]="sampleDataIndex > 0" [class.sample-unavailable]="sampleDataIndex < 1" (click)="getPreviousSample()"></i>
+  <textarea #sampleDataElement type="text" class="form-control sample-input" name="sampleData"  [ngModel]="sampleData[sampleDataIndex]" (blur)="onBlur(sample)"
+            [attr.placeholder]="placeHolderText" (focus)="sampleDataElement.placeholder=''"> </textarea>
+  <i class="fa fa-caret-right sample-iterator sample-available" aria-hidden="true" (click)="getNextSample()"></i>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/sample-data/sample-data.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/sample-data/sample-data.component.scss b/metron-interface/metron-config/src/app/shared/sample-data/sample-data.component.scss
new file mode 100644
index 0000000..918e468
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/sample-data/sample-data.component.scss
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../../_variables.scss";
+
+.sample-input {
+  height:60px;
+  display:inline-block;
+  width: 94%;
+  resize: none;
+  margin-right: -4px;
+  margin-left: -4px;
+  border-radius: inherit;
+}
+
+.sample-iterator {
+  background-color:#195d68;
+  vertical-align: top;
+  width: 3%;
+  height: 60px;
+  padding-top: 22px;
+  padding-left: 6px;
+  cursor: pointer;
+}
+
+.sample-available {
+  color: #27aae1;
+}
+
+.sample-unavailable {
+  color: #404040;
+}
+
+
+textarea {
+  &::-webkit-input-placeholder {
+    @include place-holder-text;
+  }
+  &:-moz-placeholder {
+    @include place-holder-text;
+  }
+
+  &::-moz-placeholder {
+    @include place-holder-text;
+  }
+
+  &:-ms-input-placeholder {
+    @include place-holder-text;
+  }
+}
+
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/sample-data/sample-data.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/sample-data/sample-data.component.spec.ts b/metron-interface/metron-config/src/app/shared/sample-data/sample-data.component.spec.ts
new file mode 100644
index 0000000..5488209
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/sample-data/sample-data.component.spec.ts
@@ -0,0 +1,188 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, TestBed, ComponentFixture} from '@angular/core/testing';
+import {KafkaService} from '../../service/kafka.service';
+import {Observable} from  'rxjs/Observable';
+import {SampleDataComponent} from './sample-data.component';
+import {SharedModule} from '../shared.module';
+import '../../rxjs-operators';
+
+class MockKafkaService {
+  _sample: string[];
+  _sampleCounter: number = 0;
+
+
+  public setSample(sampleMessages: string[]): void {
+    this._sample = sampleMessages;
+    this._sampleCounter = 0;
+  }
+
+  public sample(name: string): Observable<string> {
+
+    if (this._sampleCounter < this._sample.length) {
+      return Observable.create(observer => {
+        observer.next(this._sample[this._sampleCounter++]);
+        observer.complete();
+      });
+    }
+
+    return Observable.throw('Error');
+  }
+}
+
+describe('SampleDataComponent', () => {
+  let fixture: ComponentFixture<SampleDataComponent>;
+  let sampleDataComponent: SampleDataComponent;
+  let kafkaService: MockKafkaService;
+  let sampleMessages: string[] = [
+    'This is first message',
+    'This is second message',
+    'This is third message'
+  ];
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [SharedModule],
+      declarations: [ SampleDataComponent],
+      providers: [
+        SampleDataComponent,
+        {provide: KafkaService, useClass: MockKafkaService}
+      ]
+    });
+
+    fixture = TestBed.createComponent(SampleDataComponent);
+    sampleDataComponent = fixture.componentInstance;
+    kafkaService = fixture.debugElement.injector.get(KafkaService);
+
+  }));
+
+  it('can instantiate SampleDataComponent', async(() => {
+    expect(sampleDataComponent instanceof SampleDataComponent).toBe(true);
+  }));
+
+
+  it('should emmit messages', async(() => {
+    let expectedMessage;
+    let successCount = 0;
+    let failureCount = 0;
+
+    kafkaService.setSample(sampleMessages);
+
+    sampleDataComponent.onSampleDataChanged.subscribe((message: string) => {
+      ++successCount;
+      expect(message).toEqual(expectedMessage);
+    });
+
+    sampleDataComponent.onSampleDataNotAvailable.subscribe(() => {
+      ++failureCount;
+    });
+
+    spyOn(sampleDataComponent.onSampleDataNotAvailable, 'subscribe');
+    spyOn(sampleDataComponent.onSampleDataChanged, 'subscribe');
+
+    expectedMessage = sampleMessages[0];
+    sampleDataComponent.getNextSample();
+    expect(successCount).toEqual(1);
+    expect(failureCount).toEqual(0);
+
+    expectedMessage = sampleMessages[1];
+    sampleDataComponent.getNextSample();
+    expect(successCount).toEqual(2);
+    expect(failureCount).toEqual(0);
+
+    expectedMessage = sampleMessages[2];
+    sampleDataComponent.getNextSample();
+    expect(successCount).toEqual(3);
+    expect(failureCount).toEqual(0);
+
+    let tmp;
+    expectedMessage = tmp;
+    sampleDataComponent.getNextSample();
+    expect(successCount).toEqual(3);
+    expect(failureCount).toEqual(1);
+
+    expectedMessage = sampleMessages[1];
+    sampleDataComponent.getPreviousSample();
+    expect(successCount).toEqual(4);
+    expect(failureCount).toEqual(1);
+
+    expectedMessage = sampleMessages[0];
+    sampleDataComponent.getPreviousSample();
+    expect(successCount).toEqual(5);
+    expect(failureCount).toEqual(1);
+
+    expectedMessage = sampleMessages[1];
+    sampleDataComponent.getNextSample();
+    expect(successCount).toEqual(6);
+    expect(failureCount).toEqual(1);
+
+    expectedMessage = sampleMessages[0];
+    sampleDataComponent.getPreviousSample();
+    expect(successCount).toEqual(7);
+    expect(failureCount).toEqual(1);
+
+    expectedMessage = tmp;
+    sampleDataComponent.getPreviousSample();
+    expect(successCount).toEqual(7);
+    expect(failureCount).toEqual(1);
+
+  }));
+
+  it('should emmit messages on blur', async(() => {
+
+    let expectedMessage;
+    let successCount = 0;
+
+    kafkaService.setSample(sampleMessages);
+
+    sampleDataComponent.onSampleDataChanged.subscribe((message: string) => {
+      ++successCount;
+      expect(message).toEqual(expectedMessage);
+    });
+
+
+    expectedMessage = 'This is a simple message';
+    fixture.debugElement.nativeElement.querySelector('textarea').value = expectedMessage;
+    sampleDataComponent.onBlur();
+
+    expect(successCount).toEqual(1);
+    expect(sampleDataComponent.sampleDataIndex).toEqual(0);
+    expect(sampleDataComponent.sampleData.length).toEqual(1);
+    expect(sampleDataComponent.sampleData[0]).toEqual(expectedMessage);
+
+
+    expectedMessage = '';
+    fixture.debugElement.nativeElement.querySelector('textarea').value = expectedMessage;
+    sampleDataComponent.onBlur();
+
+    expect(successCount).toEqual(2);
+    expect(sampleDataComponent.sampleDataIndex).toEqual(0);
+    expect(sampleDataComponent.sampleData.length).toEqual(1);
+
+
+    expectedMessage = sampleMessages[0];
+    sampleDataComponent.getNextSample();
+
+    expect(successCount).toEqual(3);
+    expect(sampleDataComponent.sampleDataIndex).toEqual(1);
+    expect(sampleDataComponent.sampleData.length).toEqual(2);
+    expect(sampleDataComponent.sampleData[1]).toEqual(sampleMessages[0]);
+
+  }));
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/sample-data/sample-data.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/sample-data/sample-data.component.ts b/metron-interface/metron-config/src/app/shared/sample-data/sample-data.component.ts
new file mode 100644
index 0000000..801e883
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/sample-data/sample-data.component.ts
@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Component, Input, Output, EventEmitter, ViewChild, ElementRef} from '@angular/core';
+import {KafkaService} from '../../service/kafka.service';
+
+@Component({
+  selector: 'metron-config-sample-data',
+  templateUrl: 'sample-data.component.html',
+  styleUrls: ['sample-data.component.scss'],
+})
+
+export class SampleDataComponent {
+
+  @Input() topic: string;
+  @Output() onSampleDataChanged = new EventEmitter<string>();
+  @Output() onSampleDataNotAvailable = new EventEmitter<void>();
+
+  @ViewChild('sampleDataElement') sampleDataElement: ElementRef;
+
+  sampleData: string[] = [];
+  sampleDataIndex: number = -1;
+  placeHolderText = 'Paste Sample Message' + '\n' +
+                    'A data sample cannot automatically be loaded. Connect to a Kafka Topic or paste a message here.';
+
+
+  constructor(private kafkaService: KafkaService) {
+  }
+
+
+  getPreviousSample() {
+    if (this.sampleDataIndex > 0) {
+      this.sampleDataIndex = this.sampleDataIndex - 1;
+      this.onSampleDataChanged.emit(this.sampleData[this.sampleDataIndex]);
+    }
+  }
+
+  getNextSample() {
+    if (this.sampleData.length - 1 === this.sampleDataIndex) {
+      this.kafkaService.sample(this.topic).subscribe((sampleData: string) => {
+          this.sampleDataIndex = this.sampleDataIndex + 1;
+          this.sampleData[this.sampleDataIndex] = sampleData;
+          this.onSampleDataChanged.emit(this.sampleData[this.sampleDataIndex]);
+        },
+        error => {
+          this.onSampleDataNotAvailable.emit();
+        });
+    } else {
+      this.sampleDataIndex = this.sampleDataIndex + 1;
+      this.onSampleDataChanged.emit(this.sampleData[this.sampleDataIndex]);
+    }
+
+  }
+
+  onBlur() {
+    let currentValue = this.sampleDataElement.nativeElement.value;
+
+    if (currentValue.trim() !== '' && this.sampleData[this.sampleDataIndex] !== currentValue) {
+      this.sampleDataIndex = this.sampleDataIndex + 1;
+      this.sampleData[this.sampleDataIndex] = currentValue;
+      this.onSampleDataChanged.emit(this.sampleData[this.sampleDataIndex]);
+    }
+
+    if (currentValue.trim() === '') {
+      this.sampleDataElement.nativeElement.placeholder = this.placeHolderText;
+      this.onSampleDataChanged.emit('');
+    }
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/sample-data/sample-data.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/sample-data/sample-data.module.ts b/metron-interface/metron-config/src/app/shared/sample-data/sample-data.module.ts
new file mode 100644
index 0000000..e700903
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/sample-data/sample-data.module.ts
@@ -0,0 +1,13 @@
+import {NgModule} from '@angular/core';
+
+import {SampleDataComponent}   from './sample-data.component';
+import {SharedModule} from '../shared.module';
+
+@NgModule({
+    imports: [ SharedModule ],
+    exports: [ SampleDataComponent ],
+    declarations: [ SampleDataComponent ],
+    providers: [],
+})
+export class SampleDataModule {
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/shared/shared.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/shared/shared.module.ts b/metron-interface/metron-config/src/app/shared/shared.module.ts
new file mode 100644
index 0000000..4b2c4cc
--- /dev/null
+++ b/metron-interface/metron-config/src/app/shared/shared.module.ts
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule }            from '@angular/core';
+import { CommonModule }        from '@angular/common';
+import { FormsModule }         from '@angular/forms';
+import {MetronModalComponent} from './metron-modal/metron-modal.component';
+
+@NgModule({
+  imports:      [ CommonModule ],
+  declarations: [ MetronModalComponent ],
+  exports:      [ MetronModalComponent,
+    CommonModule, FormsModule ]
+})
+export class SharedModule { }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/util/enums.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/util/enums.ts b/metron-interface/metron-config/src/app/util/enums.ts
new file mode 100644
index 0000000..b89af94
--- /dev/null
+++ b/metron-interface/metron-config/src/app/util/enums.ts
@@ -0,0 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export enum Sort {
+    ASC,
+    DSC
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/util/httpUtil.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/util/httpUtil.ts b/metron-interface/metron-config/src/app/util/httpUtil.ts
new file mode 100644
index 0000000..a93e66e
--- /dev/null
+++ b/metron-interface/metron-config/src/app/util/httpUtil.ts
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Response} from '@angular/http';
+import {Observable}     from 'rxjs/Observable';
+import {RestError} from '../model/rest-error';
+export class HttpUtil {
+
+  public static extractString(res: Response): string {
+    let text: string = res.text();
+    return text || '';
+  }
+
+  public static extractData(res: Response): any {
+    let body = res.json();
+    return body || {};
+  }
+
+  public static handleError(res: Response): Observable<RestError> {
+    // In a real world app, we might use a remote logging infrastructure
+    // We'd also dig deeper into the error to get a better message
+    let restError: RestError;
+    if (res.status !== 404) {
+      restError = res.json();
+    } else {
+      restError = new RestError();
+      restError.responseCode = 404;
+    }
+    return Observable.throw(restError);
+  }
+}



[07/12] incubator-metron git commit: METRON-623 Management UI [contributed by Raghu Mitra Kandikonda and Ryan Merriman] closes apache/incubator-metron#489

Posted by rm...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.spec.ts
new file mode 100644
index 0000000..6c4eab1
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.spec.ts
@@ -0,0 +1,1014 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Inject} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Router, ActivatedRoute, Params} from '@angular/router';
+import {Http, RequestOptions, Response, ResponseOptions} from '@angular/http';
+import {SensorParserConfigComponent, Pane, KafkaStatus} from './sensor-parser-config.component';
+import {StellarService} from '../../service/stellar.service';
+import {SensorParserConfigService} from '../../service/sensor-parser-config.service';
+import {KafkaService} from '../../service/kafka.service';
+import {KafkaTopic} from '../../model/kafka-topic';
+import {GrokValidationService} from '../../service/grok-validation.service';
+import {MetronAlerts} from '../../shared/metron-alerts';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import {ParseMessageRequest} from '../../model/parse-message-request';
+import {SensorParserContext} from '../../model/sensor-parser-context';
+import {AuthenticationService} from '../../service/authentication.service';
+import {FieldTransformer} from '../../model/field-transformer';
+import {SensorParserConfigModule} from './sensor-parser-config.module';
+import {SensorEnrichmentConfigService} from '../../service/sensor-enrichment-config.service';
+import {SensorEnrichmentConfig} from '../../model/sensor-enrichment-config';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../../app.config';
+import {IAppConfig} from '../../app.config.interface';
+import {SensorIndexingConfigService} from '../../service/sensor-indexing-config.service';
+import {IndexingConfigurations} from '../../model/sensor-indexing-config';
+import '../../rxjs-operators';
+import 'rxjs/add/observable/of';
+import {HdfsService} from '../../service/hdfs.service';
+import {RestError} from '../../model/rest-error';
+import {RiskLevelRule} from '../../model/risk-level-rule';
+
+
+class MockRouter {
+  navigateByUrl(url: string) {}
+}
+
+class MockActivatedRoute {
+  private name: string;
+  params: Observable<Params>;
+
+  setNameForTest(name: string) {
+    this.name = name;
+    this.params = Observable.create(observer => {
+      observer.next({id: this.name});
+      observer.complete();
+    });
+  }
+}
+
+class MockSensorParserConfigService extends SensorParserConfigService {
+  private sensorParserConfig: SensorParserConfig;
+  private parsedMessage: any;
+  private postedSensorParserConfig: SensorParserConfig;
+  private throwError: boolean;
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public post(sensorParserConfig: SensorParserConfig): Observable<SensorParserConfig> {
+    if (this.throwError) {
+      let error = new RestError();
+      error.message = 'SensorParserConfig post error';
+      return Observable.throw(error);
+    }
+    this.postedSensorParserConfig = sensorParserConfig;
+    return Observable.create(observer => {
+      observer.next(sensorParserConfig);
+      observer.complete();
+    });
+  }
+
+  public get(name: string): Observable<SensorParserConfig> {
+    return Observable.create(observer => {
+      observer.next(this.sensorParserConfig);
+      observer.complete();
+    });
+  }
+
+  public getAvailableParsers(): Observable<{}> {
+    return Observable.create(observer => {
+      observer.next({
+        'Bro': 'org.apache.metron.parsers.bro.BasicBroParser',
+        'Grok': 'org.apache.metron.parsers.GrokParser'
+      });
+      observer.complete();
+    });
+  }
+
+  public parseMessage(parseMessageRequest: ParseMessageRequest): Observable<{}> {
+    return Observable.create(observer => {
+      observer.next(this.parsedMessage);
+      observer.complete();
+    });
+  }
+
+  public setSensorParserConfig(result: any) {
+    this.sensorParserConfig = result;
+  }
+
+  public setParsedMessage(parsedMessage: any) {
+    this.parsedMessage = parsedMessage;
+  }
+
+  public setThrowError(throwError: boolean) {
+    this.throwError = throwError;
+  }
+
+  public getPostedSensorParserConfig() {
+    return this.postedSensorParserConfig;
+  }
+}
+
+class MockSensorIndexingConfigService extends SensorIndexingConfigService {
+  private name: string;
+  private postedIndexingConfigurations: IndexingConfigurations;
+  private sensorIndexingConfig: IndexingConfigurations;
+  private throwError: boolean;
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public post(name: string, sensorIndexingConfig: IndexingConfigurations): Observable<IndexingConfigurations> {
+    if (this.throwError) {
+      let error = new RestError();
+      error.message = 'IndexingConfigurations post error';
+      return Observable.throw(error);
+    }
+    this.postedIndexingConfigurations = sensorIndexingConfig;
+    return Observable.create(observer => {
+      observer.next(sensorIndexingConfig);
+      observer.complete();
+    });
+  }
+
+  public get(name: string): Observable<IndexingConfigurations> {
+    if (this.sensorIndexingConfig === null) {
+      let error = new RestError();
+      error.message = 'IndexingConfigurations get error';
+      return Observable.throw(error);
+    }
+    return Observable.create(observer => {
+      if (name === this.name) {
+        observer.next(this.sensorIndexingConfig);
+      }
+      observer.complete();
+    });
+  }
+
+  public setSensorIndexingConfig(name: string, result: IndexingConfigurations) {
+    this.name = name;
+    this.sensorIndexingConfig = result;
+  }
+
+  public setThrowError(throwError: boolean) {
+    this.throwError = throwError;
+  }
+
+  public getPostedIndexingConfigurations(): IndexingConfigurations {
+    return this.postedIndexingConfigurations;
+  }
+}
+
+class MockKafkaService extends KafkaService {
+  private name: string;
+  private kafkaTopic: KafkaTopic;
+  private kafkaTopicForPost: KafkaTopic;
+  private sampleData = {'key1': 'value1', 'key2': 'value2'};
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public setKafkaTopic(name: string, kafkaTopic: KafkaTopic) {
+    this.name = name;
+    this.kafkaTopic = kafkaTopic;
+  }
+
+  public setSampleData(name: string, sampleData?: any) {
+    this.name = name;
+    this.sampleData = sampleData;
+  }
+
+  public sample(name: string): Observable<string> {
+    if (this.sampleData === null) {
+      return Observable.throw(new RestError());
+    }
+    return Observable.create(observer => {
+      if (name === this.name) {
+        observer.next(JSON.stringify(this.sampleData));
+      }
+      observer.complete();
+    });
+  }
+
+  public get(name: string): Observable<KafkaTopic> {
+    if (this.kafkaTopic === null) {
+      return Observable.throw(new RestError());
+    }
+    return Observable.create(observer => {
+      if (name === this.name) {
+        observer.next(this.kafkaTopic);
+      }
+      observer.complete();
+    });
+  }
+
+  public post(k: KafkaTopic): Observable<KafkaTopic> {
+    this.kafkaTopicForPost = k;
+    console.log('called post MockKafkaService: ' + this.kafkaTopicForPost);
+    return Observable.create(observer => {
+      observer.next({});
+      observer.complete();
+    });
+  }
+
+  public getKafkaTopicForPost(): KafkaTopic {
+    console.log('Called get MockKafkaService: ' + this.kafkaTopicForPost);
+    return this.kafkaTopicForPost;
+  }
+}
+
+class MockGrokValidationService extends GrokValidationService {
+
+  private path: string;
+  private contents: string;
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public setContents(path: string, contents: string) {
+    this.path = path;
+    this.contents = contents;
+  }
+
+  public list(): Observable<string[]> {
+    return Observable.create(observer => {
+      observer.next({
+        'BASE10NUM': '(?<![0-9.+-])(?>[+-]?(?:(?:[0-9]+(?:\\.[0-9]+)?)|(?:\\.[0-9]+)))',
+        'BASE16FLOAT': '\\b(?<![0-9A-Fa-f.])(?:[+-]?(?:0x)?(?:(?:[0-9A-Fa-f]+(?:\\.[0-9A-Fa-f]*)?)|(?:\\.[0-9A-Fa-f]+)))\\b',
+        'BASE16NUM': '(?<![0-9A-Fa-f])(?:[+-]?(?:0x)?(?:[0-9A-Fa-f]+))',
+        'CISCOMAC': '(?:(?:[A-Fa-f0-9]{4}\\.){2}[A-Fa-f0-9]{4})',
+        'COMMONMAC': '(?:(?:[A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2})',
+        'DATA': '.*?'
+      });
+      observer.complete();
+    });
+  }
+
+
+  public getStatement(path: string): Observable<string> {
+    if (this.contents === null) {
+      return Observable.throw('Error');
+    }
+    return Observable.create(observer => {
+      if (path === this.path) {
+        observer.next(this.contents);
+      }
+      observer.complete();
+    });
+  }
+}
+
+class MockHdfsService extends HdfsService {
+  private fileList: string[];
+  private path: string;
+  private contents: string;
+  private postedContents: string;
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public setFileList(path: string, fileList: string[]) {
+    this.path = path;
+    this.fileList = fileList;
+  }
+
+  public setContents(path: string, contents: string) {
+    this.path = path;
+    this.contents = contents;
+  }
+
+  public list(path: string): Observable<string[]> {
+    if (this.fileList === null) {
+      return Observable.throw('Error');
+    }
+    return Observable.create(observer => {
+      if (path === this.path) {
+        observer.next(this.fileList);
+      }
+      observer.complete();
+    });
+  }
+
+  public read(path: string): Observable<string> {
+    if (this.contents === null) {
+      return Observable.throw('Error');
+    }
+    return Observable.create(observer => {
+      if (path === this.path) {
+        observer.next(this.contents);
+      }
+      observer.complete();
+    });
+  }
+
+  public post(path: string, contents: string): Observable<{}> {
+    if (this.contents === null) {
+      let error = new RestError();
+      error.message = 'HDFS post Error';
+      return Observable.throw(error);
+    }
+    this.postedContents = contents;
+    return Observable.create(observer => {
+      observer.next(contents);
+      observer.complete();
+    });
+  }
+
+  public deleteFile(path: string): Observable<Response> {
+    return Observable.create(observer => {
+      observer.next({});
+      observer.complete();
+    });
+  }
+
+  public getPostedContents() {
+    return this.postedContents;
+  }
+}
+
+class MockAuthenticationService extends AuthenticationService {
+
+  constructor(private http2: Http, private router2: Router, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, router2, config2);
+  }
+
+  public getCurrentUser(options: RequestOptions): Observable<Response> {
+    let responseOptions: ResponseOptions = new ResponseOptions();
+    responseOptions.body = 'user';
+    let response: Response = new Response(responseOptions);
+    return Observable.of(response);
+  };
+}
+
+class MockTransformationValidationService extends StellarService {
+
+  private transformationValidationResult: any;
+  private transformationValidationForValidate: SensorParserContext;
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public setTransformationValidationResultForTest(transformationValidationResult: any): void {
+    this.transformationValidationResult = transformationValidationResult;
+  }
+
+  public getTransformationValidationForValidate(): SensorParserContext {
+    return this.transformationValidationForValidate;
+  }
+
+  public validate(t: SensorParserContext): Observable<{}> {
+    this.transformationValidationForValidate = t;
+    return Observable.create(observer => {
+      observer.next(this.transformationValidationResult);
+      observer.complete();
+    });
+  }
+}
+
+export class MockSensorEnrichmentConfigService {
+  private name: string;
+  private sensorEnrichmentConfig: SensorEnrichmentConfig;
+  private postedSensorEnrichmentConfig: SensorEnrichmentConfig;
+  private throwError: boolean;
+
+  public setSensorEnrichmentConfig(name: string, sensorEnrichmentConfig: SensorEnrichmentConfig) {
+    this.name = name;
+    this.sensorEnrichmentConfig = sensorEnrichmentConfig;
+  }
+
+  public get(name: string): Observable<SensorEnrichmentConfig> {
+    if (this.sensorEnrichmentConfig === null) {
+      let error = new RestError();
+      error.message = 'SensorEnrichmentConfig get error';
+      return Observable.throw(error);
+    }
+    return Observable.create(observer => {
+      if (name === this.name) {
+        observer.next(this.sensorEnrichmentConfig);
+      }
+      observer.complete();
+    });
+  }
+
+  public post(name: string, sensorEnrichmentConfig: SensorEnrichmentConfig): Observable<SensorEnrichmentConfig> {
+    if (this.throwError) {
+      let error = new RestError();
+      error.message = 'SensorEnrichmentConfig post error';
+      return Observable.throw(error);
+    }
+    this.postedSensorEnrichmentConfig = sensorEnrichmentConfig;
+    return  Observable.create(observer => {
+      observer.next(sensorEnrichmentConfig);
+      observer.complete();
+    });
+  }
+
+  public setThrowError(throwError: boolean) {
+    this.throwError = throwError;
+  }
+
+  public getPostedSensorEnrichmentConfig() {
+    return this.postedSensorEnrichmentConfig;
+  }
+}
+
+describe('Component: SensorParserConfig', () => {
+
+  let component: SensorParserConfigComponent;
+  let fixture: ComponentFixture<SensorParserConfigComponent>;
+  let sensorParserConfigService: MockSensorParserConfigService;
+  let sensorEnrichmentConfigService: MockSensorEnrichmentConfigService;
+  let sensorIndexingConfigService: MockSensorIndexingConfigService;
+  let transformationValidationService: MockTransformationValidationService;
+  let kafkaService: MockKafkaService;
+  let hdfsService: MockHdfsService;
+  let grokValidationService: MockGrokValidationService;
+  let activatedRoute: MockActivatedRoute;
+  let metronAlerts: MetronAlerts;
+  let router: MockRouter;
+
+  let squidSensorParserConfig: any = {
+    'parserClassName': 'org.apache.metron.parsers.GrokParser',
+    'sensorTopic': 'squid',
+    'parserConfig': {
+      'grokPath': '/apps/metron/patterns/squid',
+      'patternLabel': 'SQUID_DELIMITED',
+      'timestampField': 'timestamp'
+    },
+    'fieldTransformations': [
+      {
+        'input': [],
+        'output': ['full_hostname', 'domain_without_subdomains', 'hostname'],
+        'transformation': 'STELLAR',
+        'config': {
+          'full_hostname': 'URL_TO_HOST(url)',
+          'domain_without_subdomains': 'DOMAIN_REMOVE_SUBDOMAINS(full_hostname)'
+        }
+      }
+    ],
+  };
+
+  let squidSensorEnrichmentConfig = {
+    'enrichment': {
+      'fieldMap': {
+        'geo': ['ip_dst_addr'],
+        'host': ['ip_dst_addr'],
+        'whois': [],
+        'stellar': { 'config': { 'group1': {} }}
+      },
+      'fieldToTypeMap': {}, 'config': {}
+    },
+    'threatIntel': {
+      'threatIntel': {
+        'fieldMap': { 'hbaseThreatIntel': ['ip_dst_addr'] },
+        'fieldToTypeMap': { 'ip_dst_addr': ['malicious_ip'] }
+      }
+    }
+  };
+
+  let squidIndexingConfigurations = {
+    'hdfs': {
+      'index': 'squid',
+      'batchSize': 5,
+      'enabled': true
+    },
+    'elasticsearch': {
+      'index': 'squid',
+      'batchSize': 10,
+      'enabled': true
+    },
+    'solr': {
+      'index': 'squid',
+      'batchSize': 1,
+      'enabled': false
+    },
+  };
+
+  beforeEach(async(() => {
+
+    TestBed.configureTestingModule({
+      imports: [SensorParserConfigModule],
+      providers: [
+        MetronAlerts,
+        {provide: Http},
+        {provide: SensorParserConfigService, useClass: MockSensorParserConfigService},
+        {provide: SensorIndexingConfigService, useClass: MockSensorIndexingConfigService},
+        {provide: KafkaService, useClass: MockKafkaService},
+        {provide: HdfsService, useClass: MockHdfsService},
+        {provide: GrokValidationService, useClass: MockGrokValidationService},
+        {provide: StellarService, useClass: MockTransformationValidationService},
+        {provide: ActivatedRoute, useClass: MockActivatedRoute},
+        {provide: Router, useClass: MockRouter},
+        {provide: AuthenticationService, useClass: MockAuthenticationService},
+        {provide: SensorEnrichmentConfigService, useClass: MockSensorEnrichmentConfigService},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    }).compileComponents()
+      .then(() => {
+        fixture = TestBed.createComponent(SensorParserConfigComponent);
+        component = fixture.componentInstance;
+        sensorParserConfigService = fixture.debugElement.injector.get(SensorParserConfigService);
+        sensorIndexingConfigService = fixture.debugElement.injector.get(SensorIndexingConfigService);
+        transformationValidationService = fixture.debugElement.injector.get(StellarService);
+        kafkaService = fixture.debugElement.injector.get(KafkaService);
+        hdfsService = fixture.debugElement.injector.get(HdfsService);
+        grokValidationService = fixture.debugElement.injector.get(GrokValidationService);
+        sensorEnrichmentConfigService = fixture.debugElement.injector.get(SensorEnrichmentConfigService);
+        activatedRoute = fixture.debugElement.injector.get(ActivatedRoute);
+        metronAlerts = fixture.debugElement.injector.get(MetronAlerts);
+        router = fixture.debugElement.injector.get(Router);
+      });
+
+  }));
+
+  it('should create an instance of SensorParserConfigComponent', async(() => {
+    expect(component).toBeDefined();
+
+    fixture.destroy();
+  }));
+
+  it('should handle ngOnInit', async(() => {
+    spyOn(component, 'init');
+    spyOn(component, 'createForms');
+    spyOn(component, 'getAvailableParsers');
+
+    activatedRoute.setNameForTest('squid');
+
+    component.ngOnInit();
+
+    expect(component.init).toHaveBeenCalledWith('squid');
+    expect(component.createForms).toHaveBeenCalled();
+    expect(component.getAvailableParsers).toHaveBeenCalled();
+
+    fixture.destroy();
+  }));
+
+  it('should createForms', async(() => {
+    component.sensorParserConfig = Object.assign(new SensorParserConfig(), squidSensorParserConfig);
+    component.createForms();
+
+    expect(Object.keys(component.sensorConfigForm.controls).length).toEqual(15);
+    expect(Object.keys(component.transformsValidationForm.controls).length).toEqual(2);
+    expect(component.showAdvancedParserConfiguration).toEqual(true);
+
+    component.sensorParserConfig.parserConfig = {};
+    component.showAdvancedParserConfiguration = false;
+    component.createForms();
+    expect(component.showAdvancedParserConfiguration).toEqual(false);
+
+    fixture.destroy();
+  }));
+
+  it('should getAvailableParsers', async(() => {
+    component.getAvailableParsers();
+    expect(component.availableParsers).toEqual({
+      'Bro': 'org.apache.metron.parsers.bro.BasicBroParser',
+      'Grok': 'org.apache.metron.parsers.GrokParser'
+    });
+    expect(component.availableParserNames).toEqual(['Bro', 'Grok']);
+
+    fixture.destroy();
+  }));
+
+  it('should init', async(() => {
+    component.init('new');
+
+    let expectedSensorParserConfig = new SensorParserConfig();
+    expectedSensorParserConfig.parserClassName = 'org.apache.metron.parsers.GrokParser';
+    expect(component.sensorParserConfig).toEqual(expectedSensorParserConfig);
+    expect(component.sensorEnrichmentConfig).toEqual(new SensorEnrichmentConfig());
+    expect(component.indexingConfigurations).toEqual(new IndexingConfigurations());
+    expect(component.editMode).toEqual(false);
+
+    spyOn(component, 'getKafkaStatus');
+    let sensorParserConfig = Object.assign(new SensorParserConfig(), squidSensorParserConfig);
+    sensorParserConfigService.setSensorParserConfig(sensorParserConfig);
+    sensorEnrichmentConfigService.setSensorEnrichmentConfig('squid',
+        Object.assign(new SensorEnrichmentConfig(), squidSensorEnrichmentConfig));
+    sensorIndexingConfigService.setSensorIndexingConfig('squid',
+        Object.assign(new IndexingConfigurations(), squidIndexingConfigurations));
+    hdfsService.setContents('/apps/metron/patterns/squid', 'SQUID_DELIMITED grok statement');
+
+    component.init('squid');
+    expect(component.sensorParserConfig).toEqual(Object.assign(new SensorParserConfig(), squidSensorParserConfig));
+    expect(component.sensorNameValid).toEqual(true);
+    expect(component.getKafkaStatus).toHaveBeenCalled();
+    expect(component.showAdvancedParserConfiguration).toEqual(true);
+    expect(component.grokStatement).toEqual('SQUID_DELIMITED grok statement');
+    expect(component.patternLabel).toEqual('SQUID_DELIMITED');
+    expect(component.sensorEnrichmentConfig).toEqual(Object.assign(new SensorEnrichmentConfig(), squidSensorEnrichmentConfig));
+    expect(component.indexingConfigurations).toEqual(Object.assign(new IndexingConfigurations(), squidIndexingConfigurations));
+
+    component.sensorParserConfig.parserConfig['grokPath'] = '/patterns/squid';
+    hdfsService.setContents('/patterns/squid', null);
+    grokValidationService.setContents('/patterns/squid', 'SQUID grok statement from classpath');
+
+    component.init('squid');
+    expect(component.grokStatement).toEqual('SQUID grok statement from classpath');
+
+    spyOn(metronAlerts, 'showErrorMessage');
+
+    sensorEnrichmentConfigService.setSensorEnrichmentConfig('squid', null);
+    component.init('squid');
+    expect(metronAlerts.showErrorMessage).toHaveBeenCalledWith('SensorEnrichmentConfig get error');
+
+    sensorIndexingConfigService.setSensorIndexingConfig('squid', null);
+    component.init('squid');
+    expect(metronAlerts.showErrorMessage).toHaveBeenCalledWith('IndexingConfigurations get error');
+
+    grokValidationService.setContents('/patterns/squid', null);
+
+    component.init('squid');
+    expect(metronAlerts.showErrorMessage).toHaveBeenCalledWith('Could not find grok statement in HDFS or classpath at /patterns/squid');
+
+    sensorParserConfig = new SensorParserConfig();
+    sensorParserConfig.sensorTopic = 'bro';
+    sensorParserConfigService.setSensorParserConfig(sensorParserConfig);
+    component.showAdvancedParserConfiguration = false;
+
+    component.init('bro');
+    expect(component.showAdvancedParserConfiguration).toEqual(false);
+
+    fixture.destroy();
+  }));
+
+  it('should getMessagePrefix', async(() => {
+    component.getAvailableParsers();
+    expect(component.getMessagePrefix()).toEqual('Created');
+    component.editMode = true;
+    expect(component.getMessagePrefix()).toEqual('Modified');
+
+    fixture.destroy();
+  }));
+
+  it('should handle onSetSensorName', async(() => {
+    spyOn(component, 'getKafkaStatus');
+    spyOn(component, 'isConfigValid');
+
+    component.onSetSensorName();
+    expect(component.getKafkaStatus).not.toHaveBeenCalled();
+    expect(component.isConfigValid).toHaveBeenCalled();
+
+    component.sensorParserConfig.sensorTopic = 'bro';
+    component.onSetSensorName();
+    expect(component.sensorNameValid).toEqual(true);
+    expect(component.getKafkaStatus).toHaveBeenCalled();
+
+    fixture.destroy();
+  }));
+
+  it('should handle onParserTypeChange', async(() => {
+    spyOn(component, 'hidePane');
+    spyOn(component, 'isConfigValid');
+
+    component.onParserTypeChange();
+    expect(component.hidePane).not.toHaveBeenCalled();
+    expect(component.isConfigValid).toHaveBeenCalled();
+
+    component.sensorParserConfig.parserClassName = 'org.apache.metron.parsers.GrokParser';
+    component.onParserTypeChange();
+    expect(component.parserClassValid).toEqual(true);
+    expect(component.hidePane).not.toHaveBeenCalled();
+
+    component.sensorParserConfig.parserClassName = 'org.apache.metron.parsers.bro.BasicBroParser';
+    component.onParserTypeChange();
+    expect(component.hidePane).toHaveBeenCalledWith(Pane.GROK);
+
+    fixture.destroy();
+  }));
+
+  it('should handle onGrokStatementChange', async(() => {
+    spyOn(component, 'isConfigValid');
+
+    component.onGrokStatementChange();
+    expect(component.grokStatementValid).toEqual(false);
+    expect(component.isConfigValid).toHaveBeenCalled();
+
+    component.grokStatement = 'grok statement';
+    component.onGrokStatementChange();
+    expect(component.grokStatementValid).toEqual(true);
+
+    fixture.destroy();
+  }));
+
+  it('should handle isConfigValid', async(() => {
+    component.isConfigValid();
+    expect(component.configValid).toEqual(false);
+
+    component.sensorNameValid = true;
+    component.parserClassValid = true;
+
+    component.isConfigValid();
+    expect(component.configValid).toEqual(true);
+
+    component.sensorParserConfig.parserClassName = 'org.apache.metron.parsers.GrokParser';
+    component.isConfigValid();
+    expect(component.configValid).toEqual(false);
+
+    component.grokStatementValid = true;
+    component.isConfigValid();
+    expect(component.configValid).toEqual(true);
+
+    fixture.destroy();
+  }));
+
+  it('should getKafkaStatus', async(() => {
+    component.getKafkaStatus();
+    expect(component.currentKafkaStatus).toEqual(null);
+
+    component.sensorParserConfig.sensorTopic = 'squid';
+    kafkaService.setKafkaTopic('squid', null);
+
+    component.getKafkaStatus();
+    expect(component.currentKafkaStatus).toEqual(KafkaStatus.NO_TOPIC);
+
+    kafkaService.setKafkaTopic('squid', new KafkaTopic());
+    kafkaService.setSampleData('squid', null);
+
+    component.getKafkaStatus();
+    expect(component.currentKafkaStatus).toEqual(KafkaStatus.NOT_EMITTING);
+
+    kafkaService.setSampleData('squid', 'message');
+    component.getKafkaStatus();
+    expect(component.currentKafkaStatus).toEqual(KafkaStatus.EMITTING);
+
+    fixture.destroy();
+  }));
+
+  it('should getTransforms', async(() => {
+    expect(component.getTransforms()).toEqual('0 Transformations Applied');
+
+    component.sensorParserConfig.fieldTransformations.push(Object.assign(new FieldTransformer(), {'output': ['field1', 'field2']}));
+    component.sensorParserConfig.fieldTransformations.push(Object.assign(new FieldTransformer(), {'output': ['field3']}));
+
+    expect(component.getTransforms()).toEqual('3 Transformations Applied');
+
+    fixture.destroy();
+  }));
+
+  it('should handle onSaveGrokStatement', async(() => {
+    component.sensorParserConfig.sensorTopic = 'squid';
+
+    component.onSaveGrokStatement('grok statement');
+    expect(component.grokStatement).toEqual('grok statement');
+    expect(component.sensorParserConfig.parserConfig['grokPath']).toEqual('/apps/metron/patterns/squid');
+
+    component.sensorParserConfig.parserConfig['grokPath'] = '/patterns/squid';
+    component.onSaveGrokStatement('grok statement');
+    expect(component.sensorParserConfig.parserConfig['grokPath']).toEqual('/apps/metron/patterns/squid');
+
+    component.sensorParserConfig.parserConfig['grokPath'] = '/custom/grok/path';
+    component.onSaveGrokStatement('grok statement');
+    expect(component.sensorParserConfig.parserConfig['grokPath']).toEqual('/custom/grok/path');
+
+    fixture.destroy();
+  }));
+
+  it('should onSavePatternLabel', async(() => {
+    component.onSavePatternLabel('PATTERN_LABEL');
+    expect(component.patternLabel).toEqual('PATTERN_LABEL');
+    expect(component.sensorParserConfig.parserConfig['patternLabel']).toEqual('PATTERN_LABEL');
+
+    fixture.destroy();
+  }));
+
+  it('should goBack', async(() => {
+    activatedRoute.setNameForTest('new');
+
+    router.navigateByUrl = jasmine.createSpy('navigateByUrl');
+    component.goBack();
+    expect(router.navigateByUrl).toHaveBeenCalledWith('/sensors');
+
+    fixture.destroy();
+  }));
+
+  it('should save sensor configuration', async(() => {
+    let fieldTransformer = Object.assign(new FieldTransformer(), {
+      'input': [],
+      'output': ['url_host'],
+      'transformation': 'MTL',
+      'config': {'url_host': 'TO_LOWER(URL_TO_HOST(url))'}
+    });
+    let sensorParserConfigSave: SensorParserConfig = new SensorParserConfig();
+    sensorParserConfigSave.sensorTopic = 'squid';
+    sensorParserConfigSave.parserClassName = 'org.apache.metron.parsers.GrokParser';
+    sensorParserConfigSave.parserConfig['grokPath'] = '/apps/metron/patterns/squid';
+    sensorParserConfigSave.fieldTransformations = [fieldTransformer];
+    activatedRoute.setNameForTest('new');
+    sensorParserConfigService.setThrowError(true);
+
+    spyOn(metronAlerts, 'showSuccessMessage');
+    spyOn(metronAlerts, 'showErrorMessage');
+
+    component.sensorParserConfig.sensorTopic = 'squid';
+    component.sensorParserConfig.parserClassName = 'org.apache.metron.parsers.GrokParser';
+    component.sensorParserConfig.parserConfig['grokPath'] = '/apps/metron/patterns/squid';
+    component.sensorParserConfig.fieldTransformations = [fieldTransformer];
+
+    component.onSave();
+    expect(metronAlerts.showErrorMessage['calls'].mostRecent().args[0])
+        .toEqual('Unable to save sensor config: SensorParserConfig post error');
+
+    component.sensorEnrichmentConfig = Object.assign(new SensorEnrichmentConfig(), squidSensorEnrichmentConfig);
+    component.indexingConfigurations = Object.assign(new IndexingConfigurations(), squidIndexingConfigurations);
+    sensorParserConfigService.setThrowError(false);
+    hdfsService.setContents('/apps/metron/patterns/squid', 'SQUID grok statement');
+    component.grokStatement = 'SQUID grok statement';
+
+    component.onSave();
+    expect(sensorParserConfigService.getPostedSensorParserConfig()).toEqual(sensorParserConfigSave);
+    expect(sensorEnrichmentConfigService.getPostedSensorEnrichmentConfig())
+        .toEqual(Object.assign(new SensorEnrichmentConfig(), squidSensorEnrichmentConfig));
+    expect(sensorIndexingConfigService.getPostedIndexingConfigurations())
+        .toEqual(Object.assign(new IndexingConfigurations(), squidIndexingConfigurations));
+    expect(hdfsService.getPostedContents()).toEqual('SQUID grok statement');
+
+    hdfsService.setContents('/apps/metron/patterns/squid', null);
+
+    component.onSave();
+    expect(metronAlerts.showErrorMessage['calls'].mostRecent().args[0]).toEqual('HDFS post Error');
+
+    sensorEnrichmentConfigService.setThrowError(true);
+
+    component.onSave();
+    expect(metronAlerts.showErrorMessage['calls'].mostRecent().args[0])
+        .toEqual('Created Sensor parser config but unable to save enrichment configuration: SensorEnrichmentConfig post error');
+
+    sensorIndexingConfigService.setThrowError(true);
+
+    component.onSave();
+    expect(metronAlerts.showErrorMessage['calls'].mostRecent().args[0])
+        .toEqual('Created Sensor parser config but unable to save indexing configuration: IndexingConfigurations post error');
+
+    fixture.destroy();
+  }));
+
+  it('should getTransformationCount', async(() => {
+    let transforms =
+    [
+      Object.assign(new FieldTransformer(), {
+        'input': [
+          'method'
+        ],
+        'output': null,
+        'transformation': 'REMOVE',
+        'config': {
+          'condition': 'exists(method) and method == "foo"'
+        }
+      }),
+      Object.assign(new FieldTransformer(), {
+        'input': [],
+        'output': [
+          'method',
+          'status_code',
+          'url'
+        ],
+        'transformation': 'STELLAR',
+        'config': {
+          'method': 'TO_UPPER(method)',
+          'status_code': 'TO_LOWER(code)',
+          'url': 'TO_STRING(TRIM(url))'
+        }
+      })
+    ];
+
+
+
+    expect(component.getTransformationCount()).toEqual(0);
+
+    fixture.componentInstance.sensorParserConfig.fieldTransformations = transforms;
+    expect(component.getTransformationCount()).toEqual(3);
+
+    fixture.componentInstance.sensorParserConfig.fieldTransformations = [transforms[0]];
+    expect(component.getTransformationCount()).toEqual(0);
+    fixture.destroy();
+  }));
+
+  it('should getEnrichmentCount', async(() => {
+
+    component.sensorEnrichmentConfig.enrichment.fieldMap['geo'] = ['ip_src_addr', 'ip_dst_addr'];
+    component.sensorEnrichmentConfig.enrichment.fieldToTypeMap['hbaseenrichment'] = ['ip_src_addr', 'ip_dst_addr'];
+
+    expect(component.getEnrichmentCount()).toEqual(4);
+
+    fixture.destroy();
+  }));
+
+  it('should getThreatIntelCount', async(() => {
+
+    component.sensorEnrichmentConfig.threatIntel.fieldToTypeMap['hbaseenrichment'] = ['ip_src_addr', 'ip_dst_addr'];
+
+    expect(component.getThreatIntelCount()).toEqual(2);
+
+    fixture.destroy();
+  }));
+
+  it('should getRuleCount', async(() => {
+    let rule1 = Object.assign(new RiskLevelRule(), {'name': 'rule1', 'rule': 'some rule', 'score': 50});
+    let rule2 = Object.assign(new RiskLevelRule(), {'name': 'rule2', 'rule': 'some other rule', 'score': 80});
+    component.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules.push(rule1);
+    component.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules.push(rule2);
+
+    expect(component.getRuleCount()).toEqual(2);
+
+    fixture.destroy();
+  }));
+
+  it('should showPane', async(() => {
+
+    component.showPane(Pane.GROK);
+    expect(component.showGrokValidator).toEqual(true);
+    expect(component.showFieldSchema).toEqual(false);
+    expect(component.showRawJson).toEqual(false);
+
+    component.showPane(Pane.FIELDSCHEMA);
+    expect(component.showGrokValidator).toEqual(false);
+    expect(component.showFieldSchema).toEqual(true);
+    expect(component.showRawJson).toEqual(false);
+
+    component.showPane(Pane.RAWJSON);
+    expect(component.showGrokValidator).toEqual(false);
+    expect(component.showFieldSchema).toEqual(false);
+    expect(component.showRawJson).toEqual(true);
+
+    fixture.destroy();
+  }));
+
+  it('should hidePane', async(() => {
+
+    component.hidePane(Pane.GROK);
+    expect(component.showGrokValidator).toEqual(false);
+    expect(component.showFieldSchema).toEqual(false);
+    expect(component.showRawJson).toEqual(false);
+
+    component.hidePane(Pane.FIELDSCHEMA);
+    expect(component.showGrokValidator).toEqual(false);
+    expect(component.showFieldSchema).toEqual(false);
+    expect(component.showRawJson).toEqual(false);
+
+    component.hidePane(Pane.RAWJSON);
+    expect(component.showGrokValidator).toEqual(false);
+    expect(component.showFieldSchema).toEqual(false);
+    expect(component.showRawJson).toEqual(false);
+
+    fixture.destroy();
+  }));
+
+  it('should handle onShowGrokPane', async(() => {
+    spyOn(component, 'showPane');
+    component.sensorParserConfig.sensorTopic = 'squid';
+
+    component.onShowGrokPane();
+    expect(component.patternLabel).toEqual('SQUID');
+    expect(component.showPane).toHaveBeenCalledWith(component.pane.GROK);
+
+    component.patternLabel = 'PATTERN_LABEL';
+
+    component.onShowGrokPane();
+    expect(component.patternLabel).toEqual('PATTERN_LABEL');
+
+    fixture.destroy();
+  }));
+
+  it('should handle onRawJsonChanged', async(() => {
+    spyOn(component.sensorFieldSchema, 'createFieldSchemaRows');
+
+    component.onRawJsonChanged();
+
+    expect(component.sensorFieldSchema.createFieldSchemaRows).toHaveBeenCalled();
+
+    fixture.destroy();
+  }));
+
+  it('should handle onAdvancedConfigFormClose', async(() => {
+    component.onAdvancedConfigFormClose();
+
+    expect(component.showAdvancedParserConfiguration).toEqual(false);
+
+    fixture.destroy();
+  }));
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.ts
new file mode 100644
index 0000000..a4192f1
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.ts
@@ -0,0 +1,421 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {FormGroup, Validators, FormControl} from '@angular/forms';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import {SensorParserConfigService} from '../../service/sensor-parser-config.service';
+import {Router, ActivatedRoute} from '@angular/router';
+import {MetronAlerts} from '../../shared/metron-alerts';
+import {SensorParserContext} from '../../model/sensor-parser-context';
+import {SensorEnrichmentConfigService} from '../../service/sensor-enrichment-config.service';
+import {SensorEnrichmentConfig} from '../../model/sensor-enrichment-config';
+import {SensorFieldSchemaComponent} from '../sensor-field-schema/sensor-field-schema.component';
+import {SensorRawJsonComponent} from '../sensor-raw-json/sensor-raw-json.component';
+import {KafkaService} from '../../service/kafka.service';
+import {SensorIndexingConfigService} from '../../service/sensor-indexing-config.service';
+import {IndexingConfigurations} from '../../model/sensor-indexing-config';
+import {RestError} from '../../model/rest-error';
+import {HdfsService} from '../../service/hdfs.service';
+import {GrokValidationService} from '../../service/grok-validation.service';
+
+export enum Pane {
+  GROK, RAWJSON, FIELDSCHEMA, THREATTRIAGE
+}
+
+export enum KafkaStatus {
+  NO_TOPIC, NOT_EMITTING, EMITTING
+}
+
+@Component({
+  selector: 'metron-config-sensor',
+  templateUrl: 'sensor-parser-config.component.html',
+  styleUrls: ['sensor-parser-config.component.scss']
+})
+
+export class SensorParserConfigComponent implements OnInit {
+
+  sensorConfigForm: FormGroup;
+  transformsValidationForm: FormGroup;
+
+  sensorParserConfig: SensorParserConfig = new SensorParserConfig();
+  sensorEnrichmentConfig: SensorEnrichmentConfig = new SensorEnrichmentConfig();
+  indexingConfigurations: IndexingConfigurations = new IndexingConfigurations();
+
+  showGrokValidator: boolean = false;
+  showTransformsValidator: boolean = false;
+  showAdvancedParserConfiguration: boolean = false;
+  showRawJson: boolean = false;
+  showFieldSchema: boolean = false;
+  showThreatTriage: boolean = false;
+
+  configValid = false;
+  sensorNameValid = false;
+  parserClassValid = false;
+  grokStatementValid = false;
+  availableParsers = {};
+  availableParserNames = [];
+  grokStatement = '';
+  patternLabel = '';
+
+  editMode: boolean = false;
+
+  topicExists: boolean = false;
+
+  transformsValidationResult: {map: any, keys: string[]} = {map: {}, keys: []};
+  transformsValidation: SensorParserContext = new SensorParserContext();
+
+  pane = Pane;
+  openPane: Pane = null;
+
+  kafkaStatus = KafkaStatus;
+  currentKafkaStatus = null;
+
+  @ViewChild(SensorFieldSchemaComponent) sensorFieldSchema: SensorFieldSchemaComponent;
+  @ViewChild(SensorRawJsonComponent) sensorRawJson: SensorRawJsonComponent;
+
+  constructor(private sensorParserConfigService: SensorParserConfigService, private metronAlerts: MetronAlerts,
+              private sensorEnrichmentConfigService: SensorEnrichmentConfigService, private route: ActivatedRoute,
+              private sensorIndexingConfigService: SensorIndexingConfigService, private grokValidationService: GrokValidationService,
+              private router: Router, private kafkaService: KafkaService, private hdfsService: HdfsService) {
+    this.sensorParserConfig.parserConfig = {};
+  }
+
+
+  init(id: string): void {
+    if (id !== 'new') {
+      this.editMode = true;
+      this.sensorParserConfigService.get(id).subscribe((results: SensorParserConfig) => {
+        this.sensorParserConfig = results;
+        this.sensorNameValid = true;
+        this.getKafkaStatus();
+        if (Object.keys(this.sensorParserConfig.parserConfig).length > 0) {
+          this.showAdvancedParserConfiguration = true;
+        }
+        if (this.isGrokParser(this.sensorParserConfig)) {
+          let path = this.sensorParserConfig.parserConfig['grokPath'];
+          if (path) {
+            this.hdfsService.read(path).subscribe(contents => {
+              this.grokStatement = contents;
+            }, (hdfsError: RestError) => {
+              this.grokValidationService.getStatement(path).subscribe(contents => {
+                this.grokStatement = contents;
+              }, (grokError: RestError) => {
+                this.metronAlerts.showErrorMessage('Could not find grok statement in HDFS or classpath at ' + path);
+              });
+            });
+          }
+          let patternLabel = this.sensorParserConfig.parserConfig['patternLabel'];
+          if (patternLabel) {
+            this.patternLabel = patternLabel;
+          }
+      }});
+
+      this.sensorEnrichmentConfigService.get(id).subscribe((result: SensorEnrichmentConfig) => {
+        this.sensorEnrichmentConfig = result;
+      }, (error: RestError) => {
+        if (error.responseCode !== 404) {
+          this.metronAlerts.showErrorMessage(error.message);
+        }
+      });
+
+      this.sensorIndexingConfigService.get(id).subscribe((result: IndexingConfigurations) => {
+            this.indexingConfigurations = result;
+      }, (error: RestError) => {
+        if (error.responseCode !== 404) {
+          this.metronAlerts.showErrorMessage(error.message);
+        }
+      });
+    } else {
+      this.sensorParserConfig = new SensorParserConfig();
+      this.sensorParserConfig.parserClassName = 'org.apache.metron.parsers.GrokParser';
+    }
+  }
+
+  ngOnInit() {
+    this.route.params.subscribe(params => {
+      let id = params['id'];
+      this.init(id);
+    });
+    this.createForms();
+    this.getAvailableParsers();
+  }
+
+  createSensorConfigForm(): FormGroup {
+    let group: any = {};
+
+    group['sensorTopic'] = new FormControl(this.sensorParserConfig.sensorTopic, Validators.required);
+    group['parserClassName'] = new FormControl(this.sensorParserConfig.parserClassName, Validators.required);
+    group['grokStatement'] = new FormControl(this.grokStatement);
+    group['transforms'] = new FormControl(this.sensorParserConfig['transforms']);
+    group['stellar'] = new FormControl(this.sensorParserConfig);
+    group['threatTriage'] = new FormControl(this.sensorEnrichmentConfig);
+    group['hdfsIndex'] = new FormControl(this.indexingConfigurations.hdfs.index, Validators.required);
+    group['hdfsBatchSize'] = new FormControl(this.indexingConfigurations.hdfs.batchSize, Validators.required);
+    group['hdfsEnabled'] = new FormControl(this.indexingConfigurations.hdfs.enabled, Validators.required);
+    group['elasticsearchIndex'] = new FormControl(this.indexingConfigurations.elasticsearch.index, Validators.required);
+    group['elasticsearchBatchSize'] = new FormControl(this.indexingConfigurations.elasticsearch.batchSize, Validators.required);
+    group['elasticsearchEnabled'] = new FormControl(this.indexingConfigurations.elasticsearch.enabled, Validators.required);
+    group['solrIndex'] = new FormControl(this.indexingConfigurations.solr.index, Validators.required);
+    group['solrBatchSize'] = new FormControl(this.indexingConfigurations.solr.batchSize, Validators.required);
+    group['solrEnabled'] = new FormControl(this.indexingConfigurations.solr.enabled, Validators.required);
+
+    return new FormGroup(group);
+  }
+
+  createTransformsValidationForm(): FormGroup {
+    let group: any = {};
+
+    group['sampleData'] = new FormControl(this.transformsValidation.sampleData, Validators.required);
+    group['sensorParserConfig'] = new FormControl(this.transformsValidation.sensorParserConfig, Validators.required);
+
+    return new FormGroup(group);
+  }
+
+  createForms() {
+    this.sensorConfigForm = this.createSensorConfigForm();
+    this.transformsValidationForm = this.createTransformsValidationForm();
+    if (Object.keys(this.sensorParserConfig.parserConfig).length > 0) {
+      this.showAdvancedParserConfiguration = true;
+    }
+  }
+
+  getAvailableParsers() {
+    this.sensorParserConfigService.getAvailableParsers().subscribe(
+      availableParsers => {
+        this.availableParsers = availableParsers;
+        this.availableParserNames = Object.keys(availableParsers);
+      }
+    );
+  }
+
+  getMessagePrefix(): string {
+    return this.editMode ? 'Modified' : 'Created';
+  }
+
+  onSetSensorName(): void {
+    this.sensorNameValid = this.sensorParserConfig.sensorTopic !== undefined &&
+        this.sensorParserConfig.sensorTopic.length > 0;
+    if (this.sensorNameValid) {
+      this.getKafkaStatus();
+    }
+    this.isConfigValid();
+  }
+
+  onParserTypeChange(): void {
+    this.parserClassValid = this.sensorParserConfig.parserClassName !== undefined &&
+        this.sensorParserConfig.parserClassName.length > 0;
+    if (this.parserClassValid) {
+      if (this.isGrokParser(this.sensorParserConfig)) {
+      } else {
+        this.hidePane(Pane.GROK);
+      }
+    }
+    this.isConfigValid();
+  }
+
+  onGrokStatementChange(): void {
+    this.grokStatementValid = this.grokStatement !== undefined &&
+        this.grokStatement.length > 0;
+    this.isConfigValid();
+  }
+
+  isConfigValid() {
+    let isGrokParser = this.isGrokParser(this.sensorParserConfig);
+    this.configValid = this.sensorNameValid && this.parserClassValid && (!isGrokParser || this.grokStatementValid);
+  }
+
+  getKafkaStatus() {
+    if (!this.sensorParserConfig.sensorTopic || this.sensorParserConfig.sensorTopic.length === 0) {
+      this.currentKafkaStatus = null;
+      return;
+    }
+
+    this.kafkaService.get(this.sensorParserConfig.sensorTopic).subscribe(kafkaTopic => {
+      this.kafkaService.sample(this.sensorParserConfig.sensorTopic).subscribe((sampleData: string) => {
+        this.currentKafkaStatus = (sampleData && sampleData.length > 0) ? KafkaStatus.EMITTING : KafkaStatus.NOT_EMITTING;
+      },
+      error => {
+        this.currentKafkaStatus = KafkaStatus.NOT_EMITTING;
+      });
+    },
+    error => {
+      this.currentKafkaStatus = KafkaStatus.NO_TOPIC;
+    });
+
+  }
+
+  getTransforms(): string {
+    let count = 0;
+    if (this.sensorParserConfig.fieldTransformations) {
+      for (let tranforms of this.sensorParserConfig.fieldTransformations) {
+        if (tranforms.output) {
+          count += tranforms.output.length;
+        }
+      }
+    }
+
+    return count + ' Transformations Applied';
+  }
+
+  goBack() {
+    this.router.navigateByUrl('/sensors');
+    return false;
+  }
+
+  onSaveGrokStatement(grokStatement: string) {
+    this.grokStatement = grokStatement;
+    let grokPath = this.sensorParserConfig.parserConfig['grokPath'];
+    if (!grokPath || grokPath.indexOf('/patterns') === 0) {
+      this.sensorParserConfig.parserConfig['grokPath'] = '/apps/metron/patterns/' + this.sensorParserConfig.sensorTopic;
+    }
+  }
+
+  onSavePatternLabel(patternLabel: string) {
+    this.patternLabel = patternLabel;
+    this.sensorParserConfig.parserConfig['patternLabel'] = patternLabel;
+  }
+
+  onSave() {
+    let sensorParserConfigSave: SensorParserConfig = new SensorParserConfig();
+    sensorParserConfigSave.parserConfig = {};
+    sensorParserConfigSave.sensorTopic = this.sensorParserConfig.sensorTopic;
+    sensorParserConfigSave.parserClassName = this.sensorParserConfig.parserClassName;
+    sensorParserConfigSave.parserConfig = this.sensorParserConfig.parserConfig;
+    sensorParserConfigSave.fieldTransformations = this.sensorParserConfig.fieldTransformations;
+
+    if (!this.indexingConfigurations.hdfs.index) {
+      this.indexingConfigurations.hdfs.index = this.sensorParserConfig.sensorTopic;
+    }
+    if (!this.indexingConfigurations.elasticsearch.index) {
+      this.indexingConfigurations.elasticsearch.index = this.sensorParserConfig.sensorTopic;
+    }
+    if (!this.indexingConfigurations.solr.index) {
+      this.indexingConfigurations.solr.index = this.sensorParserConfig.sensorTopic;
+    }
+    this.sensorParserConfigService.post(sensorParserConfigSave).subscribe(
+      sensorParserConfig => {
+        if (this.isGrokParser(sensorParserConfig)) {
+            this.hdfsService.post(this.sensorParserConfig.parserConfig['grokPath'], this.grokStatement).subscribe(
+                response => {}, (error: RestError) => this.metronAlerts.showErrorMessage(error.message));
+        }
+        this.sensorEnrichmentConfigService.post(sensorParserConfig.sensorTopic, this.sensorEnrichmentConfig).subscribe(
+            (sensorEnrichmentConfig: SensorEnrichmentConfig) => {
+        }, (error: RestError) => {
+              let msg = ' Sensor parser config but unable to save enrichment configuration: ';
+              this.metronAlerts.showErrorMessage(this.getMessagePrefix() + msg + error.message);
+        });
+        this.sensorIndexingConfigService.post(sensorParserConfig.sensorTopic, this.indexingConfigurations).subscribe(
+            (indexingConfigurations: IndexingConfigurations) => {
+        }, (error: RestError) => {
+              let msg = ' Sensor parser config but unable to save indexing configuration: ';
+              this.metronAlerts.showErrorMessage(this.getMessagePrefix() + msg + error.message);
+        });
+        this.metronAlerts.showSuccessMessage(this.getMessagePrefix() + ' Sensor ' + sensorParserConfig.sensorTopic);
+        this.sensorParserConfigService.dataChangedSource.next([sensorParserConfigSave]);
+        this.goBack();
+      }, (error: RestError) => {
+        this.metronAlerts.showErrorMessage('Unable to save sensor config: ' + error.message);
+      });
+  }
+
+  isGrokParser(sensorParserConfig: SensorParserConfig): boolean {
+    if (sensorParserConfig && sensorParserConfig.parserClassName) {
+      return sensorParserConfig.parserClassName === 'org.apache.metron.parsers.GrokParser';
+    }
+    return false;
+  }
+
+  getTransformationCount(): number {
+    let stellarTransformations = this.sensorParserConfig.fieldTransformations.filter(fieldTransformer =>
+      fieldTransformer.transformation === 'STELLAR');
+    if (stellarTransformations.length > 0 && stellarTransformations[0].config) {
+      return Object.keys(stellarTransformations[0].config).length;
+    } else {
+      return 0;
+    }
+  }
+
+  getEnrichmentCount(): number {
+    let count = 0;
+    if (this.sensorEnrichmentConfig.enrichment.fieldMap) {
+      for (let enrichment in this.sensorEnrichmentConfig.enrichment.fieldMap) {
+        if (enrichment !== 'hbaseEnrichment' && enrichment !== 'stellar') {
+          count += this.sensorEnrichmentConfig.enrichment.fieldMap[enrichment].length;
+        }
+      }
+    }
+    if (this.sensorEnrichmentConfig.enrichment.fieldToTypeMap) {
+      for (let fieldName in this.sensorEnrichmentConfig.enrichment.fieldToTypeMap) {
+        if (this.sensorEnrichmentConfig.enrichment.fieldToTypeMap.hasOwnProperty(fieldName)) {
+          count += this.sensorEnrichmentConfig.enrichment.fieldToTypeMap[fieldName].length;
+        }
+      }
+    }
+    return count;
+  }
+
+  getThreatIntelCount(): number {
+    let count = 0;
+    if (this.sensorEnrichmentConfig.threatIntel.fieldToTypeMap) {
+      for (let fieldName in this.sensorEnrichmentConfig.threatIntel.fieldToTypeMap) {
+        if (this.sensorEnrichmentConfig.threatIntel.fieldToTypeMap.hasOwnProperty(fieldName)) {
+          count += this.sensorEnrichmentConfig.threatIntel.fieldToTypeMap[fieldName].length;
+        }
+      }
+    }
+    return count;
+  }
+
+  getRuleCount(): number {
+    let count = 0;
+    if (this.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules) {
+      count = Object.keys(this.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules).length;
+    }
+    return count;
+  }
+
+  onShowGrokPane() {
+    if (!this.patternLabel) {
+      this.patternLabel = this.sensorParserConfig.sensorTopic.toUpperCase();
+    }
+    this.showPane(this.pane.GROK);
+  }
+
+  showPane(pane: Pane) {
+    this.setPaneVisibility(pane, true);
+  }
+
+  hidePane(pane: Pane) {
+    this.setPaneVisibility(pane, false);
+  }
+
+  setPaneVisibility(pane: Pane, visibilty: boolean) {
+    this.showGrokValidator = (pane === Pane.GROK) ? visibilty : false;
+    this.showFieldSchema = (pane === Pane.FIELDSCHEMA) ? visibilty : false;
+    this.showRawJson = (pane ===  Pane.RAWJSON) ? visibilty : false;
+    this.showThreatTriage = (pane ===  Pane.THREATTRIAGE) ? visibilty : false;
+  }
+
+  onRawJsonChanged(): void {
+    this.sensorFieldSchema.createFieldSchemaRows();
+  }
+
+  onAdvancedConfigFormClose(): void {
+    this.showAdvancedParserConfiguration = false;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.module.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.module.ts
new file mode 100644
index 0000000..68e22f8
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.module.ts
@@ -0,0 +1,35 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {NgModule} from '@angular/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {routing} from './sensor-parser-config.routing';
+import {SensorParserConfigComponent} from './sensor-parser-config.component';
+import {SharedModule} from '../../shared/shared.module';
+import {NumberSpinnerModule} from '../../shared/number-spinner/number-spinner.module';
+import {AdvancedConfigFormModule} from '../../shared/advanced-config-form/advanced-config-form.module';
+import {SensorGrokModule} from '../sensor-grok/sensor-grok.module';
+import {SensorFieldSchemaModule} from '../sensor-field-schema/sensor-field-schema.module';
+import {SensorRawJsonModule} from '../sensor-raw-json/sensor-raw-json.module';
+import {SensorThreatTriageModule} from '../sensor-threat-triage/sensor-threat-triage.module';
+
+@NgModule ({
+  imports: [ routing, ReactiveFormsModule, SharedModule, NumberSpinnerModule, AdvancedConfigFormModule,
+                SensorGrokModule, SensorFieldSchemaModule, SensorRawJsonModule, SensorThreatTriageModule ],
+  declarations: [ SensorParserConfigComponent ]
+})
+export class SensorParserConfigModule { }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.routing.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.routing.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.routing.ts
new file mode 100644
index 0000000..e54b7b5
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.routing.ts
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { ModuleWithProviders }  from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+import {SensorParserConfigComponent} from './sensor-parser-config.component';
+import {AuthGuard} from '../../shared/auth-guard';
+
+const routes: Routes = [
+  { path: 'sensors-config/:id', component: SensorParserConfigComponent, canActivate: [AuthGuard], outlet: 'dialog'}
+];
+
+export const routing: ModuleWithProviders = RouterModule.forChild(routes);

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-list/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-list/index.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/index.ts
new file mode 100644
index 0000000..7a80f38
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/index.ts
@@ -0,0 +1,18 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export * from './sensor-parser-list.component';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.html b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.html
new file mode 100644
index 0000000..bd0b1fd
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.html
@@ -0,0 +1,85 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<div class="sensors details-pane-padding">
+
+  <div class="container-fluid">
+    <div class="row">
+      <div class="col-lg-10 px-0">
+        <div class="metron-title"> {{componentName}} ({{count}}) </div>
+      </div>
+      <div class="col-lg-2">
+        <div class="dropdown pull-right">
+          <button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+            ACTIONS
+          </button>
+          <div class="dropdown-menu dropdown-menu-right metron-bg-inverse" aria-labelledby="dropdownMenu1">
+            <span class="dropdown-item" data-action="Delete" (click)="onDeleteSensor()" [class.disabled]="selectedSensors.length == 0">Delete</span>
+            <span class="dropdown-item" data-action="Enable" (click)="onEnableSensors()" [class.disabled]="selectedSensors.length == 0">Enable</span>
+            <span class="dropdown-item" data-action="Disable" (click)="onDisableSensors()" [class.disabled]="selectedSensors.length == 0">Disable</span>
+            <span class="dropdown-item" data-action="Start" (click)="onStartSensors()" [class.disabled]="selectedSensors.length == 0">Start</span>
+            <span class="dropdown-item" data-action="Stop" (click)="onStopSensors()" [class.disabled]="selectedSensors.length == 0">Stop</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <table class="table card-deck" metron-config-table #table (onSort)="onSort($event)">
+    <thead>
+    <tr>
+      <th> <metron-config-sorter [sortBy]="'sensorTopic'"> Name </metron-config-sorter> </th>
+      <th> <metron-config-sorter [sortBy]="'parserClassName'"> Parser </metron-config-sorter> </th>
+      <th> <metron-config-sorter [sortBy]="'status'"> Status </metron-config-sorter> </th>
+      <th> <metron-config-sorter [sortBy]="'latency'"> Latency </metron-config-sorter> </th>
+      <th> <metron-config-sorter [sortBy]="'throughput'"> Throughput </metron-config-sorter> </th>
+      <th> <metron-config-sorter [sortBy]="'modifiedByDate'"> Last Updated </metron-config-sorter> </th>
+      <th> <metron-config-sorter [sortBy]="'modifiedBy'"> Last Editor </metron-config-sorter> </th>
+      <th style="width:100px"></th>
+      <th style="width:50px"><input id="select-deselect-all" class="fontawesome-checkbox" type="checkbox" (click)="onSelectDeselectAll($event)"><label for="select-deselect-all"></label></th>
+    </tr>
+    </thead>
+    <tbody>
+    <tr *ngFor="let sensor of sensors;" (click)="onSensorRowSelect(sensor.config, $event)" [ngClass]="{'active': (selectedSensors.indexOf(sensor) != -1 || sensorParserConfigService.getSelectedSensor() == sensor.config)}">
+      <td>{{ sensor.config.sensorTopic }}</td>
+      <td>{{ getParserType(sensor.config) }}</td>
+      <td [ngClass]="{'warning-text': (sensor.status == 'Stopped' || sensor.status == 'Disabled')}">{{ sensor.status }}</td>
+      <td>{{ sensor.latency }}</td>
+      <td>{{ sensor.throughput }}</td>
+      <td>{{ sensor.modifiedByDate }}</td>
+      <td>{{ sensor.modifiedBy }}</td>
+      <td class="icon-container">
+          <i  data-toggle="tooltip" title="Operation in progress" class="fa fa-circle-o-notch fa-spin fa-lg fa-fw" [hidden]="!sensor.config['startStopInProgress']"></i>
+
+          <i  data-toggle="tooltip" title="Stop parser topology" class="fa fa-stop fa-lg" aria-hidden="true" [hidden]="((sensor.status != 'Running' && sensor.status != 'Disabled') || sensor.config['startStopInProgress'])" (click)="onStopSensor(sensor, $event)"></i>
+          <i  data-toggle="tooltip" title="Disable parser topology" class="fa fa-ban fa-lg" aria-hidden="true" [hidden]="(sensor.status != 'Running'  || sensor.config['startStopInProgress'])" (click)="onDisableSensor(sensor, $event)"></i>
+
+          <i  data-toggle="tooltip" title="Start parser topology" class="fa fa-play fa-lg" aria-hidden="true" [hidden]="(sensor.status != 'Stopped' || sensor.config['startStopInProgress'])" (click)="onStartSensor(sensor, $event)"></i>
+          <i  data-toggle="tooltip" title="Enable parser topology" class="fa fa-check-circle-o fa-lg" aria-hidden="true" [hidden]="(sensor.status != 'Disabled' || sensor.config['startStopInProgress'])" (click)="onEnableSensor(sensor, $event)"></i>
+
+          <i  data-toggle="tooltip" title="Edit parser topology" class="fa fa-pencil fa-lg" aria-hidden="true" (click)="navigateToSensorEdit(sensor.config, $event)"></i>
+
+          <i  data-toggle="tooltip" title="Delete parser configuration" class="fa fa-trash-o fa-lg" aria-hidden="true" (click)="deleteSensor($event, [sensor.config])"></i>
+      </td>
+      <td><input id="{{ sensor.config.sensorTopic }}" class="fontawesome-checkbox" type="checkbox" name="{{sensor.config.sensorTopic}}" (click)="onRowSelected(sensor, $event)"><label attr.for="{{ sensor.config.sensorTopic }}"></label></td>
+    </tr>
+    </tbody>
+  </table>
+
+    <div class="metron-add-button hexa-button" (click)="addAddSensor()">
+      <i class="fa fa-plus"></i>
+    </div>
+
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.scss b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.scss
new file mode 100644
index 0000000..5d5f1e3
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.scss
@@ -0,0 +1,17 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */


[08/12] incubator-metron git commit: METRON-623 Management UI [contributed by Raghu Mitra Kandikonda and Ryan Merriman] closes apache/incubator-metron#489

Posted by rm...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.spec.ts
new file mode 100644
index 0000000..dbbec12
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.spec.ts
@@ -0,0 +1,708 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Observable}     from 'rxjs/Observable';
+import {Router, ActivatedRoute, Params} from '@angular/router';
+import {Inject} from '@angular/core';
+import {SensorParserConfigHistory} from '../../model/sensor-parser-config-history';
+import {RequestOptions, Response, ResponseOptions, Http} from '@angular/http';
+import {SensorParserConfigReadonlyComponent} from './sensor-parser-config-readonly.component';
+import {SensorParserConfigService} from '../../service/sensor-parser-config.service';
+import {KafkaService} from '../../service/kafka.service';
+import {TopologyStatus} from '../../model/topology-status';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import {KafkaTopic} from '../../model/kafka-topic';
+import {AuthenticationService} from '../../service/authentication.service';
+import {SensorParserConfigHistoryService} from '../../service/sensor-parser-config-history.service';
+import {StormService} from '../../service/storm.service';
+import {MetronAlerts} from '../../shared/metron-alerts';
+import {FieldTransformer} from '../../model/field-transformer';
+import {SensorParserConfigReadonlyModule} from './sensor-parser-config-readonly.module';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../../app.config';
+import {IAppConfig} from '../../app.config.interface';
+import {SensorEnrichmentConfigService} from '../../service/sensor-enrichment-config.service';
+import {SensorEnrichmentConfig, EnrichmentConfig, ThreatIntelConfig} from '../../model/sensor-enrichment-config';
+import {HdfsService} from '../../service/hdfs.service';
+import {GrokValidationService} from '../../service/grok-validation.service';
+
+class MockRouter {
+
+  navigateByUrl(url: string) {
+
+  }
+
+}
+
+class MockActivatedRoute {
+  private name: string;
+  params: Observable<Params>;
+
+  setNameForTest(name: string) {
+    this.name = name;
+    this.params = Observable.create(observer => {
+      observer.next({id: this.name});
+      observer.complete();
+    });
+  }
+}
+
+class MockAuthenticationService extends AuthenticationService {
+
+  constructor(private http2: Http, private router2: Router, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, router2, config2);
+  }
+
+  public getCurrentUser(options: RequestOptions): Observable<Response> {
+    let responseOptions: ResponseOptions = new ResponseOptions();
+    responseOptions.body = 'user';
+    let response: Response = new Response(responseOptions);
+    return Observable.create(observer => {
+      observer.next(response);
+      observer.complete();
+    });
+  };
+}
+
+class MockSensorParserConfigHistoryService extends SensorParserConfigHistoryService {
+
+  private sensorParserConfigHistory: SensorParserConfigHistory;
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public setForTest(sensorParserConfigHistory: SensorParserConfigHistory) {
+    this.sensorParserConfigHistory = sensorParserConfigHistory;
+  }
+
+  public get(name: string): Observable<SensorParserConfigHistory> {
+    return Observable.create(observer => {
+      observer.next(this.sensorParserConfigHistory);
+      observer.complete();
+    });
+  }
+}
+
+class MockSensorParserConfigService extends SensorParserConfigService {
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+}
+
+class MockStormService extends StormService {
+  private topologyStatus: TopologyStatus;
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public setForTest(topologyStatus: TopologyStatus) {
+    this.topologyStatus = topologyStatus;
+  }
+
+  public getStatus(name: string): Observable<TopologyStatus> {
+    return Observable.create(observer => {
+      observer.next(this.topologyStatus);
+      observer.complete();
+    });
+  }
+}
+
+class MockGrokValidationService extends GrokValidationService {
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public list(): Observable<string[]> {
+    return Observable.create(observer => {
+      observer.next({
+        'BASE10NUM': '(?<![0-9.+-])(?>[+-]?(?:(?:[0-9]+(?:\\.[0-9]+)?)|(?:\\.[0-9]+)))',
+        'BASE16FLOAT': '\\b(?<![0-9A-Fa-f.])(?:[+-]?(?:0x)?(?:(?:[0-9A-Fa-f]+(?:\\.[0-9A-Fa-f]*)?)|(?:\\.[0-9A-Fa-f]+)))\\b',
+        'BASE16NUM': '(?<![0-9A-Fa-f])(?:[+-]?(?:0x)?(?:[0-9A-Fa-f]+))',
+        'CISCOMAC': '(?:(?:[A-Fa-f0-9]{4}\\.){2}[A-Fa-f0-9]{4})',
+        'COMMONMAC': '(?:(?:[A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2})',
+        'DATA': '.*?'
+      });
+      observer.complete();
+    });
+  }
+}
+
+class MockKafkaService extends KafkaService {
+
+  private kafkaTopic: KafkaTopic;
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public setForTest(kafkaTopic: KafkaTopic) {
+    this.kafkaTopic = kafkaTopic;
+  }
+
+  public get(name: string): Observable<KafkaTopic> {
+    return Observable.create(observer => {
+      observer.next(this.kafkaTopic);
+      observer.complete();
+    });
+  }
+
+  public sample(name: string): Observable<string> {
+    return Observable.create(observer => {
+      observer.next(JSON.stringify({'data': 'data1', 'data2': 'data3'}));
+      observer.complete();
+    });
+  }
+}
+
+class MockHdfsService extends HdfsService {
+  private fileList: string[];
+  private contents: string;
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public setContents(contents: string) {
+    this.contents = contents;
+  }
+
+  public list(path: string): Observable<string[]> {
+    if (this.fileList === null) {
+      return Observable.throw('Error');
+    }
+    return Observable.create(observer => {
+      observer.next(this.fileList);
+      observer.complete();
+    });
+  }
+
+  public read(path: string): Observable<string> {
+    if (this.contents === null) {
+      return Observable.throw('Error');
+    }
+    return Observable.create(observer => {
+      observer.next(this.contents);
+      observer.complete();
+    });
+  }
+
+  public post(path: string, contents: string): Observable<Response> {
+    return Observable.create(observer => {
+      observer.next({});
+      observer.complete();
+    });
+  }
+
+  public deleteFile(path: string): Observable<Response> {
+    return Observable.create(observer => {
+      observer.next({});
+      observer.complete();
+    });
+  }
+}
+
+class MockSensorEnrichmentConfigService {
+  private sensorEnrichmentConfig: SensorEnrichmentConfig;
+
+  setForTest(sensorEnrichmentConfig: SensorEnrichmentConfig) {
+    this.sensorEnrichmentConfig = sensorEnrichmentConfig;
+  }
+
+  public get(name: string): Observable<SensorEnrichmentConfig> {
+    return Observable.create(observer => {
+      observer.next(this.sensorEnrichmentConfig);
+      observer.complete();
+    });
+  }
+
+  public getAvailable(): Observable<string[]> {
+    return Observable.create((observer) => {
+      observer.next(['geo', 'host', 'whois']);
+      observer.complete();
+    });
+  }
+}
+
+describe('Component: SensorParserConfigReadonly', () => {
+
+  let component: SensorParserConfigReadonlyComponent;
+  let fixture: ComponentFixture<SensorParserConfigReadonlyComponent>;
+  let sensorParserConfigHistoryService: MockSensorParserConfigHistoryService;
+  let sensorEnrichmentConfigService: MockSensorEnrichmentConfigService;
+  let sensorParserConfigService: SensorParserConfigService;
+  let kafkaService: MockKafkaService;
+  let hdfsService: MockHdfsService;
+  let grokValidationService: MockGrokValidationService;
+  let stormService: MockStormService;
+  let alerts: MetronAlerts;
+  let authenticationService: AuthenticationService;
+  let router: MockRouter;
+  let activatedRoute: MockActivatedRoute;
+
+  beforeEach(async(() => {
+
+    TestBed.configureTestingModule({
+      imports: [SensorParserConfigReadonlyModule],
+      providers: [
+        {provide: Http},
+        {provide: ActivatedRoute, useClass: MockActivatedRoute},
+        {provide: AuthenticationService, useClass: MockAuthenticationService},
+        {provide: SensorEnrichmentConfigService, useClass: MockSensorEnrichmentConfigService},
+        {provide: SensorParserConfigHistoryService, useClass: MockSensorParserConfigHistoryService},
+        {provide: SensorParserConfigService, useClass: MockSensorParserConfigService},
+        {provide: StormService, useClass: MockStormService},
+        {provide: KafkaService, useClass: MockKafkaService},
+        {provide: HdfsService, useClass: MockHdfsService},
+        {provide: GrokValidationService, useClass: MockGrokValidationService},
+        {provide: Router, useClass: MockRouter},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG},
+        MetronAlerts
+      ]
+    }).compileComponents()
+      .then(() => {
+        fixture = TestBed.createComponent(SensorParserConfigReadonlyComponent);
+        component = fixture.componentInstance;
+        activatedRoute = fixture.debugElement.injector.get(ActivatedRoute);
+        hdfsService = fixture.debugElement.injector.get(HdfsService);
+        authenticationService = fixture.debugElement.injector.get(AuthenticationService);
+        sensorParserConfigHistoryService = fixture.debugElement.injector.get(SensorParserConfigHistoryService);
+        sensorEnrichmentConfigService = fixture.debugElement.injector.get(SensorEnrichmentConfigService);
+        sensorParserConfigService = fixture.debugElement.injector.get(SensorParserConfigService);
+        stormService = fixture.debugElement.injector.get(StormService);
+        kafkaService = fixture.debugElement.injector.get(KafkaService);
+        grokValidationService = fixture.debugElement.injector.get(GrokValidationService);
+        router = fixture.debugElement.injector.get(Router);
+        alerts = fixture.debugElement.injector.get(MetronAlerts);
+      });
+
+  }));
+
+  it('should create an instance', async(() => {
+    expect(component).toBeDefined();
+  }));
+
+  it('should have metadata defined ', async(() => {
+    expect(component.editViewMetaData.length).toEqual(24);
+  }));
+
+  it('should have sensorsService with parserName and grokPattern defined and kafkaService defined', async(() => {
+    let sensorParserInfo = new SensorParserConfigHistory();
+    let sensorParserConfig = new SensorParserConfig();
+    let kafkaTopic = new KafkaTopic();
+    let topologyStatus = new TopologyStatus();
+
+    sensorParserConfig.sensorTopic = 'bro';
+    sensorParserConfig.parserClassName = 'org.apache.metron.parsers.GrokParser';
+    sensorParserConfig.parserConfig = {grokPattern: 'SQUID_DELIMITED squid grok statement'};
+    sensorParserInfo.config = sensorParserConfig;
+
+    kafkaTopic.name = 'bro';
+    kafkaTopic.numPartitions = 1;
+    kafkaTopic.replicationFactor = 1;
+
+    topologyStatus.name = 'bro';
+    topologyStatus.latency = 10.1;
+    topologyStatus.throughput = 15.2;
+
+    let broEnrichment = {
+      'fieldMap': {
+        'geo': ['ip_dst_addr'],
+        'host': ['ip_dst_addr'],
+        'whois': [],
+        'stellar': {'config': {'group1': {}}}
+      },
+      'fieldToTypeMap': {}, 'config': {}
+    };
+    let broThreatIntel = {'threatIntel': {
+      'fieldMap': { 'hbaseThreatIntel': ['ip_dst_addr'] },
+      'fieldToTypeMap': { 'ip_dst_addr': ['malicious_ip'] }
+    }
+    };
+    let broEnrichments = new SensorEnrichmentConfig();
+    broEnrichments.enrichment = Object.assign(new EnrichmentConfig(),  broEnrichment);
+    broEnrichments.threatIntel = Object.assign(new ThreatIntelConfig(), broThreatIntel);
+
+    sensorEnrichmentConfigService.setForTest(broEnrichments);
+    sensorParserConfigHistoryService.setForTest(sensorParserInfo);
+    kafkaService.setForTest(kafkaTopic);
+    stormService.setForTest(topologyStatus);
+
+    activatedRoute.setNameForTest('bro');
+
+    component.ngOnInit();
+    expect(component.startStopInProgress).toEqual(false);
+    expect(component.sensorParserConfigHistory).toEqual(Object.assign(new SensorParserConfigHistory(), sensorParserInfo));
+    expect(component.kafkaTopic).toEqual(kafkaTopic);
+    expect(component.sensorEnrichmentConfig).toEqual(broEnrichments);
+  }));
+
+  it('getSensorStatusService should initialise the state variable to appropriate values ', async(() => {
+    let sensorParserStatus = new TopologyStatus();
+    sensorParserStatus.name = 'bro';
+    sensorParserStatus.latency = 10.1;
+    sensorParserStatus.status = null;
+    sensorParserStatus.throughput = 15.2;
+
+    stormService.setForTest(sensorParserStatus);
+
+    component.getSensorStatusService();
+    expect(component.getTopologyStatus('status')).toEqual('Stopped');
+    expect(component.getTopologyStatus('sensorStatus')).toEqual('-');
+
+    sensorParserStatus.status = 'ACTIVE';
+    component.getSensorStatusService();
+    stormService.setForTest(sensorParserStatus);
+    expect(component.getTopologyStatus('status')).toEqual('Running');
+    expect(component.getTopologyStatus('sensorStatus')).toEqual('Enabled');
+
+    sensorParserStatus.status = 'KILLED';
+    component.getSensorStatusService();
+    stormService.setForTest(sensorParserStatus);
+    expect(component.getTopologyStatus('status')).toEqual('Stopped');
+    expect(component.getTopologyStatus('sensorStatus')).toEqual('-');
+
+    sensorParserStatus.status = 'INACTIVE';
+    component.getSensorStatusService();
+    stormService.setForTest(sensorParserStatus);
+    expect(component.getTopologyStatus('status')).toEqual('Disabled');
+    expect(component.getTopologyStatus('sensorStatus')).toEqual('Disabled');
+  }));
+
+  it('setGrokStatement should set the variables appropriately ', async(() => {
+    let grokStatement = 'SQUID_DELIMITED squid grok statement';
+    hdfsService.setContents(grokStatement);
+    let sensorParserInfo = new SensorParserConfigHistory();
+    let sensorParserConfig = new SensorParserConfig();
+    sensorParserConfig.parserConfig = {};
+
+    sensorParserConfig.parserConfig['grokPath'] = '/squid/grok/path';
+    sensorParserInfo.config = sensorParserConfig;
+
+    component.sensorParserConfigHistory = sensorParserInfo;
+    component.setGrokStatement();
+
+    expect(component.grokStatement).toEqual(grokStatement);
+  }));
+
+  it('setTransformsConfigKeys/getTransformsOutput should return the keys of the transforms config  ', async(() => {
+    let sensorParserInfo = new SensorParserConfigHistory();
+    let sensorParserConfig = new SensorParserConfig();
+    let fieldTransformer1 = new FieldTransformer();
+    let fieldTransformer2 = new FieldTransformer();
+
+    fieldTransformer1.config = {'a': 'abc', 'x': 'xyz'};
+    fieldTransformer1.output = ['a', 'b', 'c'];
+    fieldTransformer2.config = {'x': 'klm', 'b': 'def'};
+    fieldTransformer2.output = ['a', 'b', 'c'];
+    sensorParserConfig.fieldTransformations = [fieldTransformer1, fieldTransformer2];
+    sensorParserInfo.config = sensorParserConfig;
+
+    component.setTransformsConfigKeys();
+    let transformsOutput = component.getTransformsOutput();
+
+    expect(component.transformsConfigKeys.length).toEqual(0);
+    expect(component.transformsConfigKeys).toEqual([]);
+    expect(component.transformsConfigMap).toEqual({});
+    expect(transformsOutput).toEqual('-');
+
+    component.sensorParserConfigHistory = sensorParserInfo;
+    component.setTransformsConfigKeys();
+    transformsOutput = component.getTransformsOutput();
+
+    expect(component.transformsConfigKeys.length).toEqual(3);
+    expect(component.transformsConfigKeys).toEqual(['a', 'b', 'x']);
+    expect(component.transformsConfigMap).toEqual({'a': ['abc'], 'b': ['def'], 'x': ['xyz', 'klm']});
+    expect(transformsOutput).toEqual('a, b, c');
+  }));
+
+  it('goBack should navigate to sensors page', async(() => {
+    router.navigateByUrl = jasmine.createSpy('navigateByUrl');
+
+    component.goBack();
+
+    expect(router.navigateByUrl).toHaveBeenCalledWith('/sensors');
+  }));
+
+  it('onEditSensor should navigate to sensor edit', async(() => {
+    router.navigateByUrl = jasmine.createSpy('navigateByUrl');
+
+    component.selectedSensorName = 'abc';
+
+    component.onEditSensor();
+    expect(router.navigateByUrl).toHaveBeenCalledWith('/sensors(dialog:sensors-config/abc)');
+  }));
+
+  it('should set sensorEnrichmentConfig and aggregationConfigKeys to be initialised', async(() => {
+    let threatIntel = {
+      'fieldMap': {
+        'hbaseThreatIntel': [ 'ip_dst_addr', 'ip_src_addr', 'action']
+      },
+      'fieldToTypeMap': {
+        'ip_dst_addr': [ 'malicious_ip'], 'ip_src_addr': [ 'malicious_ip'], 'action': [ 'malicious_ip']
+      },
+      'config': {},
+      'triageConfig': {
+        'riskLevelRules': [
+          {
+            'rule': 'IN_SUBNET(ip_dst_addr, \'192.168.0.0/24\')',
+            'score': 3
+          },
+          {
+            'rule': 'user.type in [ \'admin\', \'power\' ] and asset.type == \'web\'',
+            'score': 3
+          },
+        ],
+        'aggregator': 'MAX',
+        'aggregationConfig': {}
+      }
+    };
+    let expected = [{'rule': 'IN_SUBNET(ip_dst_addr, \'192.168.0.0/24\')', 'score': 3},
+      {'rule': 'user.type in [ \'admin\', \'power\' ] and asset.type == \'web\'', 'score': 3}];
+
+    let sensorEnrichmentConfig = new SensorEnrichmentConfig();
+    sensorEnrichmentConfig.threatIntel = Object.assign(new ThreatIntelConfig(), threatIntel);
+    sensorEnrichmentConfigService.setForTest(sensorEnrichmentConfig);
+
+    component.getEnrichmentData();
+
+
+    expect(component.sensorEnrichmentConfig).toEqual(sensorEnrichmentConfig);
+    expect(component.rules).toEqual(expected);
+  }));
+
+  let setDataForSensorOperation = function () {
+    let sensorParserInfo = new SensorParserConfigHistory();
+    let sensorParserConfig = new SensorParserConfig();
+    let kafkaTopic = new KafkaTopic();
+    let topologyStatus = new TopologyStatus();
+
+    sensorParserConfig.sensorTopic = 'bro';
+    sensorParserConfig.parserClassName = 'org.apache.metron.parsers.GrokParser';
+    sensorParserConfig.parserConfig = {grokPattern: 'SQUID_DELIMITED squid grok statement'};
+    sensorParserInfo.config = sensorParserConfig;
+
+    kafkaTopic.name = 'bro';
+    kafkaTopic.numPartitions = 1;
+    kafkaTopic.replicationFactor = 1;
+
+    topologyStatus.name = 'bro';
+    topologyStatus.latency = 10.1;
+    topologyStatus.throughput = 15.2;
+
+    let broEnrichment = {
+      'fieldMap': {
+        'geo': ['ip_dst_addr'],
+        'host': ['ip_dst_addr'],
+        'whois': [],
+        'stellar': {'config': {'group1': {}}}
+      },
+      'fieldToTypeMap': {}, 'config': {}
+    };
+    let broThreatIntel = {'threatIntel': {
+      'fieldMap': { 'hbaseThreatIntel': ['ip_dst_addr'] },
+      'fieldToTypeMap': { 'ip_dst_addr': ['malicious_ip'] }
+    }
+    };
+    let broEnrichments = new SensorEnrichmentConfig();
+    broEnrichments.enrichment = Object.assign(new EnrichmentConfig(),  broEnrichment);
+    broEnrichments.threatIntel = Object.assign(new ThreatIntelConfig(), broThreatIntel);
+
+    kafkaService.setForTest(kafkaTopic);
+    stormService.setForTest(topologyStatus);
+    sensorEnrichmentConfigService.setForTest(broEnrichments);
+    sensorParserConfigHistoryService.setForTest(sensorParserInfo);
+  };
+
+  it('onStartSensor should  start sensor', async(() => {
+    spyOn(stormService, 'startParser').and.returnValue(Observable.create(observer => {
+      observer.next({});
+      observer.complete();
+    }));
+
+    alerts.showSuccessMessage = jasmine.createSpy('showSuccessMessage');
+    setDataForSensorOperation();
+
+    component.selectedSensorName = 'abc';
+
+    component.onStartSensor();
+
+    expect(stormService.startParser).toHaveBeenCalledWith('abc');
+    expect(alerts.showSuccessMessage).toHaveBeenCalledWith('Started sensor abc');
+  }));
+
+  it('onStopSensor should stop the sensor', async(() => {
+    spyOn(stormService, 'stopParser').and.returnValue(Observable.create(observer => {
+      observer.next({});
+      observer.complete();
+    }));
+
+    alerts.showSuccessMessage = jasmine.createSpy('showSuccessMessage');
+    setDataForSensorOperation();
+
+    component.selectedSensorName = 'abc';
+
+    component.onStopSensor();
+
+    expect(stormService.stopParser).toHaveBeenCalledWith('abc');
+    expect(alerts.showSuccessMessage).toHaveBeenCalledWith('Stopped sensor abc');
+  }));
+
+  it('onEnableSensor should enable sensor', async(() => {
+    spyOn(stormService, 'activateParser').and.returnValue(Observable.create(observer => {
+      observer.next({});
+      observer.complete();
+    }));
+
+    alerts.showSuccessMessage = jasmine.createSpy('showSuccessMessage');
+    setDataForSensorOperation();
+
+    component.selectedSensorName = 'abc';
+
+    component.onEnableSensor();
+
+    expect(stormService.activateParser).toHaveBeenCalledWith('abc');
+    expect(alerts.showSuccessMessage).toHaveBeenCalledWith('Enabled sensor abc');
+  }));
+
+  it('onDisableSensor should disable the sensor', async(() => {
+    spyOn(stormService, 'deactivateParser').and.returnValue(Observable.create(observer => {
+      observer.next({});
+      observer.complete();
+    }));
+
+    alerts.showSuccessMessage = jasmine.createSpy('showSuccessMessage');
+    setDataForSensorOperation();
+
+    component.selectedSensorName = 'abc';
+
+    component.onDisableSensor();
+
+    expect(stormService.deactivateParser).toHaveBeenCalledWith('abc');
+    expect(alerts.showSuccessMessage).toHaveBeenCalledWith('Disabled sensor abc');
+  }));
+
+  it('onDeleteSensor should delete the sensor', async(() => {
+    spyOn(sensorParserConfigService, 'deleteSensorParserConfig').and.returnValue(Observable.create(observer => {
+      observer.next({});
+      observer.complete();
+    }));
+
+    alerts.showSuccessMessage = jasmine.createSpy('showSuccessMessage');
+    router.navigateByUrl = jasmine.createSpy('navigateByUrl');
+    setDataForSensorOperation();
+
+    component.selectedSensorName = 'abc';
+
+    component.onDeleteSensor();
+
+    expect(sensorParserConfigService.deleteSensorParserConfig).toHaveBeenCalledWith('abc');
+    expect(alerts.showSuccessMessage).toHaveBeenCalledWith('Deleted sensor abc');
+    expect(router.navigateByUrl).toHaveBeenCalledWith('/sensors');
+  }));
+
+  it('toggleStartStopInProgress should toggle the variable for showing progressbar', async(() => {
+    expect(component.startStopInProgress).toEqual(false);
+
+    component.startStopInProgress = true;
+    expect(component.startStopInProgress).toEqual(true);
+
+    component.startStopInProgress = false;
+    expect(component.startStopInProgress).toEqual(false);
+  }));
+
+  it('should toggleTransformLink', async(() => {
+    expect(component.transformLinkText).toEqual('show more');
+
+    component.toggleTransformLink();
+    expect(component.transformLinkText).toEqual('show less');
+
+    component.toggleTransformLink();
+    expect(component.transformLinkText).toEqual('show more');
+  }));
+
+  it('should toggleThreatTriageLink', async(() => {
+    expect(component.threatTriageLinkText).toEqual('show more');
+
+    component.toggleThreatTriageLink();
+    expect(component.threatTriageLinkText).toEqual('show less');
+
+    component.toggleThreatTriageLink();
+    expect(component.threatTriageLinkText).toEqual('show more');
+  }));
+
+  it('should hide start', async(() => {
+    component.topologyStatus.status = 'ACTIVE';
+    expect(component.isStartHidden()).toEqual(true);
+
+    component.topologyStatus.status = 'INACTIVE';
+    expect(component.isStartHidden()).toEqual(true);
+
+    component.topologyStatus.status = 'Stopped';
+    expect(component.isStartHidden()).toEqual(false);
+
+    component.topologyStatus.status = 'KILLED';
+    expect(component.isStartHidden()).toEqual(false);
+  }));
+
+  it('should hide stop', async(() => {
+    component.topologyStatus.status = 'ACTIVE';
+    expect(component.isStopHidden()).toEqual(false);
+
+    component.topologyStatus.status = 'INACTIVE';
+    expect(component.isStopHidden()).toEqual(false);
+
+    component.topologyStatus.status = 'Stopped';
+    expect(component.isStopHidden()).toEqual(true);
+
+    component.topologyStatus.status = 'KILLED';
+    expect(component.isStopHidden()).toEqual(true);
+  }));
+
+  it('should hide enable', async(() => {
+    component.topologyStatus.status = 'ACTIVE';
+    expect(component.isEnableHidden()).toEqual(true);
+
+    component.topologyStatus.status = 'INACTIVE';
+    expect(component.isEnableHidden()).toEqual(false);
+
+    component.topologyStatus.status = 'Stopped';
+    expect(component.isEnableHidden()).toEqual(true);
+
+    component.topologyStatus.status = 'KILLED';
+    expect(component.isEnableHidden()).toEqual(true);
+  }));
+
+  it('should hide disable', async(() => {
+    component.topologyStatus.status = 'ACTIVE';
+    expect(component.isDisableHidden()).toEqual(false);
+
+    component.topologyStatus.status = 'INACTIVE';
+    expect(component.isDisableHidden()).toEqual(true);
+
+    component.topologyStatus.status = 'Stopped';
+    expect(component.isDisableHidden()).toEqual(true);
+
+    component.topologyStatus.status = 'KILLED';
+    expect(component.isDisableHidden()).toEqual(true);
+  }));
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.ts
new file mode 100644
index 0000000..7edc2c5
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.ts
@@ -0,0 +1,372 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {KafkaService} from '../../service/kafka.service';
+import {Router, ActivatedRoute} from '@angular/router';
+import {KafkaTopic} from '../../model/kafka-topic';
+import {MetronAlerts} from '../../shared/metron-alerts';
+import {SensorParserConfigService} from '../../service/sensor-parser-config.service';
+import {StormService} from '../../service/storm.service';
+import {TopologyStatus} from '../../model/topology-status';
+import {SensorParserConfigHistoryService} from '../../service/sensor-parser-config-history.service';
+import {SensorParserConfigHistory} from '../../model/sensor-parser-config-history';
+import {SensorEnrichmentConfigService} from '../../service/sensor-enrichment-config.service';
+import {SensorEnrichmentConfig} from '../../model/sensor-enrichment-config';
+import {RiskLevelRule} from '../../model/risk-level-rule';
+import {HdfsService} from '../../service/hdfs.service';
+import {RestError} from '../../model/rest-error';
+import {GrokValidationService} from '../../service/grok-validation.service';
+
+@Component({
+  selector: 'metron-config-sensor-parser-readonly',
+  templateUrl: 'sensor-parser-config-readonly.component.html',
+  styleUrls: ['sensor-parser-config-readonly.component.scss']
+})
+export class SensorParserConfigReadonlyComponent implements OnInit {
+
+  selectedSensorName: string;
+  startStopInProgress: boolean = false;
+  kafkaTopic: KafkaTopic = new KafkaTopic();
+  sensorParserConfigHistory: SensorParserConfigHistory = new SensorParserConfigHistory();
+  topologyStatus: TopologyStatus = new TopologyStatus();
+  sensorEnrichmentConfig: SensorEnrichmentConfig = new SensorEnrichmentConfig();
+  grokStatement: string = '';
+  transformsConfigKeys: string[] = [];
+  transformsConfigMap: {} = {};
+  rules: RiskLevelRule[] = [];
+  transformLinkText = 'show more';
+  threatTriageLinkText = 'show more';
+
+  editViewMetaData: {label?: string, value?: string, type?: string, model?: string, boldTitle?: boolean}[] = [
+    {type: 'SEPARATOR', model: '', value: ''},
+    {label: 'PARSER', model: 'sensorParserConfigHistory', value: 'parserClassName'},
+    {label: 'LAST UPDATED', model: 'sensorParserConfigHistory', value: 'modifiedByDate'},
+    {label: 'LAST EDITOR', model: 'sensorParserConfigHistory', value: 'modifiedBy'},
+    {label: 'STATE', model: 'topologyStatus', value: 'sensorStatus'},
+    {label: 'ORIGINATOR', model: 'sensorParserConfigHistory', value: 'createdBy'},
+    {label: 'CREATION DATE', model: 'sensorParserConfigHistory', value: 'createdDate'},
+
+    {type: 'SPACER', model: '', value: ''},
+
+    {label: 'STORM', model: 'topologyStatus', value: 'status', boldTitle: true},
+    {label: 'LATENCY', model: 'topologyStatus', value: 'latency'},
+    {label: 'THROUGHPUT', model: 'topologyStatus', value: 'throughput'},
+    {label: 'EMITTED(10 MIN)', model: 'topologyStatus', value: 'emitted'},
+    {label: 'ACKED(10 MIN)', model: 'topologyStatus', value: 'acked'},
+
+    {type: 'SPACER', model: '', value: ''},
+
+    {label: 'KAFKA', model: 'kafkaTopic', value: 'currentKafkaStatus', boldTitle: true},
+    {label: 'PARTITONS', model: 'kafkaTopic', value: 'numPartitions'},
+    {label: 'REPLICATION FACTOR', model: 'kafkaTopic', value: 'replicationFactor'},
+    {type: 'SEPARATOR', model: '', value: ''},
+
+    {label: '', model: 'grokStatement', value: 'grokPattern'},
+
+    {type: 'TITLE', model: '', value: 'Schema'},
+    {label: '', model: 'transforms', value: ''},
+    {type: 'SEPARATOR', model: '', value: ''},
+
+    {type: 'TITLE', model: '', value: 'Threat Triage Rules'},
+    {label: '', model: 'threatTriageRules', value: ''}
+
+  ];
+
+  constructor(private sensorParserConfigHistoryService: SensorParserConfigHistoryService,
+              private sensorParserConfigService: SensorParserConfigService,
+              private sensorEnrichmentService: SensorEnrichmentConfigService,
+              private stormService: StormService,
+              private kafkaService: KafkaService,
+              private hdfsService: HdfsService,
+              private grokValidationService: GrokValidationService,
+              private activatedRoute: ActivatedRoute, private router: Router,
+              private metronAlerts: MetronAlerts) {
+  }
+
+  getSensorInfo(): void {
+    this.sensorParserConfigHistoryService.get(this.selectedSensorName).subscribe(
+      (results: SensorParserConfigHistory) => {
+        this.sensorParserConfigHistory = results;
+        this.setGrokStatement();
+        this.setTransformsConfigKeys();
+
+        let items = this.sensorParserConfigHistory.config.parserClassName.split('.');
+        this.sensorParserConfigHistory['parserClassName'] = items[items.length - 1].replace('Basic', '').replace('Parser', '');
+
+      });
+  }
+
+  getSensorStatusService() {
+    this.stormService.getStatus(this.selectedSensorName).subscribe(
+      (results: TopologyStatus) => {
+        this.topologyStatus = results;
+      },
+      error => {
+        this.topologyStatus.status = 'Stopped';
+      });
+  }
+
+  getKafkaData(): void {
+    this.kafkaService.get(this.selectedSensorName).subscribe(
+      (results: KafkaTopic) => {
+        this.kafkaTopic = results;
+        this.kafkaService.sample(this.selectedSensorName).subscribe((sampleData: string) => {
+              this.kafkaTopic['currentKafkaStatus'] = (sampleData && sampleData.length > 0) ? 'Emitting' : 'Not Emitting';
+            },
+            error => {
+              this.kafkaTopic['currentKafkaStatus'] = 'Not Emitting';
+            });
+      }, error => {
+          this.kafkaTopic['currentKafkaStatus'] = 'No Kafka Topic';
+      });
+  }
+
+  getEnrichmentData() {
+    this.sensorEnrichmentService.get(this.selectedSensorName).subscribe((sensorEnrichmentConfig) => {
+      this.sensorEnrichmentConfig = sensorEnrichmentConfig;
+      this.rules = sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules;
+    });
+  }
+
+  getTopologyStatus(key: string): string {
+    if (key === 'latency') {
+      return this.topologyStatus.latency >= 0? (this.topologyStatus.latency + 's') : '-';
+    } else if (key === 'throughput') {
+      return this.topologyStatus.throughput >= 0 ? ((Math.round(this.topologyStatus.throughput * 100) / 100) + 'kb/s') : '-';
+    } else if (key === 'emitted') {
+      return this.topologyStatus.emitted >= 0 ? (this.topologyStatus.emitted + '') : '-';
+    } else if (key === 'acked') {
+      return this.topologyStatus.acked >= 0 ? (this.topologyStatus.acked + '') : '-';
+    } else if (key === 'sensorStatus') {
+      if (this.topologyStatus.status === 'ACTIVE') {
+        return 'Enabled';
+      } else if (this.topologyStatus.status === 'INACTIVE') {
+        return 'Disabled';
+      } else {
+        return '-';
+      }
+    } else if (key === 'status') {
+      if (this.topologyStatus.status === 'ACTIVE') {
+       return 'Running';
+      } else if (this.topologyStatus.status === 'INACTIVE') {
+        return 'Disabled';
+      } else {
+        return 'Stopped';
+      }
+    }
+
+    return this.topologyStatus[key] ? this.topologyStatus[key] : '-';
+  }
+
+  ngOnInit() {
+    this.activatedRoute.params.subscribe(params => {
+      this.selectedSensorName = params['id'];
+      this.getData();
+    });
+  }
+
+  getData() {
+    this.startStopInProgress = false;
+
+    this.getSensorInfo();
+    this.getSensorStatusService();
+    this.getKafkaData();
+    this.getEnrichmentData();
+  }
+
+  setGrokStatement() {
+    if (this.sensorParserConfigHistory.config && this.sensorParserConfigHistory.config.parserConfig) {
+      let path = this.sensorParserConfigHistory.config.parserConfig['grokPath'];
+      if (path) {
+        this.hdfsService.read(path).subscribe(contents => {
+          this.grokStatement = contents;
+        }, (hdfsError: RestError) => {
+          this.grokValidationService.getStatement(path).subscribe(contents => {
+            this.grokStatement = contents;
+          }, (grokError: RestError) => {
+            this.metronAlerts.showErrorMessage('Could not find grok statement in HDFS or classpath at ' + path);
+          });
+        });
+      }
+    }
+  }
+
+  setTransformsConfigKeys() {
+    if (this.sensorParserConfigHistory.config && this.sensorParserConfigHistory.config.fieldTransformations &&
+        this.sensorParserConfigHistory.config.fieldTransformations.length > 0) {
+      this.transformsConfigKeys = [];
+      for (let transforms of this.sensorParserConfigHistory.config.fieldTransformations) {
+        if (transforms.config) {
+          for (let key of Object.keys(transforms.config)) {
+            if (this.transformsConfigKeys.indexOf(key) === -1) {
+              this.transformsConfigMap[key] = [];
+              this.transformsConfigKeys.push(key);
+            }
+            this.transformsConfigMap[key].push(transforms.config[key]);
+          }
+        }
+      }
+      this.transformsConfigKeys = this.transformsConfigKeys.sort();
+    }
+  }
+
+  getTransformsOutput(): string {
+    if (this.sensorParserConfigHistory.config && this.sensorParserConfigHistory.config.fieldTransformations &&
+        this.sensorParserConfigHistory.config.fieldTransformations.length > 0) {
+      let output = [];
+      for (let transforms of this.sensorParserConfigHistory.config.fieldTransformations) {
+        if (transforms.output) {
+          output = output.concat(transforms.output);
+        }
+      }
+      output = output.sort().filter(function(item, pos, self) {
+        return self.indexOf(item) === pos;
+      });
+
+      return output.join(', ');
+    }
+
+    return '-';
+  }
+
+  goBack() {
+    this.router.navigateByUrl('/sensors');
+  }
+
+  onEditSensor() {
+    this.router.navigateByUrl('/sensors(dialog:sensors-config/' + this.selectedSensorName + ')');
+  }
+
+  onStartSensor() {
+    this.toggleStartStopInProgress();
+    let name = this.selectedSensorName;
+
+    this.stormService.startParser(name).subscribe(result => {
+        this.metronAlerts.showSuccessMessage('Started sensor ' + name);
+        this.toggleStartStopInProgress();
+        this.getData();
+      },
+      error => {
+        this.metronAlerts.showErrorMessage('Unable to start sensor ' + name);
+        this.toggleStartStopInProgress();
+      });
+  }
+
+  onStopSensor() {
+    this.toggleStartStopInProgress();
+
+    let name = this.selectedSensorName;
+    this.stormService.stopParser(name).subscribe(result => {
+        this.metronAlerts.showSuccessMessage('Stopped sensor ' + name);
+        this.toggleStartStopInProgress();
+        this.getData();
+      },
+      error => {
+        this.metronAlerts.showErrorMessage('Unable to stop sensor ' + name);
+        this.toggleStartStopInProgress();
+      });
+  }
+
+  onEnableSensor() {
+    this.toggleStartStopInProgress();
+
+    let name = this.selectedSensorName;
+    this.stormService.activateParser(name).subscribe(result => {
+        this.metronAlerts.showSuccessMessage('Enabled sensor ' + name);
+        this.toggleStartStopInProgress();
+        this.getData();
+      },
+      error => {
+        this.metronAlerts.showErrorMessage('Unable to enabled sensor ' + name);
+        this.toggleStartStopInProgress();
+      });
+  }
+
+  onDisableSensor() {
+    this.toggleStartStopInProgress();
+
+    let name = this.selectedSensorName;
+    this.stormService.deactivateParser(name).subscribe(result => {
+        this.metronAlerts.showSuccessMessage('Disabled sensor ' + name);
+        this.toggleStartStopInProgress();
+        this.getData();
+      },
+      error => {
+        this.metronAlerts.showErrorMessage('Unable to disable sensor ' + name);
+        this.toggleStartStopInProgress();
+      });
+  }
+
+  onDeleteSensor() {
+    this.toggleStartStopInProgress();
+
+    let name = this.selectedSensorName;
+    this.sensorParserConfigService.deleteSensorParserConfig(name).subscribe(result => {
+        this.metronAlerts.showSuccessMessage('Deleted sensor ' + name);
+        this.toggleStartStopInProgress();
+        this.sensorParserConfigService.dataChangedSource.next([this.sensorParserConfigHistory.config]);
+        this.goBack();
+      },
+      error => {
+        this.metronAlerts.showErrorMessage('Unable to delete sensor ' + name);
+        this.toggleStartStopInProgress();
+      });
+  }
+
+  toggleStartStopInProgress() {
+    this.startStopInProgress = !this.startStopInProgress;
+  }
+
+  getRuleDisplayName(): string {
+    return this.rules.map(x => this.getDisplayName(x)).join(', ');
+  }
+
+  getDisplayName(riskLevelRule: RiskLevelRule): string {
+    if (riskLevelRule.name) {
+      return riskLevelRule.name;
+    } else {
+      return riskLevelRule.rule ? riskLevelRule.rule : '';
+    }
+  }
+
+  toggleTransformLink() {
+    return this.transformLinkText = (this.transformLinkText === 'show more') ? 'show less' : 'show more';
+  }
+
+  toggleThreatTriageLink() {
+    return this.threatTriageLinkText = (this.threatTriageLinkText === 'show more') ? 'show less' : 'show more';
+  }
+
+  isStartHidden() {
+    return (this.topologyStatus.status === 'ACTIVE' || this.topologyStatus.status === 'INACTIVE');
+  }
+
+  isStopHidden() {
+    return ((this.topologyStatus.status === 'KILLED' || this.topologyStatus.status === 'Stopped'));
+  }
+
+  isEnableHidden() {
+    return (this.topologyStatus.status === 'ACTIVE' || this.topologyStatus.status === 'KILLED'
+            || this.topologyStatus.status === 'Stopped');
+  }
+
+  isDisableHidden() {
+    return (this.topologyStatus.status === 'INACTIVE' || this.topologyStatus.status === 'KILLED'
+            || this.topologyStatus.status === 'Stopped');
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.module.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.module.ts
new file mode 100644
index 0000000..c20d377
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.module.ts
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import {routing} from './sensor-parser-config-readonly.routing';
+import {SharedModule} from '../../shared/shared.module';
+import {SensorParserConfigReadonlyComponent} from './sensor-parser-config-readonly.component';
+
+@NgModule ({
+  imports: [ routing, SharedModule ],
+  declarations: [ SensorParserConfigReadonlyComponent ]
+})
+export class SensorParserConfigReadonlyModule { }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.routing.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.routing.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.routing.ts
new file mode 100644
index 0000000..71e1bad
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.routing.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { ModuleWithProviders }  from '@angular/core';
+import { RouterModule } from '@angular/router';
+import {SensorParserConfigReadonlyComponent} from './sensor-parser-config-readonly.component';
+import {AuthGuard} from '../../shared/auth-guard';
+
+export const routing: ModuleWithProviders = RouterModule.forChild([
+  { path: 'sensors-readonly/:id', component: SensorParserConfigReadonlyComponent, canActivate: [AuthGuard], outlet: 'dialog'}
+]);

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config/index.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/index.ts
new file mode 100644
index 0000000..f108057
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/index.ts
@@ -0,0 +1 @@
+export * from './sensor-parser-config.component';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.html b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.html
new file mode 100644
index 0000000..33e8fd5
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.html
@@ -0,0 +1,181 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+  http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<metron-config-metron-modal>
+    <metron-config-sensor-grok [hidden]="!showGrokValidator" [showGrok]="showGrokValidator"
+                               [grokStatement]="grokStatement"
+                               [(patternLabel)]="patternLabel"
+                               [(sensorParserConfig)]="sensorParserConfig"
+                               (hideGrok)="hidePane(pane.GROK)" (onSaveGrokStatement)="onSaveGrokStatement($event)" (onSavePatternLabel)="onSavePatternLabel($event)"></metron-config-sensor-grok>
+
+    <metron-config-sensor-raw-json [hidden]="!showRawJson" [showRawJson]="showRawJson" (hideRawJson)="hidePane(pane.RAWJSON)"
+                                   [(sensorParserConfig)]="sensorParserConfig"
+                                   [(sensorEnrichmentConfig)]="sensorEnrichmentConfig"
+                                   [(indexingConfigurations)]="indexingConfigurations"
+                                   (onRawJsonChanged)="onRawJsonChanged()"></metron-config-sensor-raw-json>
+
+    <metron-config-sensor-field-schema [hidden]="!showFieldSchema" [showFieldSchema]="showFieldSchema"
+                                       [grokStatement]="grokStatement"
+                                       [(sensorParserConfig)]="sensorParserConfig"
+                                       [(sensorEnrichmentConfig)]="sensorEnrichmentConfig"
+                                       (hideFieldSchema)="hidePane(pane.FIELDSCHEMA)"></metron-config-sensor-field-schema>
+
+    <metron-config-sensor-threat-triage [hidden]="!showThreatTriage" [showThreatTriage]="showThreatTriage"
+                                        [(sensorEnrichmentConfig)]="sensorEnrichmentConfig"
+                                        (hideThreatTriage)="hidePane(pane.THREATTRIAGE)"></metron-config-sensor-threat-triage>
+
+
+    <div class="metron-slider-pane-edit fill load-right-to-left dialog1x" style="overflow: auto" >
+        <div style="height:100%">
+            <div class="form-title">{{sensorParserConfig.sensorTopic}} </div>
+            <i class="fa fa-times pull-right main close-button" aria-hidden="true" (click)="goBack()"></i>
+
+            <form role="form" [formGroup]="sensorConfigForm">
+                <div class="form-group">
+                    <label attr.for="sensorTopic">NAME * </label>
+                    <input type="text" class="form-control" name="sensorTopic" formControlName="sensorTopic"  [(ngModel)]="sensorParserConfig.sensorTopic" (ngModelChange)="onSetSensorName()" [readonly]="editMode">
+                    <label *ngIf="currentKafkaStatus !== null && currentKafkaStatus === kafkaStatus.NO_TOPIC"><span class="warning-text"> No Matching Kafka Topic </span></label>
+                    <label *ngIf="currentKafkaStatus !== null && currentKafkaStatus === kafkaStatus.EMITTING" ><span class="success-text-color"> Kafka Topic Exists. Emitting </span></label>
+                    <label *ngIf="currentKafkaStatus !== null && currentKafkaStatus === kafkaStatus.NOT_EMITTING"><span class="success-text-color"> Kafka Topic Exists. </span><span  class="warning-text" > Not Emitting </span></label>
+                </div>
+
+                <div class="form-group">
+                    <label attr.for="parserClassName">PARSER TYPE * </label>
+                    <select  class="form-control" formControlName="parserClassName" [(ngModel)]="sensorParserConfig.parserClassName" (ngModelChange)="onParserTypeChange()" >
+                        <option *ngFor="let parserName of availableParserNames" [value]="availableParsers[parserName]">{{parserName}}</option>
+                    </select>
+                </div>
+
+                <div class="form-group" [ngClass]="{'panel-selected': showGrokValidator }" *ngIf="isGrokParser(sensorParserConfig)" >
+                    <label attr.for="grokStatement">GROK STATEMENT</label>
+                    <div  class="input-group" [attr.disabled]="!sensorNameValid || !parserClassValid">
+                        <input type="text" class="form-control" formControlName="grokStatement"  [(ngModel)]="this.grokStatement" (ngModelChange)="onGrokStatementChange()" readonly>
+                        <span class="input-group-btn">
+                            <button class="btn btn-default" type="button" (click)="sensorNameValid && parserClassValid && onShowGrokPane()" readonly>
+                            <i class="fa fa-columns" aria-hidden="true"></i>
+                            <i class="fa fa-angle-double-right" style="padding-left: 3px" aria-hidden="true"></i>
+                            </button>
+                        </span>
+                    </div>
+                </div>
+
+                <div class="form-group" [ngClass]="{'panel-selected': showFieldSchema }" >
+                    <label attr.for="fieldSchema">SCHEMA</label>
+                    <div  class="input-group" [attr.disabled]="!configValid">
+                        <div  class="form-control" style="height: 80px; resize: none;" readonly>
+                            <table cellspacing="10">
+                                <tr> <td class="p-l-1">TRANSFORMATIONS </td> <td class="p-l-1">&nbsp;&nbsp;</td><td class="p-1-1">{{getTransformationCount()}}</td> </tr>
+                                <tr> <td>ENRICHMENTS</td> <td class="p-l-1">&nbsp;&nbsp;</td> <td class="p-1-1">{{getEnrichmentCount()}}</td></tr>
+                                <tr> <td>THREAT INTEL</td> <td class="p-l-1">&nbsp;&nbsp;</td> <td class="p-1-1">{{getThreatIntelCount()}}</td></tr>
+                            </table>
+                        </div>
+                        <span class="input-group-btn">
+                            <button class="btn btn-default" type="button" (click)="configValid && showPane(pane.FIELDSCHEMA)" style="height: 80px;" readonly>
+                                <i class="fa fa-columns" aria-hidden="true"></i>
+                                <i class="fa fa-angle-double-right" style="padding-left: 3px" aria-hidden="true"></i>
+                          </button>
+                        </span>
+                    </div>
+                </div>
+
+                <div class="form-group" [ngClass]="{'panel-selected': showThreatTriage }">
+                    <label attr.for="stellar">THREAT TRIAGE</label>
+                    <div class="input-group" [attr.disabled]="!configValid">
+                        <div  class="form-control" style="resize: none;" readonly>
+                            <table style="margin: 0">
+                                <tr> <td class="p-l-1">RULES </td> <td class="p-l-1">&nbsp;&nbsp;</td><td class="p-1-1">{{getRuleCount()}}</td> </tr>
+                            </table>
+                        </div>
+                        <span class="input-group-btn">
+                            <button class="btn btn-default" type="button" (click)="configValid && showPane(pane.THREATTRIAGE)" readonly>
+                                <i class="fa fa-columns" aria-hidden="true"></i>
+                                <i class="fa fa-angle-double-right" style="padding-left: 3px" aria-hidden="true"></i>
+                            </button>
+                        </span>
+                    </div>
+                </div>
+
+                <div [hidden]="!showAdvancedParserConfiguration">
+                    <div class="form-group">
+                        <div class="form-seperator-edit"></div>
+                        <div class="advanced-title">Advanced</div>
+                        <i class="fa fa-times pull-right small-close-button" aria-hidden="true" (click)="onAdvancedConfigFormClose()"></i>
+                    </div>
+                    <div class="form-group" [ngClass]="{'panel-selected': showRawJson }">
+                        <label attr.for="stellar">RAW JSON</label>
+                        <div class="input-group">
+                            <div  class="form-control" style="resize: none;" readonly>Select</div>
+                            <span class="input-group-btn">
+                                <button class="btn btn-default" type="button" (click)="showPane(pane.RAWJSON)" readonly>
+                                    <i class="fa fa-columns" aria-hidden="true"></i>
+                                    <i class="fa fa-angle-double-right" style="padding-left: 3px" aria-hidden="true"></i>
+                                </button>
+                            </span>
+                        </div>
+                    </div>
+
+                    <div class="form-group">
+                        <label attr.for="index">HDFS INDEX NAME</label>
+                        <input type="text" class="form-control" name="hdfsIndex" formControlName="hdfsIndex"  [(ngModel)]="indexingConfigurations.hdfs.index" >
+                    </div>
+                    <div  class="form-group">
+                        <label attr.for="batchSize">HDFS BATCH SIZE</label>
+                        <metron-config-number-spinner name="hdfsBatchSize" [(ngModel)]="indexingConfigurations.hdfs.batchSize" formControlName="hdfsBatchSize"> </metron-config-number-spinner>
+                    </div>
+                    <div class="form-group">
+                        <label attr.for="index">HDFS ENABLED</label>
+                        <input type="checkbox" class="form-control" name="hdfsEnabled" formControlName="hdfsEnabled"  [(ngModel)]="indexingConfigurations.hdfs.enabled" >
+                    </div>
+                    <div class="form-group">
+                        <label attr.for="index">ELASTICSEARCH INDEX NAME</label>
+                        <input type="text" class="form-control" name="elasticsearchIndex" formControlName="elasticsearchIndex"  [(ngModel)]="indexingConfigurations.elasticsearch.index" >
+                    </div>
+                    <div  class="form-group">
+                        <label attr.for="batchSize">ELASTICSEARCH BATCH SIZE</label>
+                        <metron-config-number-spinner name="elasticsearchBatchSize" [(ngModel)]="indexingConfigurations.elasticsearch.batchSize" formControlName="elasticsearchBatchSize"> </metron-config-number-spinner>
+                    </div>
+                    <div class="form-group">
+                        <label attr.for="index">ELASTICSEARCH ENABLED</label>
+                        <input type="checkbox" class="form-control" name="elasticsearchEnabled" formControlName="elasticsearchEnabled"  [(ngModel)]="indexingConfigurations.elasticsearch.enabled" >
+                    </div>
+                    <div class="form-group">
+                        <label attr.for="index">SOLR INDEX NAME</label>
+                        <input type="text" class="form-control" name="solrIndex" formControlName="solrIndex"  [(ngModel)]="indexingConfigurations.solr.index" >
+                    </div>
+                    <div  class="form-group">
+                        <label attr.for="batchSize">SOLR BATCH SIZE</label>
+                        <metron-config-number-spinner name="solrBatchSize" [(ngModel)]="indexingConfigurations.solr.batchSize" formControlName="solrBatchSize"> </metron-config-number-spinner>
+                    </div>
+                    <div class="form-group">
+                        <label attr.for="index">SOLR ENABLED</label>
+                        <input type="checkbox" class="form-control" name="solrEnabled" formControlName="solrEnabled"  [(ngModel)]="indexingConfigurations.solr.enabled" >
+                    </div>
+                    <div class="form-group">
+                        <label attr.for="parserConfig">PARSER CONFIG</label>
+                        <metron-config-advanced-form name="parserConfig" [(config)]="sensorParserConfig.parserConfig"></metron-config-advanced-form>
+                    </div>
+
+                </div>
+
+                <div class="form-group">
+                    <div class="form-seperator-edit"></div>
+                    <div class="button-row">
+                        <button type="submit" class="btn save-button" [ngClass]="{'disabled':!configValid}"  (click)="onSave()">SAVE</button>
+                        <button class="btn form-enable-disable-button" (click)="goBack()" >CANCEL</button>
+                        <span class="advanced-link" [hidden]="showAdvancedParserConfiguration" (click)="showAdvancedParserConfiguration = true">Advanced</span>
+                    </div>
+                </div>
+            </form>
+        </div>
+    </div>
+</metron-config-metron-modal>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.scss b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.scss
new file mode 100644
index 0000000..298163a
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.scss
@@ -0,0 +1,120 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../../_variables.scss";
+@import "../../../styles.scss";
+
+.form-title
+{
+  padding-left: 25px;
+}
+
+.form-group
+{
+  padding-left: 25px;
+  padding-right: 20px;
+  padding-bottom: 10px;
+}
+
+.close-button
+{
+  padding-right: 20px;
+}
+
+.advanced-link {
+  padding-left: 10px;
+  cursor: pointer;
+  color: $field-button-color;
+  font-size: 14px;
+
+}
+
+.advanced-title {
+  font-size: 16px;
+  color: $form-field-text-color;
+  display: inline-block;
+}
+
+.small-close-button {
+  font-size: 16px;
+  padding-right: 10px;
+  cursor: pointer;
+}
+
+.input-placeholder {
+  font-size: 11px;
+  font-style: italic;
+  color:#999999;
+}
+
+.metron-slider-pane-edit {
+  background: $edit-background;
+}
+
+.input-group[disabled='true'] {
+  cursor: not-allowed;
+  opacity: .65;
+  background-color: #333333;
+
+  .btn {
+    cursor: inherit;
+  }
+
+  table tr td {
+    border: none !important;
+  }
+}
+
+.button-container {
+  padding-top: 5px;
+}
+
+.button-row {
+  padding-top: 10px;
+}
+
+.form-enable-disable-button {
+  width: 32%;
+}
+
+.save-button {
+  background-color: $form-button-border;
+  border-color: $form-button-border;
+  color: white;
+  font-size: 14px;
+  width: 32%;
+
+  &:hover
+  {
+
+  }
+
+  &:focus
+  {
+    outline: none;
+  }
+}
+
+.panel-selected {
+  background-color: $edit-background-border;
+}
+
+metron-config-sensor-grok, metron-config-sensor-raw-json, metron-config-sensor-field-schema,
+metron-config-sensor-threat-triage, metron-config-sensor-threat-triage
+{
+  @extend .flexbox-row-reverse;
+}


[09/12] incubator-metron git commit: METRON-623 Management UI [contributed by Raghu Mitra Kandikonda and Ryan Merriman] closes apache/incubator-metron#489

Posted by rm...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.spec.ts
new file mode 100644
index 0000000..d2066ea
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.spec.ts
@@ -0,0 +1,523 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* tslint:disable:no-unused-variable */
+/* tslint:disable:max-line-length */
+
+import { TestBed, async, ComponentFixture } from '@angular/core/testing';
+import {Http} from '@angular/http';
+import {SimpleChanges, SimpleChange} from '@angular/core';
+import {SensorParserConfigService} from '../../service/sensor-parser-config.service';
+import {StellarService} from '../../service/stellar.service';
+import {MetronAlerts} from '../../shared/metron-alerts';
+import {SensorFieldSchemaModule} from './sensor-field-schema.module';
+import {SensorFieldSchemaComponent, FieldSchemaRow} from './sensor-field-schema.component';
+import {KafkaService} from '../../service/kafka.service';
+import {Observable} from 'rxjs/Observable';
+import {StellarFunctionDescription} from '../../model/stellar-function-description';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import {SensorEnrichmentConfig, EnrichmentConfig, ThreatIntelConfig} from '../../model/sensor-enrichment-config';
+import {ParseMessageRequest} from '../../model/parse-message-request';
+import {AutocompleteOption} from '../../model/autocomplete-option';
+import {FieldTransformer} from '../../model/field-transformer';
+import {SensorEnrichmentConfigService} from '../../service/sensor-enrichment-config.service';
+
+
+class MockSensorParserConfigService {
+
+    parseMessage(parseMessageRequest: ParseMessageRequest): Observable<{}> {
+        let parsedJson = {
+            'elapsed': 415,
+            'code': 200,
+            'ip_dst_addr': '207.109.73.154',
+            'original_string': '1467011157.401 415 127.0.0.1 TCP_MISS/200 337891 GET http://www.aliexpress.com/',
+            'method': 'GET',
+            'bytes': 337891,
+            'action': 'TCP_MISS',
+            'ip_src_addr': '127.0.0.1',
+            'url': 'http://www.aliexpress.com/af/shoes.html?',
+            'timestamp': '1467011157.401'
+        };
+        return Observable.create((observable) => {
+            observable.next(parsedJson);
+            observable.complete();
+        });
+    }
+}
+
+class MockTransformationValidationService {
+    public listSimpleFunctions(): Observable<StellarFunctionDescription[]> {
+        let stellarFunctionDescription: StellarFunctionDescription[] = [];
+        stellarFunctionDescription.push(new StellarFunctionDescription('TO_LOWER', 'TO_LOWER description', ['input - input field']));
+        stellarFunctionDescription.push(new StellarFunctionDescription('TO_UPPER', 'TO_UPPER description', ['input - input field']));
+        stellarFunctionDescription.push(new StellarFunctionDescription('TRIM', 'Lazy to copy desc', ['input - input field']));
+        return Observable.create((observer) => {
+            observer.next(stellarFunctionDescription);
+            observer.complete();
+        });
+    }
+}
+
+class MockSensorEnrichmentConfigService {
+    public getAvailableEnrichments(): Observable<string[]> {
+        return Observable.create((observer) => {
+            observer.next(['geo', 'host', 'whois']);
+            observer.complete();
+        });
+    }
+}
+
+class MockKafkaService {
+
+}
+
+describe('Component: SensorFieldSchema', () => {
+    let component: SensorFieldSchemaComponent;
+    let sensorEnrichmentConfigService: SensorEnrichmentConfigService;
+    let sensorParserConfigService: SensorParserConfigService;
+    let fixture: ComponentFixture<SensorFieldSchemaComponent>;
+    let transformationValidationService: StellarService;
+
+    let squidSensorConfigJson = {
+        'parserClassName': 'org.apache.metron.parsers.GrokParser',
+        'sensorTopic': 'squid',
+        'parserConfig': {
+            'grokPath': 'target/patterns/squid',
+            'grokStatement': '%{NUMBER:timestamp} %{INT:elapsed} %{IPV4:ip_src_addr} %{WORD:action}/%{NUMBER:code} ' +
+                             '%{NUMBER:bytes} %{WORD:method} %{NOTSPACE:url} - %{WORD:UNWANTED}\\/%{IPV4:ip_dst_addr} ' +
+                             '%{WORD:UNWANTED}\\/%{WORD:UNWANTED}'
+        },
+        'fieldTransformations': [
+            {
+                'input': [],
+                'output': ['method'],
+                'transformation': 'STELLAR',
+                'config': {
+                    'method': 'TRIM(TO_LOWER(method))'
+                }
+            },
+            {
+                'input': ['code'],
+                'output': null,
+                'transformation': 'REMOVE',
+                'config': {
+                    'condition': 'exists(field2)'
+                }
+            },
+            {
+                'input': ['ip_src_addr'],
+                'output': null,
+                'transformation': 'REMOVE'
+            }
+        ]
+    };
+    let squidEnrichmentJson = {
+        'index': 'squid',
+        'batchSize': 1,
+        'enrichment': {
+            'fieldMap': {
+                'geo': ['ip_dst_addr', 'ip_src_addr'],
+                'host': ['ip_dst_addr'],
+                'whois': ['ip_src_addr']
+            },
+            'fieldToTypeMap': {},
+            'config': {}
+        },
+        'threatIntel': {
+            'fieldMap': {
+                'hbaseThreatIntel': ['ip_dst_addr']
+            },
+            'fieldToTypeMap': {
+                'ip_dst_addr': ['malicious_ip']
+            },
+            'config': {},
+            'triageConfig': {
+                'riskLevelRules': {},
+                'aggregator': 'MAX',
+                'aggregationConfig': {}
+            }
+        },
+        'configuration': {}
+    };
+    let sensorParserConfig = Object.assign(new SensorParserConfig(), squidSensorConfigJson);
+    let sensorEnrichmentConfig = Object.assign(new SensorEnrichmentConfig(), squidEnrichmentJson);
+
+    beforeEach(async(() => {
+      TestBed.configureTestingModule({
+        imports: [SensorFieldSchemaModule],
+        providers: [
+          MetronAlerts,
+          {provide: Http},
+          {provide: KafkaService, useClass: MockKafkaService},
+          {provide: SensorEnrichmentConfigService, useClass: MockSensorEnrichmentConfigService},
+          {provide: SensorParserConfigService, useClass: MockSensorParserConfigService},
+          {provide: StellarService, useClass: MockTransformationValidationService},
+
+        ]
+      }).compileComponents()
+          .then(() => {
+            fixture = TestBed.createComponent(SensorFieldSchemaComponent);
+            component = fixture.componentInstance;
+            sensorParserConfigService = fixture.debugElement.injector.get(SensorParserConfigService);
+            transformationValidationService = fixture.debugElement.injector.get(StellarService);
+            sensorEnrichmentConfigService = fixture.debugElement.injector.get(SensorEnrichmentConfigService);
+          });
+    }));
+
+    it('should create an instance', () => {
+      expect(component).toBeDefined();
+      fixture.destroy();
+    });
+
+
+
+    it('should read TransformFunctions, EnrichmentFunctions, ThreatIntelfunctions', () => {
+        component.ngOnInit();
+
+        expect(component.transformOptions.length).toEqual(3);
+        expect(Object.keys(component.transformFunctions).length).toEqual(3);
+        expect(component.enrichmentOptions.length).toEqual(3);
+        expect(component.threatIntelOptions.length).toEqual(1);
+
+        fixture.destroy();
+    });
+
+    it('should call getSampleData if showFieldSchema', () => {
+        spyOn(component.sampleData, 'getNextSample');
+
+        let changes: SimpleChanges = {
+            'showFieldSchema': new SimpleChange(false, true)
+        };
+        component.ngOnChanges(changes);
+        expect(component.sampleData.getNextSample['calls'].count()).toEqual(1);
+
+        changes = {
+            'showFieldSchema': new SimpleChange(true, false)
+        };
+        component.ngOnChanges(changes);
+        expect(component.sampleData.getNextSample['calls'].count()).toEqual(1);
+
+        fixture.destroy();
+    });
+
+    it('should return isSimple function', () => {
+        component.ngOnInit();
+
+        expect(component.isSimpleFunction(['TO_LOWER', 'TO_UPPER'])).toEqual(true);
+        expect(component.isSimpleFunction(['TO_LOWER', 'TO_UPPER', 'TEST'])).toEqual(false);
+
+        fixture.destroy();
+    });
+
+    it('should create FieldSchemaRows', () => {
+        component.ngOnInit();
+
+        component.sensorParserConfig = sensorParserConfig;
+        component.sensorEnrichmentConfig = sensorEnrichmentConfig;
+        component.onSampleDataChanged('DoctorStrange');
+        component.createFieldSchemaRows();
+
+        expect(component.fieldSchemaRows.length).toEqual(10);
+        expect(component.savedFieldSchemaRows.length).toEqual(10);
+
+        let methodFieldSchemaRow: FieldSchemaRow = component.fieldSchemaRows.filter(row => row.inputFieldName === 'method')[0];
+        expect(methodFieldSchemaRow).toBeDefined();
+        expect(methodFieldSchemaRow.transformConfigured.length).toEqual(2);
+        expect(methodFieldSchemaRow.enrichmentConfigured.length).toEqual(0);
+        expect(methodFieldSchemaRow.threatIntelConfigured.length).toEqual(0);
+
+        let ipSrcAddrFieldSchemaRow: FieldSchemaRow = component.fieldSchemaRows.filter(row => row.inputFieldName === 'ip_src_addr')[0];
+        expect(ipSrcAddrFieldSchemaRow).toBeDefined();
+        expect(ipSrcAddrFieldSchemaRow.transformConfigured.length).toEqual(0);
+        expect(ipSrcAddrFieldSchemaRow.enrichmentConfigured.length).toEqual(2);
+        expect(ipSrcAddrFieldSchemaRow.threatIntelConfigured.length).toEqual(0);
+
+        let ipDstAddrFieldSchemaRow: FieldSchemaRow = component.fieldSchemaRows.filter(row => row.inputFieldName === 'ip_dst_addr')[0];
+        expect(ipDstAddrFieldSchemaRow).toBeDefined();
+        expect(ipDstAddrFieldSchemaRow.transformConfigured.length).toEqual(0);
+        expect(ipDstAddrFieldSchemaRow.enrichmentConfigured.length).toEqual(2);
+        expect(ipDstAddrFieldSchemaRow.threatIntelConfigured.length).toEqual(1);
+
+        let codeSchemaRow: FieldSchemaRow = component.fieldSchemaRows.filter(row => row.inputFieldName === 'code')[0];
+        expect(codeSchemaRow).toBeDefined();
+        expect(codeSchemaRow.isRemoved).toEqual(true);
+        expect(codeSchemaRow.conditionalRemove).toEqual(true);
+        expect(codeSchemaRow.transformConfigured.length).toEqual(0);
+        expect(codeSchemaRow.enrichmentConfigured.length).toEqual(0);
+        expect(codeSchemaRow.threatIntelConfigured.length).toEqual(0);
+
+        fixture.destroy();
+    });
+
+    it('should  return getChanges', () => {
+        let fieldSchemaRow = new FieldSchemaRow('method');
+        fieldSchemaRow.transformConfigured = [];
+        fieldSchemaRow.enrichmentConfigured = [new AutocompleteOption('GEO'), new AutocompleteOption('WHOIS')];
+        fieldSchemaRow.threatIntelConfigured = [new AutocompleteOption('MALICIOUS-IP')];
+
+        expect(component.getChanges(fieldSchemaRow)).toEqual('Enrichments: GEO, WHOIS <br> Threat Intel: MALICIOUS-IP');
+
+        fieldSchemaRow.transformConfigured = [new AutocompleteOption('TO_STRING')];
+        fieldSchemaRow.enrichmentConfigured = [new AutocompleteOption('GEO')];
+        fieldSchemaRow.threatIntelConfigured = [new AutocompleteOption('MALICIOUS-IP'), new AutocompleteOption('MALICIOUS-IP')];
+
+        expect(component.getChanges(fieldSchemaRow)).toEqual('Transforms: TO_STRING(method) <br> Enrichments: GEO <br> Threat Intel: MALICIOUS-IP, MALICIOUS-IP');
+
+
+        fieldSchemaRow.transformConfigured = [new AutocompleteOption('TO_STRING'), new AutocompleteOption('TO_STRING')];
+        fieldSchemaRow.enrichmentConfigured = [];
+        fieldSchemaRow.threatIntelConfigured = [new AutocompleteOption('MALICIOUS-IP'), new AutocompleteOption('MALICIOUS-IP')];
+
+        expect(component.getChanges(fieldSchemaRow)).toEqual('Transforms: TO_STRING(TO_STRING(method)) <br> Threat Intel: MALICIOUS-IP, MALICIOUS-IP');
+
+        fieldSchemaRow.transformConfigured = [new AutocompleteOption('TO_STRING'), new AutocompleteOption('TO_STRING')];
+        fieldSchemaRow.enrichmentConfigured = [];
+        fieldSchemaRow.threatIntelConfigured = [];
+
+        expect(component.getChanges(fieldSchemaRow)).toEqual('Transforms: TO_STRING(TO_STRING(method)) <br> ');
+
+        fieldSchemaRow.transformConfigured = [new AutocompleteOption('TO_STRING'), new AutocompleteOption('TO_STRING')];
+        fieldSchemaRow.isRemoved = true;
+        expect(component.getChanges(fieldSchemaRow)).toEqual('Disabled');
+
+        fixture.destroy();
+    });
+
+    it('should call appropriate functions when onSampleDataChanged is called ', () => {
+        let returnSuccess = true;
+        spyOn(component, 'createFieldSchemaRows');
+        spyOn(component, 'onSampleDataNotAvailable');
+        spyOn(sensorParserConfigService, 'parseMessage').and.callFake(function(parseMessageRequest: ParseMessageRequest) {
+            expect(parseMessageRequest.sensorParserConfig.parserConfig['patternLabel']).toEqual(parseMessageRequest.sensorParserConfig.sensorTopic.toUpperCase());
+            expect(parseMessageRequest.sensorParserConfig.parserConfig['grokPath']).toEqual('./' + parseMessageRequest.sensorParserConfig.sensorTopic);
+            if (returnSuccess) {
+                return Observable.create(observer => {
+                    observer.next({'a': 'b', 'c': 'd'});
+                    observer.complete();
+                });
+            }
+            return Observable.throw('Error');
+        });
+
+        component.sensorParserConfig = sensorParserConfig;
+        component.sensorParserConfig.parserConfig['patternLabel'] = null;
+        component.onSampleDataChanged('DoctorStrange');
+        expect(component.parserResult).toEqual({'a': 'b', 'c': 'd'});
+        expect(component.createFieldSchemaRows).toHaveBeenCalled();
+        expect(component.onSampleDataNotAvailable).not.toHaveBeenCalled();
+
+        returnSuccess = false;
+        component.parserResult = {};
+        component.onSampleDataChanged('DoctorStrange');
+        expect(component.parserResult).toEqual({});
+        expect(component.onSampleDataNotAvailable).toHaveBeenCalled();
+        expect(component.onSampleDataNotAvailable['calls'].count()).toEqual(1);
+
+        fixture.destroy();
+    });
+
+    it('should onSampleDataChanged available and onSampleDataNotAvailable ', () => {
+        let returnSuccess = true;
+        spyOn(component, 'createFieldSchemaRows');
+
+        component.onSampleDataNotAvailable();
+        expect(component.createFieldSchemaRows['calls'].count()).toEqual(1);
+
+        fixture.destroy();
+    });
+
+    it('should call onSaveChange on onRemove/onEnable ', () => {
+        spyOn(component, 'onSave');
+
+        let fieldSchemaRow = new FieldSchemaRow('method');
+        fieldSchemaRow.outputFieldName = 'copy-of-method';
+        fieldSchemaRow.preview = 'TRIM(TO_LOWER(method))';
+        fieldSchemaRow.isRemoved = false;
+
+        component.savedFieldSchemaRows = [fieldSchemaRow];
+
+        let removeFieldSchemaRow = JSON.parse(JSON.stringify(fieldSchemaRow));
+        component.onRemove(removeFieldSchemaRow);
+        expect(removeFieldSchemaRow.isRemoved).toEqual(true);
+        expect(component.savedFieldSchemaRows[0].isRemoved).toEqual(true);
+        expect(component.onSave['calls'].count()).toEqual(1);
+
+        fieldSchemaRow.isRemoved = true;
+        let enableFieldSchemaRow = JSON.parse(JSON.stringify(fieldSchemaRow));
+        component.onEnable(enableFieldSchemaRow);
+        expect(fieldSchemaRow.isRemoved).toEqual(false);
+        expect(component.savedFieldSchemaRows[0].isRemoved).toEqual(false);
+        expect(component.onSave['calls'].count()).toEqual(2);
+
+        fixture.destroy();
+    });
+
+    it('should revert changes on cancel ', () => {
+        let fieldSchemaRow = new FieldSchemaRow('method');
+        fieldSchemaRow.showConfig = true;
+        fieldSchemaRow.outputFieldName = 'method';
+        fieldSchemaRow.preview = 'TRIM(TO_LOWER(method))';
+        fieldSchemaRow.isRemoved = false;
+        fieldSchemaRow.isSimple = true;
+        fieldSchemaRow.transformConfigured = [new AutocompleteOption('TO_LOWER'), new AutocompleteOption('TRIM')];
+
+        component.savedFieldSchemaRows.push(fieldSchemaRow);
+
+        component.onCancelChange(fieldSchemaRow);
+        expect(fieldSchemaRow.showConfig).toEqual(false);
+
+        component.hideFieldSchema.emit = jasmine.createSpy('emit');
+        component.onCancel();
+        expect(component.hideFieldSchema.emit).toHaveBeenCalled();
+
+        fixture.destroy();
+    });
+
+    it('should return formatted function on createTransformFunction call ', () => {
+        let fieldSchemaRow = new FieldSchemaRow('method');
+        fieldSchemaRow.transformConfigured = [new AutocompleteOption('TRIM'), new AutocompleteOption('TO_STRING')];
+
+        expect(component.createTransformFunction(fieldSchemaRow)).toEqual('TO_STRING(TRIM(method))');
+
+        fixture.destroy();
+    });
+
+    it('should set preview value for FieldSchemaRow ', () => {
+        let fieldSchemaRow = new FieldSchemaRow('method');
+        fieldSchemaRow.transformConfigured = [new AutocompleteOption('TRIM'), new AutocompleteOption('TO_STRING')];
+
+        component.onTransformsChange(fieldSchemaRow);
+        expect(fieldSchemaRow.preview).toEqual('TO_STRING(TRIM(method))');
+
+        fieldSchemaRow.transformConfigured = [new AutocompleteOption('TRIM')];
+        component.onTransformsChange(fieldSchemaRow);
+        expect(fieldSchemaRow.preview).toEqual('TRIM(method)');
+
+        fieldSchemaRow.transformConfigured = [];
+        component.onTransformsChange(fieldSchemaRow);
+        expect(fieldSchemaRow.preview).toEqual('');
+
+        fixture.destroy();
+    });
+
+    it('isConditionalRemoveTransform ', () => {
+        let fieldTransformationJson = {
+            'input': ['method'],
+            'transformation': 'REMOVE',
+            'config':
+            {
+                'condition': 'IS_DOMAIN(elapsed)'
+            }
+        };
+        let simpleFieldTransformationJson = {
+          'input': ['method'],
+          'transformation': 'REMOVE'
+        };
+        let fieldTransformation: FieldTransformer = Object.assign(new FieldTransformer(), fieldTransformationJson);
+        expect(component.isConditionalRemoveTransform(fieldTransformation)).toEqual(true);
+
+        let simpleFieldTransformation: FieldTransformer = Object.assign(new FieldTransformer(), simpleFieldTransformationJson);
+        expect(component.isConditionalRemoveTransform(simpleFieldTransformation)).toEqual(false);
+
+        fixture.destroy();
+    });
+
+    it('should save data ', () => {
+        let methodFieldSchemaRow = new FieldSchemaRow('method');
+        methodFieldSchemaRow.outputFieldName = 'method';
+        methodFieldSchemaRow.preview = 'TRIM(TO_LOWER(method))';
+        methodFieldSchemaRow.isRemoved = false;
+        methodFieldSchemaRow.isSimple = true;
+        methodFieldSchemaRow.transformConfigured = [new AutocompleteOption('TO_LOWER'), new AutocompleteOption('TRIM')];
+
+        let elapsedFieldSchemaRow = new FieldSchemaRow('elapsed');
+        elapsedFieldSchemaRow.outputFieldName = 'elapsed';
+        elapsedFieldSchemaRow.preview = 'IS_DOMAIN(elapsed)';
+        elapsedFieldSchemaRow.isRemoved = true;
+        elapsedFieldSchemaRow.isSimple = true;
+        elapsedFieldSchemaRow.transformConfigured = [new AutocompleteOption('IS_DOMAIN')];
+        elapsedFieldSchemaRow.enrichmentConfigured = [new AutocompleteOption('host')];
+
+        let ipDstAddrFieldSchemaRow = new FieldSchemaRow('ip_dst_addr');
+        ipDstAddrFieldSchemaRow.outputFieldName = 'ip_dst_addr';
+        ipDstAddrFieldSchemaRow.preview = 'IS_DOMAIN(elapsed)';
+        ipDstAddrFieldSchemaRow.isRemoved = false;
+        ipDstAddrFieldSchemaRow.isSimple = false;
+        ipDstAddrFieldSchemaRow.threatIntelConfigured = [new AutocompleteOption('malicious_ip')];
+        ipDstAddrFieldSchemaRow.enrichmentConfigured = [new AutocompleteOption('host')];
+
+        let codeFieldSchemaRow = new FieldSchemaRow('code');
+        codeFieldSchemaRow.outputFieldName = 'code';
+        codeFieldSchemaRow.isRemoved = true;
+        codeFieldSchemaRow.conditionalRemove = true;
+
+        component.savedFieldSchemaRows = [methodFieldSchemaRow, elapsedFieldSchemaRow, ipDstAddrFieldSchemaRow, codeFieldSchemaRow];
+
+        component.sensorParserConfig = new SensorParserConfig();
+        component.sensorParserConfig.parserClassName = 'org.apache.metron.parsers.GrokParser';
+        component.sensorParserConfig.sensorTopic = 'squid';
+
+      component.sensorParserConfig.fieldTransformations = [new FieldTransformer()];
+      component.sensorParserConfig.fieldTransformations[0].transformation = 'REMOVE';
+      component.sensorParserConfig.fieldTransformations[0].input = ['code'];
+      component.sensorParserConfig.fieldTransformations[0].config = {'condition': 'exists(method)'};
+
+        component.sensorEnrichmentConfig = new SensorEnrichmentConfig();
+        component.sensorEnrichmentConfig.enrichment = new  EnrichmentConfig();
+        component.sensorEnrichmentConfig.threatIntel = new ThreatIntelConfig();
+        component.sensorEnrichmentConfig.configuration = {};
+
+        component.onSave();
+
+        let fieldTransformationJson = {
+            'output': ['method', 'elapsed'],
+            'transformation': 'STELLAR',
+            'config':
+            {
+                    'method': 'TRIM(TO_LOWER(method))',
+                'elapsed': 'IS_DOMAIN(elapsed)'
+            }
+        };
+
+        let fieldTransformationRemoveJson = {
+            'input': ['elapsed'],
+            'transformation': 'REMOVE',
+        };
+
+        let conditionalFieldTransformationRemoveJson = {
+          'input': ['code'],
+          'transformation': 'REMOVE',
+          'config': {
+            'condition': 'exists(method)'
+          }
+        };
+
+        let fieldTransformation = Object.assign(new FieldTransformer(), fieldTransformationJson);
+        let fieldTransformationRemove = Object.assign(new FieldTransformer(), fieldTransformationRemoveJson);
+      let conditionalFieldTransformationRemove = Object.assign(new FieldTransformer(), conditionalFieldTransformationRemoveJson);
+
+        expect(component.sensorParserConfig.fieldTransformations.length).toEqual(3);
+        let expectedStellar = component.sensorParserConfig.fieldTransformations.filter(transform => transform.transformation === 'STELLAR')[0];
+        let expectedRemove = component.sensorParserConfig.fieldTransformations.filter(transform => transform.transformation === 'REMOVE' && !transform.config)[0];
+        let expectedConditionalRemove = component.sensorParserConfig.fieldTransformations.filter(transform => transform.transformation === 'REMOVE' && transform.config)[0];
+        expect(expectedStellar).toEqual(fieldTransformation);
+        expect(expectedRemove).toEqual(fieldTransformationRemove);
+      expect(expectedConditionalRemove).toEqual(conditionalFieldTransformationRemove);
+
+        fixture.destroy();
+    });
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.ts b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.ts
new file mode 100644
index 0000000..8d9dd11
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.ts
@@ -0,0 +1,435 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* tslint:disable:max-line-length */
+import { Component, OnInit, Input, OnChanges, ViewChild, SimpleChanges, Output, EventEmitter } from '@angular/core';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import {ParseMessageRequest} from '../../model/parse-message-request';
+import {SensorParserConfigService} from '../../service/sensor-parser-config.service';
+import {StellarService} from '../../service/stellar.service';
+import {AutocompleteOption} from '../../model/autocomplete-option';
+import {StellarFunctionDescription} from '../../model/stellar-function-description';
+import {SensorEnrichmentConfig, EnrichmentConfig, ThreatIntelConfig} from '../../model/sensor-enrichment-config';
+import {FieldTransformer} from '../../model/field-transformer';
+import {SampleDataComponent} from '../../shared/sample-data/sample-data.component';
+import {MetronAlerts} from '../../shared/metron-alerts';
+import {SensorEnrichmentConfigService} from '../../service/sensor-enrichment-config.service';
+
+export class FieldSchemaRow {
+  inputFieldName: string;
+  outputFieldName: string;
+  preview: string;
+  showConfig: boolean;
+  isRemoved: boolean;
+  isSimple: boolean;
+  isNew: boolean;
+  isParserGenerated: boolean;
+  conditionalRemove: boolean;
+  transformConfigured: AutocompleteOption[] = [];
+  enrichmentConfigured: AutocompleteOption[] = [];
+  threatIntelConfigured: AutocompleteOption[] = [];
+
+  constructor(fieldName: string) {
+    this.inputFieldName = fieldName;
+    this.outputFieldName = fieldName;
+    this.conditionalRemove = false;
+    this.isParserGenerated = false;
+    this.showConfig = false;
+    this.isSimple = true;
+    this.isRemoved = false;
+    this.preview = '';
+  }
+}
+
+@Component({
+  selector: 'metron-config-sensor-field-schema',
+  templateUrl: './sensor-field-schema.component.html',
+  styleUrls: ['./sensor-field-schema.component.scss']
+})
+export class SensorFieldSchemaComponent implements OnInit, OnChanges {
+
+  @Input() sensorParserConfig: SensorParserConfig;
+  @Input() sensorEnrichmentConfig: SensorEnrichmentConfig;
+  @Input() showFieldSchema: boolean;
+  @Input() grokStatement: string;
+
+  parserResult: any = {};
+  fieldSchemaRows: FieldSchemaRow[] = [];
+  savedFieldSchemaRows: FieldSchemaRow[] = [];
+
+  transformOptions: AutocompleteOption[] = [];
+  enrichmentOptions: AutocompleteOption[] = [];
+  threatIntelOptions: AutocompleteOption[] = [];
+
+  transformFunctions: StellarFunctionDescription[];
+
+  @ViewChild(SampleDataComponent) sampleData: SampleDataComponent;
+  @Output() hideFieldSchema: EventEmitter<boolean> = new EventEmitter<boolean>();
+  @Output() onFieldSchemaChanged: EventEmitter<boolean> = new EventEmitter<boolean>();
+
+  sampleThreatIntels: string[] = ['malicious_ip'];
+
+  constructor(private sensorParserConfigService: SensorParserConfigService,
+              private transformationValidationService: StellarService,
+              private sensorEnrichmentConfigService: SensorEnrichmentConfigService,
+              private metronAlerts: MetronAlerts) { }
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (changes['showFieldSchema'] && changes['showFieldSchema'].currentValue) {
+      this.sampleData.getNextSample();
+    }
+  }
+
+  ngOnInit() {
+    this.getTransformFunctions();
+    this.getEnrichmentFunctions();
+    this.getThreatIntelfunctions();
+  }
+
+  getTransformFunctions() {
+    this.transformOptions = [];
+
+    this.transformationValidationService.listSimpleFunctions().subscribe((result: StellarFunctionDescription[]) => {
+      this.transformFunctions = result;
+      for (let fun of result) {
+        this.transformOptions.push(new AutocompleteOption(fun.name, fun.name, fun.description));
+      }
+    });
+  }
+
+  getEnrichmentFunctions() {
+    this.enrichmentOptions = [];
+
+    this.sensorEnrichmentConfigService.getAvailableEnrichments().subscribe((result: string[]) => {
+      for (let fun of result) {
+        this.enrichmentOptions.push(new AutocompleteOption(fun));
+      }
+    });
+  }
+
+  getThreatIntelfunctions() {
+    this.threatIntelOptions = [];
+    for (let threatName of this.sampleThreatIntels) {
+      this.threatIntelOptions.push(new AutocompleteOption(threatName));
+    }
+
+  }
+
+  isSimpleFunction(configuredFunctions: string[]) {
+    for (let configuredFunction of configuredFunctions) {
+      if (this.transformFunctions.filter(stellarFunctionDescription => stellarFunctionDescription.name === configuredFunction).length === 0) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  isConditionalRemoveTransform(fieldTransformer: FieldTransformer): boolean {
+    if (fieldTransformer && fieldTransformer.transformation === 'REMOVE' &&
+        fieldTransformer.config && fieldTransformer.config['condition']) {
+      return true;
+    }
+
+    return false;
+  }
+
+  createFieldSchemaRows() {
+    this.fieldSchemaRows = [];
+    this.savedFieldSchemaRows = [];
+    let fieldSchemaRowsCreated = {};
+
+    // Update rows with Stellar transformations
+    let stellarTransformations = this.sensorParserConfig.fieldTransformations.filter(fieldTransformer => fieldTransformer.transformation === 'STELLAR');
+    for (let fieldTransformer of stellarTransformations) {
+      if (fieldTransformer.config) {
+        for (let outputFieldName of Object.keys(fieldTransformer.config)) {
+          let stellarFunctionStatement = fieldTransformer.config[outputFieldName];
+          let configuredFunctions = stellarFunctionStatement.split('(');
+          let inputFieldName = configuredFunctions.splice(-1, 1)[0].replace(new RegExp('\\)', 'g'), '');
+          configuredFunctions.reverse();
+          if (!fieldSchemaRowsCreated[inputFieldName]) {
+            fieldSchemaRowsCreated[inputFieldName] = new FieldSchemaRow(inputFieldName);
+          }
+          fieldSchemaRowsCreated[inputFieldName].outputFieldName = outputFieldName;
+          fieldSchemaRowsCreated[inputFieldName].preview = stellarFunctionStatement;
+          fieldSchemaRowsCreated[inputFieldName].isSimple = this.isSimpleFunction(configuredFunctions);
+          if (fieldSchemaRowsCreated[inputFieldName].isSimple) {
+            for (let configuredFunction of configuredFunctions) {
+              fieldSchemaRowsCreated[inputFieldName].transformConfigured.push(new AutocompleteOption(configuredFunction));
+            }
+          }
+        }
+      }
+    }
+
+    // Update rows with Remove Transformations
+    let removeTransformations = this.sensorParserConfig.fieldTransformations.filter(fieldTransformer => fieldTransformer.transformation === 'REMOVE');
+    for (let fieldTransformer of removeTransformations) {
+      for (let inputFieldName of fieldTransformer.input) {
+        if (!fieldSchemaRowsCreated[inputFieldName]) {
+          fieldSchemaRowsCreated[inputFieldName] = new FieldSchemaRow(inputFieldName);
+        }
+        fieldSchemaRowsCreated[inputFieldName].isRemoved = true;
+        if (fieldTransformer.config && fieldTransformer.config['condition']) {
+          fieldSchemaRowsCreated[inputFieldName].conditionalRemove = true;
+        }
+
+      }
+    }
+
+    // Update rows with enrichments
+    if (this.sensorEnrichmentConfig.enrichment.fieldMap) {
+      for (let enrichment in this.sensorEnrichmentConfig.enrichment.fieldMap) {
+        if (enrichment !== 'hbaseEnrichment' && enrichment !== 'stellar') {
+          let fieldNames = this.sensorEnrichmentConfig.enrichment.fieldMap[enrichment];
+          for (let fieldName of fieldNames) {
+            if (!fieldSchemaRowsCreated[fieldName]) {
+              fieldSchemaRowsCreated[fieldName] = new FieldSchemaRow(fieldName);
+            }
+            fieldSchemaRowsCreated[fieldName].enrichmentConfigured.push(new AutocompleteOption(enrichment));
+          }
+        }
+      }
+    }
+
+    // Update rows with HBase enrichments
+    if (this.sensorEnrichmentConfig.enrichment.fieldToTypeMap) {
+      for (let fieldName of Object.keys(this.sensorEnrichmentConfig.enrichment.fieldToTypeMap)) {
+        let enrichments = this.sensorEnrichmentConfig.enrichment.fieldToTypeMap[fieldName];
+        if (!fieldSchemaRowsCreated[fieldName]) {
+          fieldSchemaRowsCreated[fieldName] = new FieldSchemaRow(fieldName);
+        }
+        for (let enrichment of enrichments) {
+          fieldSchemaRowsCreated[fieldName].enrichmentConfigured.push(new AutocompleteOption(enrichment));
+        }
+      }
+    }
+
+    // Update rows with threatIntels
+    if (this.sensorEnrichmentConfig.threatIntel.fieldToTypeMap) {
+      for (let fieldName of  Object.keys(this.sensorEnrichmentConfig.threatIntel.fieldToTypeMap)) {
+        let threatIntels = this.sensorEnrichmentConfig.threatIntel.fieldToTypeMap[fieldName];
+
+        if (!fieldSchemaRowsCreated[fieldName]) {
+          fieldSchemaRowsCreated[fieldName] = new FieldSchemaRow(fieldName);
+        }
+
+        for (let threatIntel of threatIntels) {
+          fieldSchemaRowsCreated[fieldName].threatIntelConfigured.push(new AutocompleteOption(threatIntel));
+        }
+      }
+    }
+
+    this.fieldSchemaRows = Object.keys(fieldSchemaRowsCreated).map(key => fieldSchemaRowsCreated[key]);
+
+    // Adds rows from parseResult with no transformations/enrichments/threatIntels
+    let fieldSchemaRowsCreatedKeys = Object.keys(fieldSchemaRowsCreated);
+    for (let fieldName of Object.keys(this.parserResult).filter(fieldName => fieldSchemaRowsCreatedKeys.indexOf(fieldName) === -1)) {
+        let field = new FieldSchemaRow(fieldName);
+        field.isParserGenerated = true;
+        this.fieldSchemaRows.push(field);
+    }
+
+    // save the initial fieldSchemaRows
+    for (let fieldSchemaRow of this.fieldSchemaRows) {
+      this.savedFieldSchemaRows.push(JSON.parse(JSON.stringify(fieldSchemaRow)));
+    }
+  }
+
+  getChanges(fieldSchemaRow: FieldSchemaRow): string {
+
+    if (fieldSchemaRow.isRemoved) {
+      return 'Disabled';
+    }
+
+    let transformFunction = fieldSchemaRow.transformConfigured.length > 0 ? this.createTransformFunction(fieldSchemaRow) : '';
+    let enrichments = fieldSchemaRow.enrichmentConfigured.map(autocomplete => autocomplete.name).join(', ');
+    let threatIntel = fieldSchemaRow.threatIntelConfigured.map(autocomplete => autocomplete.name).join(', ');
+
+    transformFunction = transformFunction.length > 30 ? (transformFunction.substring(0, 25) + '...') : transformFunction;
+
+    let displayString = transformFunction.length > 0 ? ('Transforms: ' + transformFunction) : '';
+    displayString += (transformFunction.length > 0 ? ' <br> ' : '') + (enrichments.length > 0 ? ('Enrichments: ' + enrichments) : '');
+    displayString += (enrichments.length > 0 ? ' <br> ' : '') + (threatIntel.length > 0 ? ('Threat Intel: ' + threatIntel) : '');
+
+    return displayString;
+  }
+
+  onSampleDataChanged(sampleData: string) {
+    let sensorTopicUpperCase = this.sensorParserConfig.sensorTopic.toUpperCase();
+    let parseMessageRequest = new ParseMessageRequest();
+    parseMessageRequest.sensorParserConfig = JSON.parse(JSON.stringify(this.sensorParserConfig));
+    parseMessageRequest.grokStatement = this.grokStatement;
+    parseMessageRequest.sampleData = sampleData;
+
+    if (parseMessageRequest.sensorParserConfig.parserConfig['patternLabel'] == null) {
+      parseMessageRequest.sensorParserConfig.parserConfig['patternLabel'] = sensorTopicUpperCase;
+    }
+    parseMessageRequest.sensorParserConfig.parserConfig['grokPath'] = './' + parseMessageRequest.sensorParserConfig.sensorTopic;
+
+    this.sensorParserConfigService.parseMessage(parseMessageRequest).subscribe(
+        parserResult => {
+          this.parserResult = parserResult;
+          this.createFieldSchemaRows();
+        },
+        error => {
+          this.onSampleDataNotAvailable();
+        });
+  }
+
+  onSampleDataNotAvailable() {
+    this.createFieldSchemaRows();
+  }
+
+  onDelete(fieldSchemaRow: FieldSchemaRow) {
+    this.fieldSchemaRows.splice(this.fieldSchemaRows.indexOf(fieldSchemaRow), 1);
+    this.savedFieldSchemaRows.splice(this.fieldSchemaRows.indexOf(fieldSchemaRow), 1);
+  }
+
+  onRemove(fieldSchemaRow: FieldSchemaRow) {
+    fieldSchemaRow.isRemoved = true;
+    this.onSaveChange(fieldSchemaRow);
+  }
+
+  onEnable(fieldSchemaRow: FieldSchemaRow) {
+    if (fieldSchemaRow.conditionalRemove) {
+      this.metronAlerts.showErrorMessage('The "' + fieldSchemaRow.outputFieldName + '" field cannot be enabled because the REMOVE transformation has a condition.  Please remove the condition in the RAW JSON editor.');
+      return;
+    }
+    fieldSchemaRow.isRemoved = false;
+    this.onSaveChange(fieldSchemaRow);
+  }
+
+  onSaveChange(savedFieldSchemaRow: FieldSchemaRow) {
+    savedFieldSchemaRow.showConfig = false;
+    savedFieldSchemaRow.isNew = false;
+    let initialSchemaRow = this.savedFieldSchemaRows.filter(fieldSchemaRow => fieldSchemaRow.inputFieldName === savedFieldSchemaRow.inputFieldName)[0];
+    Object.assign(initialSchemaRow, JSON.parse(JSON.stringify(savedFieldSchemaRow)));
+
+    this.onSave();
+  }
+
+  onCancelChange(cancelledFieldSchemaRow: FieldSchemaRow) {
+    cancelledFieldSchemaRow.showConfig = false;
+    let initialSchemaRow = this.savedFieldSchemaRows.filter(fieldSchemaRow => fieldSchemaRow.inputFieldName === cancelledFieldSchemaRow.inputFieldName)[0];
+    Object.assign(cancelledFieldSchemaRow, JSON.parse(JSON.stringify(initialSchemaRow)));
+  }
+
+  onCancel(): void {
+    this.hideFieldSchema.emit(true);
+  }
+
+  createTransformFunction(fieldSchemaRow: FieldSchemaRow): string {
+    let func = fieldSchemaRow.inputFieldName;
+
+    for (let config of fieldSchemaRow.transformConfigured) {
+      func = config.name + '(' + func + ')';
+    }
+
+    return func;
+  }
+
+  onTransformsChange(fieldSchemaRow: FieldSchemaRow): void {
+    fieldSchemaRow.preview = fieldSchemaRow.transformConfigured.length === 0 ? '' : this.createTransformFunction(fieldSchemaRow);
+  }
+
+  addNewRule() {
+    let fieldSchemaRow = new FieldSchemaRow('new');
+    fieldSchemaRow.isNew = true;
+    fieldSchemaRow.showConfig = true;
+    fieldSchemaRow.inputFieldName = '';
+    this.fieldSchemaRows.push(fieldSchemaRow);
+  }
+
+  onSave() {
+    let removeTransformations: string[] = [];
+
+    // Remove all STELLAR functions and retain only the REMOVE objects
+    this.sensorParserConfig.fieldTransformations = this.sensorParserConfig.fieldTransformations.filter(fieldTransformer => {
+      if (this.isConditionalRemoveTransform(fieldTransformer)) {
+        return true;
+      }
+      return false;
+    });
+
+    let transformConfigObject = new FieldTransformer();
+    transformConfigObject.output = [];
+    transformConfigObject.config = {};
+    transformConfigObject.transformation = 'STELLAR';
+
+    let enrichmentConfigObject = new EnrichmentConfig();
+    enrichmentConfigObject.config = {};
+    let threatIntelConfigObject = new ThreatIntelConfig();
+    threatIntelConfigObject.triageConfig = this.sensorEnrichmentConfig.threatIntel.triageConfig;
+
+
+    for (let fieldSchemaRow of this.savedFieldSchemaRows) {
+      if (fieldSchemaRow.transformConfigured.length > 0) {
+        transformConfigObject.output.push(fieldSchemaRow.outputFieldName);
+        transformConfigObject.config[fieldSchemaRow.outputFieldName] = this.createTransformFunction(fieldSchemaRow);
+      }
+      if (fieldSchemaRow.isRemoved && !fieldSchemaRow.conditionalRemove) {
+        removeTransformations.push(fieldSchemaRow.inputFieldName);
+      }
+      if (fieldSchemaRow.enrichmentConfigured.length > 0) {
+        for (let option of fieldSchemaRow.enrichmentConfigured) {
+          if (option.name === 'geo' || option.name === 'host') {
+            if (!enrichmentConfigObject.fieldMap[option.name]) {
+              enrichmentConfigObject.fieldMap[option.name] = [];
+            }
+            enrichmentConfigObject.fieldMap[option.name].push(fieldSchemaRow.inputFieldName);
+          } else {
+            if (!enrichmentConfigObject.fieldMap['hbaseEnrichment']) {
+              enrichmentConfigObject.fieldMap['hbaseEnrichment'] = [];
+            }
+            enrichmentConfigObject.fieldMap['hbaseEnrichment'].push(fieldSchemaRow.inputFieldName);
+            if (!enrichmentConfigObject.fieldToTypeMap[fieldSchemaRow.inputFieldName]) {
+              enrichmentConfigObject.fieldToTypeMap[fieldSchemaRow.inputFieldName] = [];
+            }
+            enrichmentConfigObject.fieldToTypeMap[fieldSchemaRow.inputFieldName].push(option.name);
+          }
+        }
+      }
+      if (fieldSchemaRow.threatIntelConfigured.length > 0) {
+        for (let option of fieldSchemaRow.threatIntelConfigured) {
+          if (!threatIntelConfigObject.fieldMap['hbaseThreatIntel']) {
+            threatIntelConfigObject.fieldMap['hbaseThreatIntel'] = [];
+          }
+          threatIntelConfigObject.fieldMap['hbaseThreatIntel'].push(fieldSchemaRow.inputFieldName);
+          if (!threatIntelConfigObject.fieldToTypeMap[fieldSchemaRow.inputFieldName]) {
+            threatIntelConfigObject.fieldToTypeMap[fieldSchemaRow.inputFieldName] = [];
+          }
+          threatIntelConfigObject.fieldToTypeMap[fieldSchemaRow.inputFieldName].push(option.name);
+        }
+      }
+    }
+
+    if (Object.keys(transformConfigObject.config).length > 0) {
+      this.sensorParserConfig.fieldTransformations.push(transformConfigObject);
+    }
+
+    if (removeTransformations.length > 0) {
+      let removeConfigObject = new FieldTransformer();
+      removeConfigObject.transformation = 'REMOVE';
+      removeConfigObject.input = removeTransformations;
+      this.sensorParserConfig.fieldTransformations.push(removeConfigObject);
+    }
+
+    this.sensorEnrichmentConfig.enrichment = enrichmentConfigObject;
+    this.sensorEnrichmentConfig.threatIntel = threatIntelConfigObject;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.module.ts b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.module.ts
new file mode 100644
index 0000000..afa3d55
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.module.ts
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {NgModule} from '@angular/core';
+import {SharedModule} from '../../shared/shared.module';
+import {MultipleInputModule} from '../../shared/multiple-input/multiple-input.module';
+import {SampleDataModule} from '../../shared/sample-data/sample-data.module';
+import {SensorFieldSchemaComponent} from './sensor-field-schema.component';
+
+@NgModule ({
+    imports: [ SharedModule, MultipleInputModule, SampleDataModule ],
+    declarations: [ SensorFieldSchemaComponent ],
+    exports: [ SensorFieldSchemaComponent ]
+})
+
+export class SensorFieldSchemaModule { }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-grok/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-grok/index.ts b/metron-interface/metron-config/src/app/sensors/sensor-grok/index.ts
new file mode 100644
index 0000000..03c0507
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-grok/index.ts
@@ -0,0 +1,18 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export * from './sensor-grok.component';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.html b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.html
new file mode 100644
index 0000000..c9bdc06
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.html
@@ -0,0 +1,46 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<div class="metron-slider-pane-edit fill load-left-to-right dialog2x">
+
+    <div class="form-title">Grok Validator</div>
+    <i class="fa fa-times pull-right close-button" aria-hidden="true" (click)="onCancelGrok()"></i>
+
+    <form role="form" class="grok-form">
+        <metron-config-sample-data [topic]="sensorParserConfig.sensorTopic"
+                                   (onSampleDataChanged)="onSampleDataChanged($event)"></metron-config-sample-data>
+        <label attr.for="patternLabel">PATTERN LABEL</label>
+        <select class="form-control pattern-label-dropdown" [ngModelOptions]="{standalone: true}" [(ngModel)]="newPatternLabel">
+          <option *ngFor="let patternLabel of availablePatternLabels" [value]="patternLabel"> {{ patternLabel }} </option>
+        </select>
+        <label attr.for="grokStatement">STATEMENT</label>
+        <metron-config-ace-editor [(ngModel)]="newGrokStatement" [ngModelOptions]="{standalone: true}" [type]="'GROK'" [options]="grokFunctionList" [placeHolder]="'Enter Grok statement'" (ngModelChange)="getAvailablePatternLabels()"> </metron-config-ace-editor>
+
+        <div class="buttons-bar">
+            <button type="submit" class="btn form-enable-disable-button" [disabled]="isTestDisabled()" (click)="onTestGrokStatement()">TEST</button>
+            <button type="submit" class="btn form-enable-disable-button" [disabled]="isSaveDisabled()" (click)="onSaveGrok()">SAVE</button>
+        </div>
+
+        <label> PREVIEW </label>
+        <table class="table form-table" #table>
+            <tbody>
+            <tr *ngFor="let key of parsedMessageKeys">
+                <td>{{ key }}</td>
+                <td>{{ parsedMessage[key] }}</td>
+            </tr>
+            </tbody>
+        </table>
+    </form>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.scss b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.scss
new file mode 100644
index 0000000..924d6a4
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.scss
@@ -0,0 +1,67 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.form-title
+{
+  padding-left: 25px;
+}
+
+.grok-form
+{
+  padding-left: 25px;
+  padding-right: 20px;
+}
+
+.close-button
+{
+  padding-right: 20px;
+}
+
+
+.form-table
+{
+  display: inline-block;
+  min-height: 100px;
+  background: #033339;
+  margin-bottom: 100px;
+}
+.table, table
+{
+  margin-top: 0px;
+  tr td
+  {
+    border-bottom: 1px solid #3F4748;
+  }
+  tr:last-child td
+  {
+    border-bottom: none;
+  }
+  td:last-child {
+    width: 100%;
+  }
+
+}
+.buttons-bar
+{
+  margin-top: 10px;
+  margin-bottom: 15px;
+}
+
+.pattern-label-dropdown
+{
+  width: 40%;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.spec.ts
new file mode 100644
index 0000000..eac839e
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.spec.ts
@@ -0,0 +1,234 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { TestBed, async, ComponentFixture } from '@angular/core/testing';
+import {SimpleChange} from '@angular/core';
+import {Http} from '@angular/http';
+import {SensorParserConfigService} from '../../service/sensor-parser-config.service';
+import {MetronAlerts} from '../../shared/metron-alerts';
+import {KafkaService} from '../../service/kafka.service';
+import {Observable} from 'rxjs/Observable';
+import {ParseMessageRequest} from '../../model/parse-message-request';
+import {SensorGrokComponent} from './sensor-grok.component';
+import {GrokValidationService} from '../../service/grok-validation.service';
+import {SensorGrokModule} from './sensor-grok.module';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import '../../rxjs-operators';
+
+class MockSensorParserConfigService {
+
+  private parsedMessage: string;
+
+  public parseMessage(parseMessageRequest: ParseMessageRequest): Observable<{}> {
+    if (this.parsedMessage === 'ERROR') {
+      return Observable.throw({'_body': JSON.stringify({'abc': 'def'}) });
+    }
+
+    return Observable.create(observer => {
+      observer.next(this.parsedMessage);
+      observer.complete();
+    });
+  }
+
+  public setParsedMessage(parsedMessage: any) {
+    this.parsedMessage = parsedMessage;
+  }
+}
+
+class MockGrokValidationService {
+  public list(): Observable<string[]> {
+    return Observable.create(observer => {
+      observer.next({
+        'BASE10NUM': '(?<![0-9.+-])(?>[+-]?(?:(?:[0-9]+(?:\\.[0-9]+)?)|(?:\\.[0-9]+)))',
+        'BASE16FLOAT': '\\b(?<![0-9A-Fa-f.])(?:[+-]?(?:0x)?(?:(?:[0-9A-Fa-f]+(?:\\.[0-9A-Fa-f]*)?)|(?:\\.[0-9A-Fa-f]+)))\\b',
+        'BASE16NUM': '(?<![0-9A-Fa-f])(?:[+-]?(?:0x)?(?:[0-9A-Fa-f]+))',
+        'CISCOMAC': '(?:(?:[A-Fa-f0-9]{4}\\.){2}[A-Fa-f0-9]{4})',
+        'COMMONMAC': '(?:(?:[A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2})',
+        'DATA': '.*?'
+      });
+      observer.complete();
+    });
+  }
+}
+
+class MockKafkaService {
+
+}
+
+describe('Component: SensorGrok', () => {
+  let component: SensorGrokComponent;
+  let grokValidationService: GrokValidationService;
+  let fixture: ComponentFixture<SensorGrokComponent>;
+  let sensorParserConfigService: MockSensorParserConfigService;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [SensorGrokModule],
+      providers: [
+        MetronAlerts,
+        {provide: Http},
+        {provide: KafkaService, useClass: MockKafkaService},
+        {provide: SensorParserConfigService, useClass: MockSensorParserConfigService},
+        {provide: GrokValidationService, useClass: MockGrokValidationService},
+
+      ]
+    }).compileComponents()
+        .then(() => {
+          fixture = TestBed.createComponent(SensorGrokComponent);
+          component = fixture.componentInstance;
+          sensorParserConfigService = fixture.debugElement.injector.get(SensorParserConfigService);
+          grokValidationService = fixture.debugElement.injector.get(GrokValidationService);
+        });
+  }));
+
+  it('should create an instance', () => {
+    expect(component).toBeDefined();
+    fixture.destroy();
+  });
+
+  it('should handle ngOnInit', async(() => {
+    component.ngOnInit();
+
+    expect(Object.keys(component.grokFunctionList).length).toEqual(6);
+
+    fixture.destroy();
+  }));
+
+  it('should handle ngOnChanges', async(() => {
+    spyOn(component.sampleData, 'getNextSample');
+
+    let changes = {
+      'showGrok': new SimpleChange(true, false)
+    };
+    component.ngOnChanges(changes);
+    expect(component.sampleData.getNextSample['calls'].count()).toEqual(0);
+
+    changes = {
+      'showGrok': new SimpleChange(false, true)
+    };
+
+    component.grokStatement = 'STATEMENT_1 grok statement 1\nSTATEMENT_2 grok statement 2\n';
+    component.patternLabel = 'STATEMENT_2';
+    component.ngOnChanges(changes);
+    expect(component.newGrokStatement).toEqual('STATEMENT_1 grok statement 1\nSTATEMENT_2 grok statement 2\n');
+    expect(component.newPatternLabel).toEqual('STATEMENT_2');
+    expect(component.availablePatternLabels).toEqual(['STATEMENT_1', 'STATEMENT_2']);
+
+    component.grokStatement = '';
+    component.patternLabel = 'PATTERN_LABEL';
+    component.ngOnChanges(changes);
+    expect(component.newGrokStatement).toEqual('PATTERN_LABEL ');
+    expect(component.newPatternLabel).toEqual('PATTERN_LABEL');
+    expect(component.availablePatternLabels).toEqual(['PATTERN_LABEL']);
+
+    expect(component.sampleData.getNextSample['calls'].count()).toEqual(2);
+
+    fixture.destroy();
+  }));
+
+  it('should test grok statement validation', async(() => {
+
+    let parsedMessage = {
+      'action': 'TCP_MISS',
+      'bytes': 337891,
+      'code': 200,
+      'elapsed': 415,
+      'ip_dst_addr': '207.109.73.154',
+      'ip_src_addr': '127.0.0.1',
+      'method': 'GET',
+      'timestamp': '1467011157.401',
+      'url': 'http://www.aliexpress.com/af/shoes.html?'
+    };
+    sensorParserConfigService.setParsedMessage(parsedMessage);
+
+    let sampleData = '1467011157.401 415 127.0.0.1 TCP_MISS/200 337891 GET http://www.aliexpress.com/af/shoes.html? ' +
+      '- DIRECT/207.109.73.154 text/html';
+    let grokStatement = 'SQUID_DELIMITED %{NUMBER:timestamp} %{INT:elapsed} %{IPV4:ip_src_addr} %{WORD:action}/%{NUMBER:code} ' +
+      '%{NUMBER:bytes} %{WORD:method} %{NOTSPACE:url} - %{WORD:UNWANTED}\/%{IPV4:ip_dst_addr} %{WORD:UNWANTED}\/%{WORD:UNWANTED}';
+
+    component.sensorParserConfig = new SensorParserConfig();
+    component.sensorParserConfig.sensorTopic = 'squid';
+    component.newGrokStatement = grokStatement;
+
+    component.onSampleDataChanged('');
+    expect(component.parsedMessage).toEqual({});
+    expect(component.parsedMessageKeys).toEqual([]);
+
+    component.onSampleDataChanged(sampleData);
+    expect(component.parsedMessage).toEqual(parsedMessage);
+    expect(component.parsedMessageKeys).toEqual(['action', 'bytes', 'code', 'elapsed', 'ip_dst_addr',
+      'ip_src_addr', 'method', 'timestamp', 'url']);
+
+    sensorParserConfigService.setParsedMessage('ERROR');
+    component.onTestGrokStatement();
+
+    expect(component.parsedMessage).toEqual({});
+
+    component.newGrokStatement = '';
+    component.onTestGrokStatement();
+    expect(component.parsedMessage).toEqual({});
+
+    fixture.destroy();
+  }));
+
+  it('should call appropriate functions on save ', () => {
+    spyOn(component.hideGrok, 'emit');
+    spyOn(component.onSaveGrokStatement, 'emit');
+    spyOn(component.onSavePatternLabel, 'emit');
+    component.newGrokStatement = 'grok statement';
+    component.newPatternLabel = 'PATTERN_LABEL';
+
+    component.onSaveGrok();
+
+    expect(component.onSaveGrokStatement.emit).toHaveBeenCalledWith('grok statement');
+    expect(component.onSavePatternLabel.emit).toHaveBeenCalledWith('PATTERN_LABEL');
+    expect(component.hideGrok.emit).toHaveBeenCalled();
+    fixture.destroy();
+  });
+
+  it('should call appropriate functions on cancel ', () => {
+    spyOn(component.hideGrok, 'emit');
+    spyOn(component.onSaveGrokStatement, 'emit');
+    spyOn(component.onSavePatternLabel, 'emit');
+
+    component.onCancelGrok();
+
+    expect(component.onSaveGrokStatement.emit).not.toHaveBeenCalled();
+    expect(component.onSavePatternLabel.emit).not.toHaveBeenCalled();
+    expect(component.hideGrok.emit).toHaveBeenCalled();
+    fixture.destroy();
+  });
+
+  it('should disable test', () => {
+    expect(component.isTestDisabled()).toEqual(true);
+    component.newGrokStatement = 'new grok statement';
+    expect(component.isTestDisabled()).toEqual(true);
+    component.parseMessageRequest.sampleData = 'sample data';
+    expect(component.isTestDisabled()).toEqual(false);
+  });
+
+  it('should disable save', () => {
+    component.availablePatternLabels = ['LABEL_1', 'LABEL_2'];
+    expect(component.isSaveDisabled()).toEqual(true);
+    component.newGrokStatement = 'new grok statement';
+    expect(component.isSaveDisabled()).toEqual(true);
+    component.newPatternLabel = 'LABEL_2';
+    expect(component.isSaveDisabled()).toEqual(false);
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.ts b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.ts
new file mode 100644
index 0000000..c8bf513
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.ts
@@ -0,0 +1,135 @@
+import { Component, OnInit, Input, OnChanges, SimpleChanges, ViewChild, EventEmitter, Output} from '@angular/core';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import {ParseMessageRequest} from '../../model/parse-message-request';
+import {SensorParserConfigService} from '../../service/sensor-parser-config.service';
+import {AutocompleteOption} from '../../model/autocomplete-option';
+import {GrokValidationService} from '../../service/grok-validation.service';
+import {SampleDataComponent} from '../../shared/sample-data/sample-data.component';
+import {MetronAlerts} from '../../shared/metron-alerts';
+
+@Component({
+  selector: 'metron-config-sensor-grok',
+  templateUrl: './sensor-grok.component.html',
+  styleUrls: ['./sensor-grok.component.scss']
+})
+export class SensorGrokComponent implements OnInit, OnChanges {
+
+  @Input() showGrok; boolean;
+  @Input() sensorParserConfig: SensorParserConfig;
+  @Input() grokStatement: string;
+  @Input() patternLabel: string;
+
+  @Output() hideGrok = new EventEmitter<void>();
+  @Output() onSaveGrokStatement = new EventEmitter<string>();
+  @Output() onSavePatternLabel = new EventEmitter<string>();
+
+  @ViewChild(SampleDataComponent) sampleData: SampleDataComponent;
+
+  newGrokStatement = '';
+  newPatternLabel = '';
+  availablePatternLabels = [];
+  parsedMessage: any = {};
+  parsedMessageKeys: string[] = [];
+  grokFunctionList: AutocompleteOption[] = [];
+  parseMessageRequest: ParseMessageRequest = new ParseMessageRequest();
+
+  constructor(private sensorParserConfigService: SensorParserConfigService, private grokValidationService: GrokValidationService,
+              private metronAlerts: MetronAlerts) {
+    this.parseMessageRequest.sampleData = '';
+  }
+
+  ngOnInit() {
+    this.getGrokFunctions();
+  }
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (changes['showGrok'] && changes['showGrok'].currentValue) {
+      this.newPatternLabel = this.patternLabel;
+      if (this.grokStatement) {
+        this.newGrokStatement = this.grokStatement;
+      } else {
+        this.newGrokStatement = this.newPatternLabel + ' ';
+      }
+      this.getAvailablePatternLabels();
+      this.sampleData.getNextSample();
+    }
+  }
+
+  onSampleDataChanged(sampleData: string) {
+    if (sampleData) {
+      this.parseMessageRequest.sampleData = sampleData;
+      this.onTestGrokStatement();
+    }
+  }
+
+  onTestGrokStatement() {
+    this.parsedMessage = {};
+
+    if (this.newGrokStatement.indexOf('%{') === -1) {
+      return;
+    }
+
+    this.parseMessageRequest.sensorParserConfig = JSON.parse(JSON.stringify(this.sensorParserConfig));
+    this.parseMessageRequest.grokStatement = this.newGrokStatement;
+    this.parseMessageRequest.sensorParserConfig.parserConfig['patternLabel'] = this.newPatternLabel;
+    this.parseMessageRequest.sensorParserConfig.parserConfig['grokPath'] = './' + this.parseMessageRequest.sensorParserConfig.sensorTopic;
+
+    this.sensorParserConfigService.parseMessage(this.parseMessageRequest).subscribe(
+        result => {
+          this.parsedMessage = result;
+          this.setParsedMessageKeys();
+        }, error => {
+          this.metronAlerts.showErrorMessage(error.message);
+          this.setParsedMessageKeys();
+        });
+  }
+
+  private getGrokFunctions() {
+    this.grokValidationService.list().subscribe(result => {
+      Object.keys(result).forEach(name => {
+        let autocompleteOption: AutocompleteOption = new AutocompleteOption();
+        autocompleteOption.name = name;
+        this.grokFunctionList.push(autocompleteOption);
+      });
+    });
+  }
+
+  private setParsedMessageKeys() {
+    try {
+      this.parsedMessageKeys = Object.keys(this.parsedMessage).sort();
+    } catch (e) {
+      this.parsedMessageKeys = [];
+    }
+  }
+
+  onSaveGrok(): void {
+    this.onSaveGrokStatement.emit(this.newGrokStatement);
+    this.onSavePatternLabel.emit(this.newPatternLabel);
+    this.hideGrok.emit();
+  }
+
+  onCancelGrok(): void {
+    this.hideGrok.emit();
+  }
+
+  private getAvailablePatternLabels() {
+    this.availablePatternLabels = [];
+    let statements = this.newGrokStatement.split('\n');
+    for (let statement of statements) {
+      if (statement) {
+        let patternLabel = statement.split(' ')[0];
+        this.availablePatternLabels.push(patternLabel);
+      }
+    }
+  }
+
+  isTestDisabled() {
+    return this.parseMessageRequest.sampleData.length === 0 || this.newGrokStatement.length === 0;
+  }
+
+  isSaveDisabled() {
+    return this.newGrokStatement.length === 0 || this.availablePatternLabels.indexOf(this.newPatternLabel) === -1;
+  }
+
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.module.ts b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.module.ts
new file mode 100644
index 0000000..ef31798
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.module.ts
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {NgModule} from '@angular/core';
+import {SharedModule} from '../../shared/shared.module';
+import {SensorGrokComponent} from './sensor-grok.component';
+import {AceEditorModule} from '../../shared/ace-editor/ace-editor.module';
+import {SampleDataModule} from '../../shared/sample-data/sample-data.module';
+
+@NgModule ({
+    imports: [ SharedModule, AceEditorModule, SampleDataModule ],
+    declarations: [ SensorGrokComponent ],
+    exports: [ SensorGrokComponent ]
+})
+
+export class SensorGrokModule { }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/index.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/index.ts
new file mode 100644
index 0000000..3a63c39
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/index.ts
@@ -0,0 +1 @@
+export * from './sensor-parser-config-readonly.component';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.html b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.html
new file mode 100644
index 0000000..d988dd1
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.html
@@ -0,0 +1,108 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<metron-config-metron-modal [backgroundMasked]="false">
+
+    <div class="metron-slider-pane fill load-right-to-left dialog1x">
+      <div class="metron-readonly-pane">
+      <div class="row">
+        <div class="col-xs-12 form-title">{{selectedSensorName}}
+          <i class="fa fa-times pull-right close-button" aria-hidden="true" (click)="goBack()"></i>
+        </div>
+      </div>
+
+      <div *ngFor="let item of editViewMetaData">
+        <div [ngSwitch]="item.type">
+
+          <div *ngSwitchCase="'SEPARATOR'" class="row form-seperator">
+          </div>
+
+            <div  *ngSwitchCase="'SPACER'" class="row">
+                <div class="col-xs-12">&nbsp;</div>
+            </div>
+
+          <div  *ngSwitchCase="'TITLE'" class="row">
+            <div class="col-xs-12 form-sub-title">{{ item.value }}</div>
+          </div>
+
+          <div *ngSwitchDefault class="row">
+            <div *ngIf="item.label!=''" class="col-xs-6 form-label" [ngClass]="{'form-value font-weight-bold': item.boldTitle}">{{ item.label }}</div>
+            <div *ngIf="item.model == 'sensorParserConfigHistory'" class="col-xs-6  px-0 pull-left form-value">{{ sensorParserConfigHistory[item.value] ? sensorParserConfigHistory[item.value] : "-" }}</div>
+            <div *ngIf="item.model == 'kafkaTopic'" class="col-xs-6  px-0 pull-left form-value">{{ kafkaTopic[item.value] ? kafkaTopic[item.value] : "-" }}</div>
+            <div *ngIf="item.model == 'topologyStatus'" class="col-xs-6  px-0  pull-left form-value">{{ getTopologyStatus(item.value) }}</div>
+
+            <div *ngIf="item.model == 'grokStatement' && sensorParserConfigHistory.config.parserClassName === 'org.apache.metron.parsers.GrokParser'" style="border: none">
+              <div class="col-xs-12 form-sub-title">Grok Statement</div>
+              <div id="collapseGrok" class="col-xs-12  pull-left form-value panel-collapse collapse"></div>
+              <div class="col-xs-12  pull-left form-value grok" [innerHtml]="grokStatement"></div>
+              <a *ngIf="grokStatement.length>0" class="collapsed blue-label font-weight-bold col-xs-8 col-xs-push-4" data-toggle="collapse" href="#collapseGrok" aria-expanded="false" aria-controls="collapseGrok"  #grokLink (click)="grokLink.text=(grokLink.text==='show more')?'show less':'show more'">show more</a>
+              <div class="px-1"> <div class="col-xs-12 form-seperator"></div> </div>
+            </div>
+
+            <div *ngIf="item.model == 'transforms'">
+
+              <div id="collapseTransform" class="col-xs-12  pull-left form-value panel-collapse collapse">
+                <div class="form-sub-sub-title">Transforms</div>
+                <div>
+                  <div *ngFor="let results of transformsConfigKeys" >
+                    <div class="form-label">{{ results }}</div>
+                    <div class="form-value">{{ transformsConfigMap[results] }}</div>
+                  </div>
+                </div>
+              </div>
+
+              <div class="transforms">
+                <div class="col-xs-12 form-sub-sub-title">Transforms</div>
+                <div class="col-xs-12 form-label " [innerHtml]="getTransformsOutput()"></div>
+              </div>
+              <a *ngIf="transformsConfigKeys.length>0"  class="collapsed blue-label font-weight-bold col-xs-8 col-xs-push-4" data-toggle="collapse" href="#collapseTransform" aria-expanded="false" aria-controls="collapseTransform" (click)="toggleTransformLink()">{{transformLinkText}}</a>
+            </div>
+            <div *ngIf="item.model == 'threatTriageRules'" class="threat-triage-rules">
+              <div class="col-xs-6 form-label">AGGREGATOR</div><div class="col-xs-6 form-value">{{sensorEnrichmentConfig.threatIntel.triageConfig.aggregator}}</div>
+              <div id="collapseThreatTriage" class="col-xs-12  pull-left form-value panel-collapse collapse">
+                <div>
+                  <div class="col-xs-6 form-sub-sub-title">NAME</div><div class="col-xs-6 form-sub-sub-title">SCORE</div>
+                  <div *ngFor="let riskLevelRule of rules" >
+                    <div class="col-xs-6 form-label">{{ getDisplayName(riskLevelRule) }}</div>
+                    <div class="col-xs-6 form-value">{{ riskLevelRule.score }}</div>
+                  </div>
+                </div>
+              </div>
+              <div  class="col-xs-12 form-label threatIntel">{{ getRuleDisplayName() }}</div>
+              <a *ngIf="rules.length>0" class="collapsed blue-label font-weight-bold col-xs-8 col-xs-push-4" data-toggle="collapse" href="#collapseThreatTriage" aria-expanded="false" aria-controls="collapseThreatTriage" (click)="toggleThreatTriageLink()">{{threatTriageLinkText}}</a>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+      <div class="metron-button-bar" >
+          <div class="row pl-0 py-1">
+            <button class="btn btn-primary" type="button" (click)="onEditSensor()"  [disabled]="startStopInProgress">EDIT</button>
+
+            <button class="btn btn-primary" type="button" (click)="onStartSensor()" [disabled]="startStopInProgress" [hidden]="isStartHidden()">START</button>
+            <button class="btn form-enable-disable-button" type="button" (click)="onStopSensor()" [disabled]="startStopInProgress" [hidden]="isStopHidden()">STOP</button>
+
+            <button class="btn btn-primary" type="button" (click)="onEnableSensor()" [disabled]="startStopInProgress" [hidden]="isEnableHidden()" >ENABLE</button>
+            <button class="btn form-enable-disable-button" type="button" (click)="onDisableSensor()" [disabled]="startStopInProgress" [hidden]="isDisableHidden()">DISABLE</button>
+
+            <button class="btn btn-link delete" type="button" (click)="onDeleteSensor()"> Delete </button>
+
+            <div class="start-stop-progress"> <i class="fa fa-circle-o-notch fa-spin fa-lg fa-fw" [hidden]="!startStopInProgress"></i> </div>
+          </div>
+      </div>
+    </div>
+
+</metron-config-metron-modal>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.scss b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.scss
new file mode 100644
index 0000000..b78b559
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.scss
@@ -0,0 +1,110 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../../_variables.scss";
+@import "../../../styles.scss";
+
+.metron-readonly-pane {
+  margin-bottom: $button-bar-height + 10px;
+}
+
+.metron-button-bar
+{
+  background: $gray-light;
+  border-top: solid 2px $gray-border;
+  border-left: solid 1px $gray-border;
+  position: fixed;
+  width: inherit;
+}
+
+.grok
+{
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  font-family: Roboto-Regular;
+  font-size: 13px;
+  color: $text-color-white
+}
+
+.in + .grok
+{
+  font-size: 13px;
+  overflow: auto;
+  display: inline-block;
+  white-space: normal;
+  font-family: Roboto-Regular;
+  color: $text-color-white;
+}
+
+.threatIntel
+{
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.in ~ .transforms, .in + .threatIntel
+{
+  display: none;
+}
+
+a
+{
+  width: 100%;
+  text-align: center;
+  font-family: Roboto-Regular;
+}
+
+.form-enable-disable-button
+{
+  min-width: 50px;
+}
+
+.delete
+{
+  color:#E45D55;
+  font-weight: bold;
+  padding: 0.1rem;
+}
+
+.form-sub-sub-title
+{
+  font-size: 14px;
+  font-family: Roboto-Regular;
+  color: $text-color-white;
+}
+
+.form-sub-title
+{
+  font-size: 16px;
+  font-family: Roboto-Regular;
+  color: $form-field-text-color;
+}
+
+.start-stop-progress
+{
+  position: absolute;
+  top: 20px;
+  right: 140px;
+}
+
+.threat-triage-rules {
+  .form-label {
+    word-wrap: initial;
+  }
+}


[12/12] incubator-metron git commit: METRON-623 Management UI [contributed by Raghu Mitra Kandikonda and Ryan Merriman] closes apache/incubator-metron#489

Posted by rm...@apache.org.
METRON-623 Management UI [contributed by Raghu Mitra Kandikonda and Ryan Merriman] closes apache/incubator-metron#489


Project: http://git-wip-us.apache.org/repos/asf/incubator-metron/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-metron/commit/1ef8cd8f
Tree: http://git-wip-us.apache.org/repos/asf/incubator-metron/tree/1ef8cd8f
Diff: http://git-wip-us.apache.org/repos/asf/incubator-metron/diff/1ef8cd8f

Branch: refs/heads/master
Commit: 1ef8cd8ff692b29d1042548a903092b0a0d88c4d
Parents: 0e629f3
Author: merrimanr <me...@gmail.com>
Authored: Tue Apr 11 08:40:43 2017 -0500
Committer: merrimanr <me...@gmail.com>
Committed: Tue Apr 11 08:40:43 2017 -0500

----------------------------------------------------------------------
 .travis.yml                                     |   10 +-
 .../docker/rpm-docker/SPECS/metron.spec         |   46 +
 .../packaging/docker/rpm-docker/pom.xml         |    6 +
 metron-interface/metron-config/.gitignore       |   42 +
 metron-interface/metron-config/LICENSE          |  220 ++++
 metron-interface/metron-config/NOTICE           |   11 +
 metron-interface/metron-config/README.md        |   77 ++
 metron-interface/metron-config/angular-cli.json |   51 +
 metron-interface/metron-config/assembly.xml     |   65 ++
 .../metron-config/e2e/app/app.e2e-spec.ts       |   50 +
 .../metron-config/e2e/app/app.po.ts             |   47 +
 .../metron-config/e2e/login/login.e2e-spec.ts   |   45 +
 .../metron-config/e2e/login/login.po.ts         |   60 ++
 .../sensor-config-readonly.e2e-spec.ts          |  113 ++
 .../sensor-config-readonly.po.ts                |  125 +++
 .../e2e/sensor-config/sensor-config.po.ts       |  243 +++++
 .../sensor-list-parser-actions.e2e-spec.ts      |  132 +++
 .../e2e/sensor-list/sensor-list.e2e-spec.ts     |  161 +++
 .../e2e/sensor-list/sensor-list.po.ts           |  240 +++++
 .../metron-config/e2e/tsconfig.json             |   16 +
 .../sensor-config-single-parser.e2e-spec.ts     |  159 +++
 .../metron-config/e2e/utils/e2e_util.ts         |   30 +
 metron-interface/metron-config/karma.conf.js    |   64 ++
 metron-interface/metron-config/package.json     |   65 ++
 metron-interface/metron-config/pom.xml          |  143 +++
 .../metron-config/protractor.conf.js            |   64 ++
 metron-interface/metron-config/proxy.conf.json  |   10 +
 .../metron-config/scripts/package.json          |   21 +
 .../scripts/prepend_license_header.sh           |   42 +
 .../metron-config/scripts/server.js             |   82 ++
 .../metron-config/scripts/start_dev.sh          |   19 +
 .../scripts/start_management_ui.sh              |   24 +
 .../metron-config/src/app/_main.scss            |  112 ++
 .../metron-config/src/app/_variables.scss       |   71 ++
 .../metron-config/src/app/app.component.html    |   25 +
 .../metron-config/src/app/app.component.scss    |   34 +
 .../metron-config/src/app/app.component.spec.ts |   89 ++
 .../metron-config/src/app/app.component.ts      |   39 +
 .../src/app/app.config.interface.ts             |    3 +
 .../metron-config/src/app/app.config.ts         |    8 +
 .../metron-config/src/app/app.module.ts         |   59 +
 .../metron-config/src/app/app.routes.ts         |   33 +
 .../metron-config/src/app/environment.ts        |   24 +
 .../general-settings.component.html             |   57 +
 .../general-settings.component.scss             |   41 +
 .../general-settings.component.spec.ts          |  161 +++
 .../general-settings.component.ts               |   72 ++
 .../general-settings/general-settings.module.ts |   30 +
 .../general-settings.routing.ts                 |   27 +
 .../src/app/general-settings/index.ts           |   18 +
 .../metron-config/src/app/global.scss           |  594 ++++++++++
 metron-interface/metron-config/src/app/index.ts |   22 +
 .../src/app/login/login.component.html          |   28 +
 .../src/app/login/login.component.scss          |   55 +
 .../src/app/login/login.component.spec.ts       |   65 ++
 .../src/app/login/login.component.ts            |   43 +
 .../metron-config/src/app/login/login.module.ts |   24 +
 .../src/app/login/login.routing.ts              |   25 +
 .../src/app/model/autocomplete-option.ts        |   30 +
 .../src/app/model/field-transformation.ts       |   20 +
 .../src/app/model/field-transformer.ts          |   24 +
 .../src/app/model/grok-validation.ts            |   23 +
 .../metron-config/src/app/model/kafka-topic.ts  |   23 +
 .../src/app/model/parse-message-request.ts      |   23 +
 .../metron-config/src/app/model/rest-error.ts   |   22 +
 .../src/app/model/risk-level-rule.ts            |   23 +
 .../src/app/model/sensor-enrichment-config.ts   |   44 +
 .../src/app/model/sensor-enrichments.ts         |   24 +
 .../src/app/model/sensor-indexing-config.ts     |   28 +
 .../app/model/sensor-parser-config-history.ts   |   29 +
 .../src/app/model/sensor-parser-config.ts       |   33 +
 .../src/app/model/sensor-parser-context.ts      |   22 +
 .../src/app/model/sensor-parser-info.ts         |   25 +
 .../src/app/model/sensor-parser-response.ts     |   21 +
 .../src/app/model/sensor-parser-status.ts       |   25 +
 .../app/model/stellar-function-description.ts   |   30 +
 .../src/app/model/threat-triage-config.ts       |   23 +
 .../src/app/model/topology-response.ts          |   21 +
 .../src/app/model/topology-status.ts            |   26 +
 .../metron-config/src/app/navbar/index.ts       |   18 +
 .../src/app/navbar/navbar.component.scss        |   28 +
 .../src/app/navbar/navbar.component.spec.ts     |   48 +
 .../src/app/navbar/navbar.component.ts          |   31 +
 .../metron-config/src/app/navbar/navbar.html    |   29 +
 .../metron-config/src/app/rxjs-operators.ts     |   32 +
 .../app/sensors/sensor-field-schema/index.ts    |   18 +
 .../sensor-field-schema.component.html          |  113 ++
 .../sensor-field-schema.component.scss          |  168 +++
 .../sensor-field-schema.component.spec.ts       |  523 +++++++++
 .../sensor-field-schema.component.ts            |  435 ++++++++
 .../sensor-field-schema.module.ts               |   30 +
 .../src/app/sensors/sensor-grok/index.ts        |   18 +
 .../sensor-grok/sensor-grok.component.html      |   46 +
 .../sensor-grok/sensor-grok.component.scss      |   67 ++
 .../sensor-grok/sensor-grok.component.spec.ts   |  234 ++++
 .../sensor-grok/sensor-grok.component.ts        |  135 +++
 .../sensors/sensor-grok/sensor-grok.module.ts   |   30 +
 .../sensor-parser-config-readonly/index.ts      |    1 +
 ...sensor-parser-config-readonly.component.html |  108 ++
 ...sensor-parser-config-readonly.component.scss |  110 ++
 ...sor-parser-config-readonly.component.spec.ts |  708 ++++++++++++
 .../sensor-parser-config-readonly.component.ts  |  372 +++++++
 .../sensor-parser-config-readonly.module.ts     |   27 +
 .../sensor-parser-config-readonly.routing.ts    |   25 +
 .../app/sensors/sensor-parser-config/index.ts   |    1 +
 .../sensor-parser-config.component.html         |  181 ++++
 .../sensor-parser-config.component.scss         |  120 +++
 .../sensor-parser-config.component.spec.ts      | 1014 ++++++++++++++++++
 .../sensor-parser-config.component.ts           |  421 ++++++++
 .../sensor-parser-config.module.ts              |   35 +
 .../sensor-parser-config.routing.ts             |   27 +
 .../src/app/sensors/sensor-parser-list/index.ts |   18 +
 .../sensor-parser-list.component.html           |   85 ++
 .../sensor-parser-list.component.scss           |   17 +
 .../sensor-parser-list.component.spec.ts        |  742 +++++++++++++
 .../sensor-parser-list.component.ts             |  368 +++++++
 .../sensor-parser-list.module.ts                |   30 +
 .../sensor-parser-list.routing.ts               |   25 +
 .../sensor-raw-json.component.html              |   49 +
 .../sensor-raw-json.component.scss              |   36 +
 .../sensor-raw-json.component.spec.ts           |  191 ++++
 .../sensor-raw-json.component.ts                |  103 ++
 .../sensor-raw-json/sensor-raw-json.module.ts   |   28 +
 .../sensor-rule-editor.component.html           |   49 +
 .../sensor-rule-editor.component.scss           |   90 ++
 .../sensor-rule-editor.component.spec.ts        |   74 ++
 .../rule-editor/sensor-rule-editor.component.ts |   49 +
 .../rule-editor/sensor-rule-editor.module.ts    |   28 +
 .../sensor-threat-triage.component.html         |   88 ++
 .../sensor-threat-triage.component.scss         |  137 +++
 .../sensor-threat-triage.component.spec.ts      |  211 ++++
 .../sensor-threat-triage.component.ts           |  208 ++++
 .../sensor-threat-triage.module.ts              |   29 +
 .../app/service/authentication.service.spec.ts  |  190 ++++
 .../src/app/service/authentication.service.ts   |   92 ++
 .../app/service/global-config.service.spec.ts   |   99 ++
 .../src/app/service/global-config.service.ts    |   75 ++
 .../app/service/grok-validation.service.spec.ts |  106 ++
 .../src/app/service/grok-validation.service.ts  |   56 +
 .../src/app/service/hdfs.service.spec.ts        |  110 ++
 .../src/app/service/hdfs.service.ts             |   63 ++
 .../src/app/service/kafka.service.spec.ts       |  114 ++
 .../src/app/service/kafka.service.ts            |   59 +
 .../sensor-enrichment-config.service.spec.ts    |  144 +++
 .../service/sensor-enrichment-config.service.ts |   71 ++
 .../sensor-indexing-config.service.spec.ts      |  118 ++
 .../service/sensor-indexing-config.service.ts   |   58 +
 ...sensor-parser-config-history.service.spec.ts |   97 ++
 .../sensor-parser-config-history.service.ts     |   61 ++
 .../sensor-parser-config.service.spec.ts        |  149 +++
 .../app/service/sensor-parser-config.service.ts |  116 ++
 .../src/app/service/stellar.service.spec.ts     |  142 +++
 .../src/app/service/stellar.service.ts          |   68 ++
 .../src/app/service/storm.service.spec.ts       |  252 +++++
 .../src/app/service/storm.service.ts            |  144 +++
 .../shared/ace-editor/ace-editor.component.html |   16 +
 .../shared/ace-editor/ace-editor.component.scss |   32 +
 .../ace-editor/ace-editor.component.spec.ts     |   26 +
 .../shared/ace-editor/ace-editor.component.ts   |  182 ++++
 .../app/shared/ace-editor/ace-editor.module.ts  |   12 +
 .../src/app/shared/ace-editor/index.ts          |   18 +
 .../advanced-config-form.component.html         |   39 +
 .../advanced-config-form.component.scss         |   61 ++
 .../advanced-config-form.component.spec.ts      |  151 +++
 .../advanced-config-form.component.ts           |  110 ++
 .../advanced-config-form.module.ts              |   14 +
 .../src/app/shared/auth-guard.spec.ts           |   92 ++
 .../metron-config/src/app/shared/auth-guard.ts  |   50 +
 .../metron-config/src/app/shared/index.ts       |   19 +
 .../src/app/shared/login-guard.spec.ts          |   61 ++
 .../metron-config/src/app/shared/login-guard.ts |   40 +
 .../src/app/shared/metron-alerts.spec.ts        |   75 ++
 .../src/app/shared/metron-alerts.ts             |   52 +
 .../src/app/shared/metron-dialog-box.spec.ts    |   63 ++
 .../src/app/shared/metron-dialog-box.ts         |   75 ++
 .../src/app/shared/metron-modal/index.ts        |    1 +
 .../metron-modal/metron-modal.component.html    |   20 +
 .../metron-modal/metron-modal.component.scss    |   45 +
 .../metron-modal/metron-modal.component.spec.ts |   43 +
 .../metron-modal/metron-modal.component.ts      |   27 +
 .../shared/metron-table/metron-sorter/index.ts  |    1 +
 .../metron-sorter/metron-sorter.component.html  |   21 +
 .../metron-sorter/metron-sorter.component.scss  |   17 +
 .../metron-sorter.component.spec.ts             |   75 ++
 .../metron-sorter/metron-sorter.component.ts    |   28 +
 .../metron-table/metron-table.directive.spec.ts |   52 +
 .../metron-table/metron-table.directive.ts      |   42 +
 .../shared/metron-table/metron-table.module.ts  |   14 +
 .../src/app/shared/multiple-input/index.ts      |    1 +
 .../multiple-input.component.html               |   39 +
 .../multiple-input.component.scss               |   21 +
 .../multiple-input.component.spec.ts            |  147 +++
 .../multiple-input/multiple-input.component.ts  |   83 ++
 .../multiple-input/multiple-input.module.ts     |   13 +
 .../src/app/shared/number-spinner/index.ts      |    1 +
 .../number-spinner.component.html               |   22 +
 .../number-spinner.component.scss               |   55 +
 .../number-spinner.component.spec.ts            |   46 +
 .../number-spinner/number-spinner.component.ts  |   70 ++
 .../number-spinner/number-spinner.module.ts     |   13 +
 .../sample-data/sample-data.component.html      |   22 +
 .../sample-data/sample-data.component.scss      |   66 ++
 .../sample-data/sample-data.component.spec.ts   |  188 ++++
 .../shared/sample-data/sample-data.component.ts |   84 ++
 .../shared/sample-data/sample-data.module.ts    |   13 +
 .../src/app/shared/shared.module.ts             |   29 +
 .../metron-config/src/app/util/enums.ts         |   21 +
 .../metron-config/src/app/util/httpUtil.ts      |   45 +
 .../src/app/util/httpUtils.spec.ts              |   47 +
 .../metron-config/src/app/util/stringUtils.ts   |   31 +
 .../src/app/verticalnavbar/index.ts             |   18 +
 .../verticalnavbar.component.scss               |   50 +
 .../verticalnavbar.component.spec.ts            |   53 +
 .../verticalnavbar/verticalnavbar.component.ts  |   35 +
 .../src/app/verticalnavbar/verticalnavbar.html  |   29 +
 .../metron-config/src/assets/.npmignore         |    0
 .../metron-config/src/assets/ace/LICENSE        |   24 +
 .../metron-config/src/assets/ace/mode-grok.js   |  106 ++
 .../src/assets/ace/snippets/grok.js             |   24 +
 .../src/assets/fonts/Roboto/LICENSE.txt         |  202 ++++
 .../src/assets/fonts/Roboto/Roboto-Black.ttf    |  Bin 0 -> 163488 bytes
 .../assets/fonts/Roboto/Roboto-BlackItalic.ttf  |  Bin 0 -> 165444 bytes
 .../src/assets/fonts/Roboto/Roboto-Bold.ttf     |  Bin 0 -> 162464 bytes
 .../assets/fonts/Roboto/Roboto-BoldItalic.ttf   |  Bin 0 -> 163644 bytes
 .../src/assets/fonts/Roboto/Roboto-Italic.ttf   |  Bin 0 -> 161484 bytes
 .../src/assets/fonts/Roboto/Roboto-Light.ttf    |  Bin 0 -> 162420 bytes
 .../assets/fonts/Roboto/Roboto-LightItalic.ttf  |  Bin 0 -> 166492 bytes
 .../src/assets/fonts/Roboto/Roboto-Medium.ttf   |  Bin 0 -> 162588 bytes
 .../assets/fonts/Roboto/Roboto-MediumItalic.ttf |  Bin 0 -> 165636 bytes
 .../src/assets/fonts/Roboto/Roboto-Regular.ttf  |  Bin 0 -> 162876 bytes
 .../src/assets/fonts/Roboto/Roboto-Thin.ttf     |  Bin 0 -> 163132 bytes
 .../assets/fonts/Roboto/Roboto-ThinItalic.ttf   |  Bin 0 -> 168276 bytes
 .../metron-config/src/assets/images/login.jpg   |  Bin 0 -> 335521 bytes
 .../metron-config/src/assets/images/logo.png    |  Bin 0 -> 21186 bytes
 .../src/environments/environment.prod.ts        |    3 +
 .../src/environments/environment.ts             |    8 +
 metron-interface/metron-config/src/favicon.ico  |  Bin 0 -> 1150 bytes
 metron-interface/metron-config/src/index.html   |   38 +
 metron-interface/metron-config/src/main.ts      |   29 +
 metron-interface/metron-config/src/polyfills.ts |   36 +
 metron-interface/metron-config/src/styles.scss  |  739 +++++++++++++
 metron-interface/metron-config/src/test.ts      |   64 ++
 .../metron-config/src/tsconfig.json             |   24 +
 metron-interface/metron-config/src/typings.d.ts |   23 +
 metron-interface/metron-config/tslint.json      |  112 ++
 .../metron/rest/model/TopologyStatus.java       |   14 +-
 metron-interface/metron-rest/README.md          |    8 +
 .../metron/rest/controller/GrokController.java  |    8 +
 .../metron/rest/controller/HdfsController.java  |    2 +-
 .../metron/rest/controller/KafkaController.java |    2 +-
 .../SensorEnrichmentConfigController.java       |   12 +-
 .../apache/metron/rest/service/GrokService.java |    2 +
 .../service/SensorEnrichmentConfigService.java  |    3 +
 .../rest/service/impl/GrokServiceImpl.java      |    9 +
 .../impl/SensorEnrichmentConfigServiceImpl.java |    7 +
 .../impl/SensorParserConfigServiceImpl.java     |    1 +
 .../GrokControllerIntegrationTest.java          |   14 +
 ...richmentConfigControllerIntegrationTest.java |   12 +-
 .../StormControllerIntegrationTest.java         |   12 +-
 .../rest/service/impl/GrokServiceImplTest.java  |   16 +-
 .../SensorEnrichmentConfigServiceImplTest.java  |   11 +
 .../metron-rest/src/test/resources/README.vm    |   76 +-
 metron-interface/pom.xml                        |    1 +
 pom.xml                                         |   15 +-
 site-book/bin/generate-md.sh                    |    1 +
 265 files changed, 19397 insertions(+), 50 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/.travis.yml
----------------------------------------------------------------------
diff --git a/.travis.yml b/.travis.yml
index 9e9f536..1632b6e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,3 +1,11 @@
+env:
+  - CXX=g++-4.8
+addons:
+  apt:
+    sources:
+    - ubuntu-toolchain-r-test
+    packages:
+    - g++-4.8
 install: true
 language: java
 jdk:
@@ -9,7 +17,7 @@ before_install:
   - export PATH=$M2_HOME/bin:$PATH
 script:
   - |
-    time mvn -q -T 2C -DskipTests install && time mvn -q -T 2C surefire:test@unit-tests && mvn -q surefire:test@integration-tests  && time build_utils/verify_licenses.sh
+    time mvn -q -T 2C -DskipTests install && time mvn -q -T 2C surefire:test@unit-tests && mvn -q surefire:test@integration-tests  && time mvn -q test --projects metron-interface/metron-config && time build_utils/verify_licenses.sh
 cache:
   directories:
   - $HOME/.m2

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec
----------------------------------------------------------------------
diff --git a/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec b/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec
index 2c619e1..593cf5c 100644
--- a/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec
+++ b/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec
@@ -50,6 +50,7 @@ Source5:        metron-enrichment-%{full_version}-archive.tar.gz
 Source6:        metron-indexing-%{full_version}-archive.tar.gz
 Source7:        metron-pcap-backend-%{full_version}-archive.tar.gz
 Source8:        metron-profiler-%{full_version}-archive.tar.gz
+Source10:       metron-config-%{full_version}-archive.tar.gz
 
 %description
 Apache Metron provides a scalable advanced security analytics framework
@@ -80,6 +81,7 @@ tar -xzf %{SOURCE5} -C %{buildroot}%{metron_home}
 tar -xzf %{SOURCE6} -C %{buildroot}%{metron_home}
 tar -xzf %{SOURCE7} -C %{buildroot}%{metron_home}
 tar -xzf %{SOURCE8} -C %{buildroot}%{metron_home}
+tar -xzf %{SOURCE10} -C %{buildroot}%{metron_home}
 
 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -313,6 +315,50 @@ This package installs the Metron Profiler %{metron_home}
 %{metron_home}/flux/profiler/remote.yaml
 %attr(0644,root,root) %{metron_home}/lib/metron-profiler-%{full_version}-uber.jar
 
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+%package        config
+Summary:        Metron Management UI
+Group:          Applications/Internet
+Provides:       config = %{version}
+
+%description    config
+This package installs the Metron Management UI %{metron_home}
+
+%files          config
+%defattr(-,root,root,755)
+%dir %{metron_root}
+%dir %{metron_home}
+%dir %{metron_home}/bin
+%dir %{metron_home}/web
+%dir %{metron_home}/web/expressjs
+%dir %{metron_home}/web/management-ui
+%dir %{metron_home}/web/management-ui/assets
+%dir %{metron_home}/web/management-ui/assets/ace
+%dir %{metron_home}/web/management-ui/assets/ace/snippets
+%dir %{metron_home}/web/management-ui/assets/fonts
+%dir %{metron_home}/web/management-ui/assets/fonts/Roboto
+%dir %{metron_home}/web/management-ui/assets/images
+%dir %{metron_home}/web/management-ui/license
+%{metron_home}/bin/start_management_ui.sh
+%attr(0755,root,root) %{metron_home}/web/expressjs/server.js
+%attr(0644,root,root) %{metron_home}/web/expressjs/package.json
+%attr(0644,root,root) %{metron_home}/web/management-ui/favicon.ico
+%attr(0644,root,root) %{metron_home}/web/management-ui/index.html
+%attr(0644,root,root) %{metron_home}/web/management-ui/*.js
+%attr(0644,root,root) %{metron_home}/web/management-ui/*.js.gz
+%attr(0644,root,root) %{metron_home}/web/management-ui/*.ttf
+%attr(0644,root,root) %{metron_home}/web/management-ui/*.svg
+%attr(0644,root,root) %{metron_home}/web/management-ui/*.eot
+%attr(0644,root,root) %{metron_home}/web/management-ui/*.woff
+%attr(0644,root,root) %{metron_home}/web/management-ui/*.woff2
+%attr(0644,root,root) %{metron_home}/web/management-ui/assets/ace/*.js
+%attr(0644,root,root) %{metron_home}/web/management-ui/assets/ace/LICENSE
+%attr(0644,root,root) %{metron_home}/web/management-ui/assets/ace/snippets/*.js
+%attr(0644,root,root) %{metron_home}/web/management-ui/assets/fonts/Roboto/LICENSE.txt
+%attr(0644,root,root) %{metron_home}/web/management-ui/assets/fonts/Roboto/*.ttf
+%attr(0644,root,root) %{metron_home}/web/management-ui/assets/images/*
+%attr(0644,root,root) %{metron_home}/web/management-ui/license/*
 
 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-deployment/packaging/docker/rpm-docker/pom.xml
----------------------------------------------------------------------
diff --git a/metron-deployment/packaging/docker/rpm-docker/pom.xml b/metron-deployment/packaging/docker/rpm-docker/pom.xml
index f96694e..d3e3052 100644
--- a/metron-deployment/packaging/docker/rpm-docker/pom.xml
+++ b/metron-deployment/packaging/docker/rpm-docker/pom.xml
@@ -199,6 +199,12 @@
                                         <include>*.tar.gz</include>
                                     </includes>
                                 </resource>
+                                <resource>
+                                    <directory>${metron_dir}/metron-interface/metron-config/target/</directory>
+                                    <includes>
+                                        <include>*.tar.gz</include>
+                                    </includes>
+                                </resource>
                             </resources>
                         </configuration>
                     </execution>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/.gitignore
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/.gitignore b/metron-interface/metron-config/.gitignore
new file mode 100644
index 0000000..4be186c
--- /dev/null
+++ b/metron-interface/metron-config/.gitignore
@@ -0,0 +1,42 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# compiled output
+/dist
+/tmp
+
+# dependencies
+/node
+/node_modules
+/bower_components
+
+# IDEs and editors
+/.idea
+.project
+.classpath
+*.launch
+.settings/
+
+# misc
+/.sass-cache
+/connect.lock
+/coverage
+/libpeerconnection.log
+npm-debug.log
+testem.log
+/typings
+
+# e2e
+/e2e/*.js
+/e2e/*.map
+
+#assets
+/src/assets/ace/snippets/json.js
+/src/assets/ace/snippets/text.js
+/src/assets/ace/ext-language_tools.js
+/src/assets/ace/mode-json.js
+/src/assets/ace/theme-monokai.js
+/src/assets/ace/worker-json.js
+
+#System Files
+.DS_Store
+Thumbs.db

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/LICENSE
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/LICENSE b/metron-interface/metron-config/LICENSE
new file mode 100644
index 0000000..9243f26
--- /dev/null
+++ b/metron-interface/metron-config/LICENSE
@@ -0,0 +1,220 @@
+Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+------------------------------------------------------------------------------------
+
+------------------------------------------------------------------------------------
+  MIT
+------------------------------------------------------------------------------------
+
+This product bundles Twitter Bootstrap 4, which is available under a "MIT Software License" license.  For details, see https://github.com/twbs/bootstrap
+This product bundles core-js, which is available under a "MIT Software License" license.  For details, see https://github.com/zloirock/core-js
+This product bundles Jquery 2.2.4, which is available under a "MIT Software License" license.  For details, see https://jquery.com
+This product bundles tether 1.3.4, which is available under a "MIT Software License" license.  For details, see https://github.com/HubSpot/tether
+This product bundles ts-helpers 1.1.1, which is available under a "MIT Software License" license.  For details, see https://github.com/ngParty/ts-helpers
+This product bundles zone.js 0.6.23, which is available under a "MIT Software License" license.  For details, see htps://github.com/angular/zone.js
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/NOTICE
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/NOTICE b/metron-interface/metron-config/NOTICE
new file mode 100644
index 0000000..fd52449
--- /dev/null
+++ b/metron-interface/metron-config/NOTICE
@@ -0,0 +1,11 @@
+metron-config
+Copyright 2006-2016 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+This product includes Font Awesome 4.6.3 (http://fortawesome.github.com/Font-Awesome - SIL OFL 1.1)
+Copyright (c) 2013 Dave Gandy
+
+This product includes Ace (Ajax.org Cloud9 Editor) (http://github.com/ajaxorg/ace - http://github.com/ajaxorg/ace/blob/master/LICENSE)
+Copyright (c) 2010, Ajax.org B.V.
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/README.md
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/README.md b/metron-interface/metron-config/README.md
new file mode 100644
index 0000000..92878b5
--- /dev/null
+++ b/metron-interface/metron-config/README.md
@@ -0,0 +1,77 @@
+# Metron Management UI
+
+This module provides a user interface for management functions in Metron.
+
+## Prerequisites
+
+* A network accessible Metron REST application
+* nodejs v6.9+ (nodejs can be installed on quick dev with `curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - && yum install -y nodejs`)
+
+## Installation
+1. Build Metron:
+    ```
+    mvn clean package -DskipTests
+    ```
+  
+1. Copy `incubator-metron/metron-interface/metron-config/target/metron-config-METRON_VERSION-archive.tar.gz` to the desired host.
+
+1. Untar the archive in the target directory.  The directory structure will look like:
+    ```
+    bin
+      start_management_ui.sh
+    web
+      expressjs
+        package.json
+        server.js
+      management-ui
+        web assets (html, css, js, ...)
+    ```
+
+1. For production use, the contents of the `./web/management-ui` directory should be deployed to a web server with paths `/api/v1` and `/logout` mapped to the REST application url.  
+
+1. As an example, a convenience script is included that will install a simple [expressjs](https://github.com/expressjs/express) webserver.
+
+1. Then start the application with the script:
+    ```
+    ./bin/start_management_ui.sh
+    Usage: server.js -p [port] -r [restUrl]
+    Options:
+      -p             Port to run metron management ui                [required]
+      -r, --resturl  Url where metron rest application is available  [required]
+    ```
+
+## Usage
+
+The application will be available at http://host:4200 with credentials `user/password`, assuming the default port is configured and the `dev` profile is included when starting the REST application.  See the [REST application](../metron-rest#security) documentation for more information about security configuration for production.
+
+## Development
+
+The Management UI can also be started in development mode.  This allows changes to web assets to be seen interactively.
+
+1. Install the application with dev dependencies:
+    ```
+    npm install
+    ```
+  
+1. Start the application:
+    ```
+    ./scripts/start_dev.sh
+    ```
+
+The application will be available at http://localhost:4200/.  The REST application url defaults to `http://localhost:8080` but can be changed in the `proxy.conf.json` file.
+
+## Testing
+
+1. Install the application with dev dependencies:
+    ```
+    npm install
+    ```
+
+1. Unit tests can be run with:
+    ```
+    npm test
+    ```
+
+## License
+
+This projects bundles Font Awesome which is available under the SIL Open Font License.  See http://fontawesome.io/license/ for more details.
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/angular-cli.json
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/angular-cli.json b/metron-interface/metron-config/angular-cli.json
new file mode 100644
index 0000000..7e99b78
--- /dev/null
+++ b/metron-interface/metron-config/angular-cli.json
@@ -0,0 +1,51 @@
+{
+  "project": {
+    "version": "1.0.0-beta.15",
+    "name": "metron-config"
+  },
+  "apps": [
+    {
+      "root": "src",
+      "outDir": "dist",
+      "assets": "assets",
+      "index": "index.html",
+      "main": "main.ts",
+      "test": "test.ts",
+      "tsconfig": "tsconfig.json",
+      "prefix": "metron-config",
+      "mobile": false,
+      "styles": [
+        "../node_modules/bootstrap/dist/css/bootstrap.css",
+        "../node_modules/font-awesome/css/font-awesome.css",
+        "styles.scss"
+      ],
+      "scripts": [
+        "../node_modules/jquery/dist/jquery.js",
+        "../node_modules/tether/dist/js/tether.js",
+        "../node_modules/bootstrap/dist/js/bootstrap.js",
+        "../node_modules/ace-builds/src-noconflict/ace.js"
+      ],
+      "environments": {
+        "source": "environments/environment.ts",
+        "dev": "environments/environment.ts",
+        "prod": "environments/environment.prod.ts"
+      }
+    }
+  ],
+  "addons": [],
+  "packages": [],
+  "e2e": {
+    "protractor": {
+      "config": "./protractor.conf.js"
+    }
+  },
+  "test": {
+    "karma": {
+      "config": "./karma.conf.js"
+    }
+  },
+  "defaults": {
+    "styleExt": "scss",
+    "prefixInterfaces": false
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/assembly.xml
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/assembly.xml b/metron-interface/metron-config/assembly.xml
new file mode 100644
index 0000000..86eedd4
--- /dev/null
+++ b/metron-interface/metron-config/assembly.xml
@@ -0,0 +1,65 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+	Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+
+<assembly>
+  <id>archive</id>
+  <formats>
+    <format>tar.gz</format>
+  </formats>
+  <includeBaseDirectory>false</includeBaseDirectory>
+  <fileSets>
+    <fileSet>
+      <directory>${project.basedir}/dist</directory>
+      <outputDirectory>/web/management-ui</outputDirectory>
+      <excludes>
+        <exclude>**/.npmignore</exclude>
+      </excludes>
+      <fileMode>0644</fileMode>
+    </fileSet>
+    <fileSet>
+      <directory>${project.basedir}/scripts</directory>
+      <outputDirectory>/web/expressjs</outputDirectory>
+      <includes>
+        <include>package.json</include>
+        <include>server.js</include>
+      </includes>
+      <fileMode>0644</fileMode>
+    </fileSet>
+    <fileSet>
+      <directory>${project.basedir}/scripts</directory>
+      <outputDirectory>/bin</outputDirectory>
+      <useDefaultExcludes>true</useDefaultExcludes>
+      <includes>
+        <include>start_management_ui.sh</include>
+      </includes>
+      <fileMode>0755</fileMode>
+      <lineEnding>unix</lineEnding>
+      <filtered>true</filtered>
+    </fileSet>
+    <fileSet>
+      <directory>${project.basedir}</directory>
+      <outputDirectory>/web/management-ui/license</outputDirectory>
+      <includes>
+        <include>LICENSE</include>
+        <include>NOTICE</include>
+      </includes>
+      <fileMode>0644</fileMode>
+    </fileSet>
+  </fileSets>
+  <files>
+    <file>
+      <source>src/favicon.ico</source>
+      <outputDirectory>/web/management-ui</outputDirectory>
+    </file>
+  </files>
+</assembly>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/e2e/app/app.e2e-spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/e2e/app/app.e2e-spec.ts b/metron-interface/metron-config/e2e/app/app.e2e-spec.ts
new file mode 100644
index 0000000..a10935c
--- /dev/null
+++ b/metron-interface/metron-config/e2e/app/app.e2e-spec.ts
@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { AppPage } from './app.po';
+import {LoginPage} from '../login/login.po';
+
+describe('Application Skeleton', function() {
+  let page = new AppPage();
+  let loginPage = new LoginPage();
+
+  beforeAll(() => {
+    loginPage.login();
+  });
+
+  afterAll(() => {
+    loginPage.logout();
+  });
+
+  it('should have metron logo', () => {
+    expect(page.isMetronLogoPresent()).toEqual(true);
+  });
+
+  it('should have navigations', () => {
+    expect(page.getNavigationTitle()).toEqual('Operations');
+    expect(page.getNavigationLinks()).toEqual([ 'Sensors', 'General Settings' ]);
+  });
+
+  it('should navigate to all pages', () => {
+    expect(page.selectNavLink('General Settings')).toEqual(true);
+    expect(page.getUrl()).toEqual('http://localhost:4200/general-settings');
+
+    expect(page.selectNavLink('Sensors')).toEqual(true);
+    expect(page.getUrl()).toEqual('http://localhost:4200/sensors');
+  });
+  
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/e2e/app/app.po.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/e2e/app/app.po.ts b/metron-interface/metron-config/e2e/app/app.po.ts
new file mode 100644
index 0000000..39f2c7a
--- /dev/null
+++ b/metron-interface/metron-config/e2e/app/app.po.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { browser, element, by } from 'protractor/globals';
+
+export class AppPage {
+
+  getNavigationLinks() {
+    return element.all(by.css('.navigation .nav-item')).getText();
+  }
+
+  getNavigationTitle() {
+    return element(by.css('.navigation .nav-link-title')).getText();
+  }
+
+  getUrl() {
+    return browser.getCurrentUrl();
+  }
+
+  isMetronLogoPresent() {
+    return element(by.css('.navbar-brand')).isPresent();
+  }
+
+  navigateTo() {
+    return browser.get('/');
+  }
+
+  selectNavLink(linkName: string) {
+    return element(by.cssContainingText('.navigation .nav-link', linkName)).click().then((args) => {
+      return true;
+    });
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/e2e/login/login.e2e-spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/e2e/login/login.e2e-spec.ts b/metron-interface/metron-config/e2e/login/login.e2e-spec.ts
new file mode 100644
index 0000000..ac1f8cb
--- /dev/null
+++ b/metron-interface/metron-config/e2e/login/login.e2e-spec.ts
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { LoginPage } from './login.po';
+
+describe('login to application', function() {
+    let page: LoginPage;
+
+    beforeEach(() => {
+        page = new LoginPage();
+    });
+
+    it('should display error message for invalid credentials', () => {
+        page.navigateToLogin();
+        page.setUserNameAndPassword('admin', 'admin');
+        page.submitLoginForm();
+        expect(page.getErrorMessage()).toEqual('Login failed for admin');
+    });
+
+    it('should login for valid credentials', () => {
+        page.navigateToLogin();
+        page.setUserNameAndPassword('admin', 'password');
+        page.submitLoginForm();
+        expect(page.getLocation()).toEqual('http://localhost:4200/sensors');
+    });
+
+    it('should logout', () => {
+        page.logout();
+        expect(page.getLocation()).toEqual('http://localhost:4200/login');
+    });
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/e2e/login/login.po.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/e2e/login/login.po.ts b/metron-interface/metron-config/e2e/login/login.po.ts
new file mode 100644
index 0000000..9907583
--- /dev/null
+++ b/metron-interface/metron-config/e2e/login/login.po.ts
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { browser, element, by } from 'protractor/globals';
+
+export class LoginPage {
+    navigateToLogin() {
+        return browser.get('/');
+    }
+
+    login() {
+        browser.wait(function() {return element(by.css('input.form-control')).isPresent();});
+        this.setUserNameAndPassword('admin', 'password');
+        this.submitLoginForm();
+        browser.wait(function() {return element(by.css('.logout')).isPresent();});
+    }
+
+    logout() {
+        browser.ignoreSynchronization = true;
+        element.all(by.css('.alert .close')).click();
+        element.all(by.css('.logout-link')).click();
+        browser.sleep(2000);
+    }
+
+    setUserNameAndPassword(userName: string, password: string) {
+        element.all(by.css('input.form-control')).get(0).sendKeys(userName);
+        element.all(by.css('input.form-control')).get(1).sendKeys(password);
+    }
+
+    submitLoginForm() {
+        return element.all(by.buttonText('LOG IN')).click();
+    }
+
+    getErrorMessage() {
+        let errorMessage = element(by.css('div[style="color:#a94442"]')).getText();
+        return errorMessage.then(message => {
+            return message.replace(/\n/,'').replace(/LOG\ IN$/,'');
+        })
+    }
+
+    getLocation() {
+        return browser.getCurrentUrl().then(url => {
+            return url;
+        });
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/e2e/sensor-config-readonly/sensor-config-readonly.e2e-spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/e2e/sensor-config-readonly/sensor-config-readonly.e2e-spec.ts b/metron-interface/metron-config/e2e/sensor-config-readonly/sensor-config-readonly.e2e-spec.ts
new file mode 100644
index 0000000..85e10e4
--- /dev/null
+++ b/metron-interface/metron-config/e2e/sensor-config-readonly/sensor-config-readonly.e2e-spec.ts
@@ -0,0 +1,113 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {LoginPage} from '../login/login.po';
+import {SensorDetailsPage} from './sensor-config-readonly.po';
+
+describe('Sensor Details View', function () {
+    let page = new SensorDetailsPage();
+    let loginPage = new LoginPage();
+
+    beforeAll(() => {
+        loginPage.login();
+    });
+
+    afterAll(() => {
+        loginPage.logout();
+    });
+
+    it('should have squid attributes defined', (done) => {
+        let parserNotRunnigExpected = ['',
+            'PARSERS\nGrok',
+            'LAST UPDATED\n-',
+            'LAST EDITOR\n-',
+            'STATE\n-',
+            'ORIGINATOR\n-',
+            'CREATION DATE\n-',
+            ' ',
+            'STORM\nStopped',
+            'LATENCY\n-',
+            'THROUGHPUT\n-',
+            'EMITTED(10 MIN)\n-',
+            'ACKED(10 MIN)\n-',
+            ' ',
+            'KAFKA\nNo Kafka Topic',
+            'PARTITONS\n-',
+            'REPLICATION FACTOR\n-',
+            ''];
+        let parserRunningExpected = ['',
+            'PARSERS\nGrok',
+            'LAST UPDATED\n-',
+            'LAST EDITOR\n-',
+            'STATE\nEnabled',
+            'ORIGINATOR\n-',
+            'CREATION DATE\n-',
+            ' ',
+            'STORM\nRunning',
+            'LATENCY\n-',
+            'THROUGHPUT\n-',
+            'EMITTED(10 MIN)\n-',
+            'ACKED(10 MIN)\n-',
+            ' ',
+            'KAFKA\nNo Kafka Topic',
+            'PARTITONS\n-',
+            'REPLICATION FACTOR\n-',
+            ''];
+        let parserDisabledExpected = ['',
+            'PARSERS\nGrok',
+            'LAST UPDATED\n-',
+            'LAST EDITOR\n-',
+            'STATE\nDisabled',
+            'ORIGINATOR\n-',
+            'CREATION DATE\n-',
+            ' ',
+            'STORM\nDisabled',
+            'LATENCY\n-',
+            'THROUGHPUT\n-',
+            'EMITTED(10 MIN)\n-',
+            'ACKED(10 MIN)\n-',
+            ' ',
+            'KAFKA\nNo Kafka Topic',
+            'PARTITONS\n-',
+            'REPLICATION FACTOR\n-',
+            ''];
+        expect(page.navigateTo('squid')).toEqual('http://localhost:4200/sensors(dialog:sensors-readonly/squid)');
+        expect(page.getTitle()).toEqual("squid");
+        expect(page.getParserConfig()).toEqual(parserNotRunnigExpected);
+        expect(page.getButtons()).toEqual([ 'EDIT', 'START', 'Delete' ]);
+
+        expect(page.startParser()).toEqual(true);
+        expect(page.getButtons()).toEqual([ 'EDIT', 'STOP', 'DISABLE',  'Delete' ]);
+        expect(page.getParserConfig()).toEqual(parserRunningExpected);
+
+        expect(page.disableParser()).toEqual(true);
+        expect(page.getButtons()).toEqual([ 'EDIT', 'STOP', 'ENABLE',  'Delete' ]);
+        expect(page.getParserConfig()).toEqual(parserDisabledExpected);
+
+        expect(page.enableParser()).toEqual(true);
+        expect(page.getButtons()).toEqual([ 'EDIT', 'STOP', 'DISABLE',  'Delete' ]);
+        expect(page.getParserConfig()).toEqual(parserRunningExpected);
+
+        expect(page.stopParser()).toEqual(true);
+        expect(page.getButtons()).toEqual([ 'EDIT', 'START', 'Delete' ]);
+
+        page.closePane('squid');
+
+        done();
+
+    }, 300000);
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/e2e/sensor-config-readonly/sensor-config-readonly.po.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/e2e/sensor-config-readonly/sensor-config-readonly.po.ts b/metron-interface/metron-config/e2e/sensor-config-readonly/sensor-config-readonly.po.ts
new file mode 100644
index 0000000..41e5279
--- /dev/null
+++ b/metron-interface/metron-config/e2e/sensor-config-readonly/sensor-config-readonly.po.ts
@@ -0,0 +1,125 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { browser, element, by, protractor } from 'protractor/globals';
+import {changeURL, waitForElementVisibility} from '../utils/e2e_util';
+
+export class SensorDetailsPage {
+
+    private enableButton;
+    private disableButton;
+    private startButton;
+    private stopButton;
+
+    constructor() {
+        this.enableButton = element(by.cssContainingText('metron-config-sensor-parser-readonly .btn', 'ENABLE'));
+        this.disableButton = element(by.cssContainingText('metron-config-sensor-parser-readonly .btn', 'DISABLE'));
+        this.startButton = element(by.cssContainingText('metron-config-sensor-parser-readonly .btn', 'START'));
+        this.stopButton = element(by.cssContainingText('metron-config-sensor-parser-readonly .btn', 'STOP'));
+    }
+
+    clickToggleShowMoreLess(text: string, index: number) {
+        return element.all(by.linkText(text)).get(index).click().then(() => {
+            browser.sleep(1000);
+            return true;
+        })
+    }
+
+    closePane(name: string) {
+        return element(by.css('metron-config-sensor-parser-readonly .fa-times')).click().then(() => {
+            return true;
+        });
+    }
+
+    disableParser() {
+        return waitForElementVisibility(this.disableButton).then(() => {
+            return this.disableButton.click().then(() => {
+                return waitForElementVisibility(this.enableButton).then(() => {
+                    return true;
+                })
+            });
+        });
+    }
+
+    enableParser() {
+        return waitForElementVisibility(this.enableButton).then(() => {
+            return this.enableButton.click().then(() => {
+                return waitForElementVisibility(this.disableButton).then(() => {
+                    return true;
+                })
+            });
+        });
+    }
+
+    startParser() {
+        return waitForElementVisibility(this.startButton).then(() => {
+            return this.startButton.click().then(() => {
+                return waitForElementVisibility(this.stopButton).then(() => {
+                    return true;
+                })
+            });
+        });
+    }
+
+    stopParser() {
+        return waitForElementVisibility(this.stopButton).then(() => {
+            return this.stopButton.click().then(() => {
+                return waitForElementVisibility(this.startButton).then(() => {
+                    return true;
+                })
+            });
+        });
+    }
+
+    getButtons() {
+        return element.all(by.css('metron-config-sensor-parser-readonly button:not([hidden=""])')).getText();
+    }
+
+    getGrokStatement() {
+        return element(by.css('.form-value.grok')).getText();
+    }
+
+    getParserConfig() {
+        return element.all(by.css('metron-config-sensor-parser-readonly .row')).getText().then(data => {
+            return data.slice(1, 19);
+        });
+    }
+
+    getSchemaSummary() {
+        return element.all(by.css('.transforms')).getText();
+    }
+
+    getSchemaFullSummary() {
+        return element.all(by.css('.collapse.in')).getText();
+    }
+
+    getThreatTriageSummary() {
+        return element.all(by.css('.threat-triage-rules')).getText();
+    }
+
+    getTitle() {
+        let title = element(by.css('metron-config-sensor-parser-readonly .form-title'));
+        return waitForElementVisibility(title).then(() => {
+            return title.getText();
+        });
+    }
+
+    navigateTo(parserName: string) {
+        let url = browser.baseUrl + '/sensors(dialog:sensors-readonly/'+ parserName + ')';
+        return changeURL(url);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/e2e/sensor-config/sensor-config.po.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/e2e/sensor-config/sensor-config.po.ts b/metron-interface/metron-config/e2e/sensor-config/sensor-config.po.ts
new file mode 100644
index 0000000..ce0f824
--- /dev/null
+++ b/metron-interface/metron-config/e2e/sensor-config/sensor-config.po.ts
@@ -0,0 +1,243 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { browser, element, by, protractor } from 'protractor/globals';
+import {changeURL, waitForElementVisibility, waitForElementInVisibility, waitForElementPresence} from '../utils/e2e_util';
+
+export class SensorConfigPage {
+
+  private getEnrichmentsMultipleInput() {
+    return element.all(by.css('.config.container')).all(by.css('metron-config-multiple-input')).get(1).all(by.css('select')).last();
+  }
+
+  private getThreatIntelMultipleInput() {
+    return element.all(by.css('.config.container')).all(by.css('metron-config-multiple-input')).last().all(by.css('select')).last();
+  }
+
+  private getTransformationMultipleInput() {
+    return element.all(by.css('.config.container')).all(by.css('metron-config-multiple-input')).first().all(by.css('select')).last();
+  }
+
+  clickAddButton() {
+    changeURL(browser.baseUrl + '/sensors');
+    let addButton = element(by.css('.metron-add-button.hexa-button'));
+    return waitForElementPresence(addButton).then(() => {
+      addButton.click();
+    });
+  }
+
+  clickAddThreatTriageRule() {
+    let addThreatTriageButton = element(by.css('metron-config-sensor-threat-triage .add-button'));
+    return waitForElementVisibility(addThreatTriageButton).then(() => {
+      return addThreatTriageButton.click();
+    });
+  }
+
+  clickGrokStatement() {
+    let grokInput = element(by.css('input[formcontrolname="grokStatement"] + span'));
+    return waitForElementVisibility(grokInput).then(() => {
+      return grokInput.click();
+    });
+  }
+
+  clickSchema() {
+    let schemaInput = element(by.css('div[name="fieldSchema"] button'));
+    return waitForElementVisibility(schemaInput).then(() => {
+      return schemaInput.click();
+    });
+  }
+
+  clickThreatTriage() {
+    let threatTriageInput = element(by.css('div[name="threatTriage"] button'));
+    return waitForElementVisibility(threatTriageInput).then(() => {
+      return threatTriageInput.click();
+    });
+  }
+
+  closeMainPane() {
+    return element(by.css('.btn.save-button + button')).click();
+  }
+
+  closeSchemaPane() {
+    return element.all(by.css('metron-config-sensor-field-schema .form-title + i')).click();
+  }
+
+  closeThreatTriagePane() {
+    return element.all(by.css('metron-config-sensor-threat-triage .form-title + i')).click();
+  }
+
+  getGrokStatementFromMainPane() {
+    browser.waitForAngular;
+    return element.all(by.css('input[formcontrolname="grokStatement"]')). getAttribute('value');
+  }
+
+  getFieldSchemaSummary() {
+    return element.all(by.css('[name="fieldSchema"] table tr')).getText();
+  }
+
+  getFieldSchemaValues() {
+    return waitForElementPresence(element(by.css('metron-config-sensor-field-schema .field-schema-row'))).then(() => {
+      return element.all(by.css('metron-config-sensor-field-schema .field-schema-row')).getText();
+    });
+  }
+
+  getGrokResponse() {
+    let tableRows = element(by.css('metron-config-sensor-grok table tr'));
+    return waitForElementPresence(tableRows).then(() => {
+      return element.all(by.css('metron-config-sensor-grok table tr')).getText();
+    });
+  }
+
+  getFieldSchemaEditButton(name: string) {
+    let fieldRowClassName = 'metron-config-sensor-field-schema .field-schema-row';
+    return element(by.cssContainingText(fieldRowClassName, name)).element(by.xpath("..")).element(by.css('.fa-pencil'));
+  }
+
+  getFormData() {
+    let mainPanel = element.all(by.css('.metron-slider-pane-edit')).last();
+    return protractor.promise.all([
+      mainPanel.element(by.css('.form-title')).getText(),
+      mainPanel.element(by.css('input[name="sensorTopic"]')).getAttribute('value'),
+      mainPanel.element(by.css('select[formcontrolname="parserClassName"]')).getAttribute('value'),
+      mainPanel.element(by.css('input[formcontrolname="grokStatement"]')).getAttribute('value'),
+      mainPanel.all(by.css('div[name="fieldSchema"] table tr')).getText(),
+      mainPanel.all(by.css('div[name="threatTriage"] table tr')).getText(),
+      mainPanel.element(by.css('input[formcontrolname="index"]')).getAttribute('value'),
+      mainPanel.element(by.css('metron-config-number-spinner[name="batchSize"] input')).getAttribute('value'),
+      mainPanel.all(by.css('metron-config-advanced-form input')).getAttribute('value')
+    ]).then(args => {
+      return  {
+        title: args[0],
+        parserName: args[1],
+        parserType: args[2],
+        grokStatement: args[3],
+        fieldSchemaSummary: args[4],
+        threatTriageSummary: args[5],
+        indexName: args[6],
+        batchSize: args[7],
+        advancedConfig: args[8]
+      }
+    });
+  }
+
+  getKafkaStatusText() {
+    return element(by.css('input[name="sensorTopic"] + label')).getText();
+  }
+
+  getThreatTrigaeRule() {
+    return element.all(by.css('.threat-triage-rule-row.threat-triage-rule-str')).getText();
+  }
+
+  getThreatTriageSummary() {
+    return element.all(by.css('div[name="threatTriage"] table tr')).getText();
+  }
+
+  getTransformText() {
+    return element.all(by.css('.config.container')).all(by.css('.edit-pane-readonly')).getText();
+  }
+
+  navigateTo(parserName: string) {
+    let url = browser.baseUrl + '/sensors(dialog:sensors-readonly/'+ parserName + ')';
+    return changeURL(url);
+  }
+
+  setAdvancedConfig(key: string, value: string) {
+    return element.all(by.css('.advanced-link')).click().then(() => {
+      element(by.css('input[formcontrolname="newConfigKey"]')).sendKeys(key);
+      element(by.css('input[ng-reflect-name="newConfigValue"]')).sendKeys(value);
+    });
+  }
+
+  saveFieldSchemaConfig() {
+    return element.all(by.css('.config.container')).all(by.cssContainingText('.btn', 'SAVE')).click();
+  }
+
+  saveGrokStatement() {
+    let saveButton = element(by.cssContainingText('metron-config-sensor-grok button', 'SAVE'));
+    return waitForElementVisibility(saveButton).then(() => {
+      return element(by.cssContainingText('metron-config-sensor-grok button', 'SAVE')).click();
+    })
+  }
+
+  saveThreatTriageRule() {
+    return element(by.cssContainingText('metron-config-sensor-rule-editor .btn', 'SAVE')).click();
+  }
+
+  setGrokStatement(statement: string) {
+    return element(by.css('metron-config-sensor-grok .ace_text-input')).sendKeys(statement);
+  }
+
+  saveParser() {
+    return element(by.css('.btn.save-button')).click();
+  }
+
+  setParserName(name: string) {
+    return element(by.css('input[name="sensorTopic"]')).sendKeys(name);
+  }
+
+  setParserType(parserName: string) {
+    return element(by.css('select[formcontrolname="parserClassName"]')).click().then(() => {
+      return element.all(by.cssContainingText('select[formcontrolname="parserClassName"] option', parserName)).get(0).click();
+    })
+  }
+
+  setSampleMessage(paneName: string, message: string) {
+    let sampleMsgTextArea = element(by.css('metron-config-' + paneName + ' .form-control.sample-input'));
+    return waitForElementVisibility(sampleMsgTextArea).then(() => {
+      return sampleMsgTextArea.sendKeys(message);
+    })
+  }
+
+  setSchemaConfig(fieldName: string, transformValues: string[], enrichmentValues: string[], threatIntelValues: string[]) {
+    this.getFieldSchemaEditButton(fieldName).click();
+    
+    transformValues.forEach(transform => {
+      let transformSelect = this.getTransformationMultipleInput();
+      transformSelect.click().then(() => {
+        transformSelect.all(by.cssContainingText('option', transform)).last().click();
+      });
+    });
+    
+    enrichmentValues.forEach(enrichment => {
+      let enrichmentSelect = this.getEnrichmentsMultipleInput();
+      enrichmentSelect.click().then(() => {
+        enrichmentSelect.all(by.cssContainingText('option', enrichment)).last().click();
+      });
+    })
+
+    threatIntelValues.forEach(threatIntel => {
+      let threatIntelSelect = this.getThreatIntelMultipleInput();
+      threatIntelSelect.click().then(() => {
+        threatIntelSelect.all(by.cssContainingText('option', threatIntel)).last().click();
+      });
+    })
+  }
+
+  setThreatTriageRule(rule: string) {
+    let threatTriageTextArea = element(by.css('metron-config-sensor-rule-editor textarea'));
+    return waitForElementVisibility(threatTriageTextArea).then(() => {
+      return threatTriageTextArea.sendKeys(rule);
+    })
+  }
+
+  testGrokStatement() {
+    let saveButton = element(by.cssContainingText('metron-config-sensor-grok button', 'TEST'));
+    return waitForElementVisibility(saveButton).then(() => {
+      return element(by.cssContainingText('metron-config-sensor-grok button', 'TEST')).click();
+    })
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/e2e/sensor-list/sensor-list-parser-actions.e2e-spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/e2e/sensor-list/sensor-list-parser-actions.e2e-spec.ts b/metron-interface/metron-config/e2e/sensor-list/sensor-list-parser-actions.e2e-spec.ts
new file mode 100644
index 0000000..f8477cb
--- /dev/null
+++ b/metron-interface/metron-config/e2e/sensor-list/sensor-list-parser-actions.e2e-spec.ts
@@ -0,0 +1,132 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { SensorListPage } from './sensor-list.po';
+import {LoginPage} from '../login/login.po';
+
+describe('Sensor List Long Running Cases', function() {
+    let page: SensorListPage = new SensorListPage();
+    let loginPage = new LoginPage();
+    let ASCENDING_CSS = 'fa fa-sort-asc';
+    let DESCENDING_CSS = 'fa fa-sort-desc';
+    let defaultActionState = [
+        { classNames: 'fa-circle-o-notch', displayed: false },
+        { classNames: 'fa-stop', displayed: false },
+        { classNames: 'fa-ban', displayed: false },
+        { classNames: 'fa-play', displayed: true },
+        { classNames: 'fa-check-circle-o', displayed: false },
+        { classNames: 'fa-pencil', displayed: true },
+        { classNames: 'fa-trash-o', displayed: true }
+    ];
+    let runningActionstate = [
+        { classNames: 'fa-circle-o-notch', displayed: false},
+        { classNames: 'fa-stop', displayed: true},
+        { classNames: 'fa-ban', displayed: true},
+        { classNames: 'fa-play', displayed: false},
+        { classNames: 'fa-check-circle-o', displayed: false},
+        { classNames: 'fa-pencil', displayed: true},
+        { classNames: 'fa-trash-o', displayed: true}
+    ];
+    let disabledActionState = [
+        { classNames: 'fa-circle-o-notch', displayed: false},
+        { classNames: 'fa-stop', displayed: true},
+        { classNames: 'fa-ban', displayed: false},
+        { classNames: 'fa-play', displayed: false},
+        { classNames: 'fa-check-circle-o', displayed: true},
+        { classNames: 'fa-pencil', displayed: true},
+        { classNames: 'fa-trash-o', displayed: true}
+    ];
+    
+    beforeAll(() => {
+        loginPage.login();
+    });
+
+    afterAll(() => {
+        loginPage.logout();
+    });
+
+    it('should start parsers from actions', (done) => {
+        expect(page.getAddButton()).toEqual(true);
+        page.startParsers(['websphere', 'jsonMap', 'squid', 'asa', 'snort', 'bro', 'yaf']).then((args) => {
+            expect(args).toEqual([true, true, true, true, true, true, true])
+            done();
+        })
+
+    }, 300000)
+
+    it('should have correct values when parser is running', () => {
+        expect(page.getColumnValues(2)).toEqual(['Running', 'Running', 'Running', 'Running', 'Running', 'Running', 'Running']);
+        expect(page.getColumnValues(3)).toEqual(['0s', '0s', '0s', '0s', '0s', '0s', '0s']);
+        expect(page.getColumnValues(4)).toEqual(['0kb/s', '0kb/s', '0kb/s', '0kb/s', '0kb/s', '0kb/s', '0kb/s']);
+        expect(page.getColumnValues(5)).toEqual(['', '', '', '', '', '', '']);
+        expect(page.getColumnValues(6)).toEqual(['', '', '', '', '', '', '']);
+    });
+
+    it('should disable all parsers from actions', (done) => {
+        page.disableParsers(['websphere', 'jsonMap', 'squid', 'asa', 'snort', 'bro', 'yaf']).then((args) => {
+            expect(args).toEqual([true, true, true, true, true, true, true])
+            done();
+        })
+
+    }, 300000)
+
+    it('should have correct values when parser is disabled', () => {
+        expect(page.getColumnValues(2)).toEqual(['Disabled', 'Disabled', 'Disabled', 'Disabled', 'Disabled', 'Disabled', 'Disabled']);
+    });
+
+    it('should enable all parsers from actions', (done) => {
+        page.enableParsers(['websphere', 'jsonMap', 'squid', 'asa', 'snort', 'bro', 'yaf']).then((args) => {
+            expect(args).toEqual([true, true, true, true, true, true, true])
+            done();
+        })
+    }, 300000)
+
+    it('should stop parsers from actions', (done) => {
+        page.stopParsers(['websphere', 'jsonMap', 'squid', 'asa', 'snort', 'yaf', 'bro']).then((args) => {
+            expect(args).toEqual([true, true, true, true, true, true, true]);
+            done();
+        })
+    }, 300000)
+
+    it('should start parsers from dropdown', (done) => {
+        page.startParsersFromDropdown(['websphere', 'jsonMap', 'squid', 'asa', 'snort', 'bro', 'yaf']).then((args) => {
+            expect(args).toEqual([true, true, true, true, true, true, true]);
+            done();
+        })
+    }, 300000)
+
+    it('should disable parsers from dropdown', (done) => {
+        page.disableParsersFromDropdown(['websphere', 'jsonMap', 'squid', 'asa', 'snort', 'bro', 'yaf']).then((args) => {
+            expect(args).toEqual([true, true, true, true, true, true, true]);
+            done();
+        })
+    }, 300000)
+
+    it('should enable parsers from dropdown', (done) => {
+        page.enableParsersFromDropdown(['websphere', 'jsonMap', 'squid', 'asa', 'snort', 'bro', 'yaf']).then((args) => {
+            expect(args).toEqual([true, true, true, true, true, true, true]);
+            done();
+        })
+    }, 300000)
+
+    it('should stop parsers from dropdown', (done) => {
+        page.stopParsersFromDropdown(['websphere', 'jsonMap', 'squid', 'asa', 'snort', 'bro', 'yaf']).then((args) => {
+            expect(args).toEqual([true, true, true, true, true, true, true]);
+            done();
+        })
+    }, 300000)
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/e2e/sensor-list/sensor-list.e2e-spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/e2e/sensor-list/sensor-list.e2e-spec.ts b/metron-interface/metron-config/e2e/sensor-list/sensor-list.e2e-spec.ts
new file mode 100644
index 0000000..24c2215
--- /dev/null
+++ b/metron-interface/metron-config/e2e/sensor-list/sensor-list.e2e-spec.ts
@@ -0,0 +1,161 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { SensorListPage } from './sensor-list.po';
+import {LoginPage} from '../login/login.po';
+
+describe('Sensor List', function() {
+    let page: SensorListPage = new SensorListPage();
+    let loginPage = new LoginPage();
+    let ASCENDING_CSS = 'fa fa-sort-asc';
+    let DESCENDING_CSS = 'fa fa-sort-desc';
+    let defaultActionState = [
+        { classNames: 'fa-circle-o-notch', displayed: false },
+        { classNames: 'fa-stop', displayed: false },
+        { classNames: 'fa-ban', displayed: false },
+        { classNames: 'fa-play', displayed: true },
+        { classNames: 'fa-check-circle-o', displayed: false },
+        { classNames: 'fa-pencil', displayed: true },
+        { classNames: 'fa-trash-o', displayed: true }
+    ];
+    let runningActionstate = [
+        { classNames: 'fa-circle-o-notch', displayed: false},
+        { classNames: 'fa-stop', displayed: true},
+        { classNames: 'fa-ban', displayed: true},
+        { classNames: 'fa-play', displayed: false},
+        { classNames: 'fa-check-circle-o', displayed: false},
+        { classNames: 'fa-pencil', displayed: true},
+        { classNames: 'fa-trash-o', displayed: true}
+    ];
+    let disabledActionState = [
+        { classNames: 'fa-circle-o-notch', displayed: false},
+        { classNames: 'fa-stop', displayed: true},
+        { classNames: 'fa-ban', displayed: false},
+        { classNames: 'fa-play', displayed: false},
+        { classNames: 'fa-check-circle-o', displayed: true},
+        { classNames: 'fa-pencil', displayed: true},
+        { classNames: 'fa-trash-o', displayed: true}
+    ];
+
+    beforeAll(() => {
+        loginPage.login();
+    });
+
+    afterAll(() => {
+        loginPage.logout();
+    });
+
+    it('should have title defined', () => {
+        expect(page.getTitle()).toEqual('Sensors (7)');
+    });
+
+    it('should have add button', () => {
+        expect(page.getAddButton()).toEqual(true);
+    });
+
+    it('should have all the default parsers', () => {
+      expect(page.getParserCount()).toEqual(7);
+    });
+
+    it('should have all the table headers', () => {
+        expect(page.getTableColumnNames()).toEqual([ 'Name', 'Parser', 'Status', 'Latency', 'Throughput', 'Last Updated', 'Last Editor' ]);
+    });
+
+    it('should sort table headers', () => {
+        page.toggleSort('Name');
+        expect(page.getSortOrder('Name')).toEqual(ASCENDING_CSS);
+        expect(page.getColumnValues(0)).toEqual(['asa', 'bro', 'jsonMap', 'snort', 'squid', 'websphere', 'yaf']);
+
+        page.toggleSort('Name');
+        expect(page.getSortOrder('Name')).toEqual(DESCENDING_CSS);
+        expect(page.getColumnValues(0)).toEqual(['yaf', 'websphere', 'squid', 'snort', 'jsonMap', 'bro', 'asa']);
+
+        page.toggleSort('Parser');
+        expect(page.getSortOrder('Parser')).toEqual(ASCENDING_CSS);
+        expect(page.getColumnValues(1)).toEqual(['Grok', 'Grok', 'Java', 'Java', 'Java', 'Java', 'Java']);
+
+        page.toggleSort('Parser');
+        expect(page.getSortOrder('Parser')).toEqual(DESCENDING_CSS);
+        expect(page.getColumnValues(1)).toEqual(['Java', 'Java', 'Java', 'Java', 'Java', 'Grok', 'Grok']);
+
+        page.toggleSort('Status');
+        expect(page.getSortOrder('Status')).toEqual(ASCENDING_CSS);
+        expect(page.getColumnValues(2)).toEqual(['Stopped', 'Stopped', 'Stopped', 'Stopped', 'Stopped', 'Stopped', 'Stopped']);
+
+        page.toggleSort('Status');
+        expect(page.getSortOrder('Status')).toEqual(DESCENDING_CSS);
+        expect(page.getColumnValues(2)).toEqual(['Stopped', 'Stopped', 'Stopped', 'Stopped', 'Stopped', 'Stopped', 'Stopped']);
+    });
+
+    it('should select deselect all rows', () => {
+        page.toggleSelectAll();
+        expect(page.getSelectedRowCount()).toEqual(7);
+        page.toggleSelectAll();
+        expect(page.getSelectedRowCount()).toEqual(0);
+    })
+
+    it('should select deselect individual rows', () => {
+        ['websphere', 'jsonMap', 'squid', 'asa', 'snort', 'bro', 'yaf'].map(pName => {
+            page.toggleRowSelect(pName);
+            expect(page.getSelectedRowCount()).toEqual(1);
+            page.toggleRowSelect(pName);
+            expect(page.getSelectedRowCount()).toEqual(0);
+        });
+    })
+
+    it('should enable action in dropdown', () => {
+        expect(page.getDropdownActionState()).toEqual({ enabled: 0, disabled: 0, displayed: false});
+
+        page.toggleDropdown();
+        expect(page.getDropdownActionState()).toEqual({ enabled: 0, disabled: 5, displayed: true});
+        page.toggleDropdown();
+
+        page.toggleSelectAll();
+        page.toggleDropdown();
+        expect(page.getDropdownActionState()).toEqual({ enabled: 5, disabled: 0, displayed: true});
+        page.toggleDropdown();
+        page.toggleSelectAll();
+
+        ['websphere', 'jsonMap', 'squid', 'asa', 'snort', 'bro', 'yaf'].map(pName => {
+            page.toggleRowSelect(pName);
+            page.toggleDropdown();
+            expect(page.getDropdownActionState()).toEqual({ enabled: 5, disabled: 0, displayed: true});
+            page.toggleDropdown();
+            page.toggleRowSelect(pName);
+        });
+    })
+
+    it('should have all the actions with default value', () => {
+        ['websphere', 'jsonMap', 'squid', 'asa', 'snort', 'bro', 'yaf'].map(pName => {
+            expect(page.getActions(pName)).toEqual(defaultActionState);
+        });
+    })
+
+    it('should open the details pane', (done) => {
+        ['websphere', 'jsonMap', 'squid', 'asa', 'snort', 'bro', 'yaf'].map(pName => {
+            expect(page.openDetailsPane(pName)).toEqual('http://localhost:4200/sensors(dialog:sensors-readonly/' + pName +')');
+        });
+        page.closePane().then(() => {done();});
+    }, 300000)
+
+    it('should open the edit pane', () => {
+        ['websphere', 'jsonMap', 'squid', 'asa', 'snort', 'bro', 'yaf'].map(pName => {
+            expect(page.openEditPaneAndClose(pName)).toEqual('http://localhost:4200/sensors(dialog:sensors-config/' + pName +')');
+        });
+    })
+
+});



[05/12] incubator-metron git commit: METRON-623 Management UI [contributed by Raghu Mitra Kandikonda and Ryan Merriman] closes apache/incubator-metron#489

Posted by rm...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.html b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.html
new file mode 100644
index 0000000..9d67df6
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.html
@@ -0,0 +1,88 @@
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You under the Apache License,
+	Version 2.0 (the "License"); you may not use this file except in compliance
+	with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software distributed
+	under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+	OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+  the specific language governing permissions and limitations under the License.
+  -->
+<metron-config-sensor-rule-editor *ngIf="showTextEditor" [riskLevelRule]="currentRiskLevelRule"
+                                  (onCancelTextEditor)="onCancelTextEditor()"
+                                  (onSubmitTextEditor)="onSubmitTextEditor($event)"></metron-config-sensor-rule-editor>
+
+<div class="metron-slider-pane-edit fill load-left-to-right dialog1x">
+
+    <div class="form-title">Threat Triage Rules</div>
+    <i class="fa fa-times pull-right close-button" aria-hidden="true" (click)="onClose()"></i>
+
+
+    <form role="form" class="threat-intel-form">
+        <div class="form-group threat-triage-aggregator">
+            <label attr.for="aggregator">AGGREGATOR</label>
+            <select class="form-control" name="aggregator"
+                    [(ngModel)]="sensorEnrichmentConfig.threatIntel.triageConfig.aggregator">
+                <option *ngFor="let aggregator of availableAggregators" [value]="aggregator">{{aggregator}}</option>
+            </select>
+        </div>
+        <div class="threat-triage-summary">
+            <div class="form-group">
+                <div class="rules-summary-title">Rules</div>
+                <div class="row mx-0">
+                    <div class="btn" (click)="onFilterChange(threatTriageFilter.HIGH)" [ngClass]="{'filter-button': filter != threatTriageFilter.HIGH, 'filter-button-selected': filter == threatTriageFilter.HIGH}">
+                        <i aria-hidden="true" class="fa fa-circle" style="color: red"></i> {{highAlerts}}
+                    </div>
+                    <div class="btn" (click)="onFilterChange(threatTriageFilter.MEDIUM)" [ngClass]="{'filter-button': filter != threatTriageFilter.MEDIUM, 'filter-button-selected': filter == threatTriageFilter.MEDIUM}">
+                        <i aria-hidden="true" class="fa fa-circle" style="color: orange"></i> {{mediumAlerts}}
+                    </div>
+                    <div class="btn" (click)="onFilterChange(threatTriageFilter.LOW)" [ngClass]="{'filter-button': filter != threatTriageFilter.LOW, 'filter-button-selected': filter == threatTriageFilter.LOW}">
+                        <i aria-hidden="true" class="fa fa-circle" style="color: khaki"></i> {{lowAlerts}}
+                    </div>
+                </div>
+                <div class="row mx-0 threat-triage-rules-sort">
+                    <span class="label">Sort by </span>
+                    <li class="nav-item dropdown">
+                        <span class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">{{sortOrderOption[sortOrder].replace('_', ' ')}}</span>
+                        <div class="dropdown-menu bg-inverse">
+                            <span class="dropdown-item" (click)="onSortOrderChange(sortOrderOption.Highest_Score)">Highest Score</span>
+                            <span class="dropdown-item" (click)="onSortOrderChange(sortOrderOption.Lowest_Score)">Lowest Score</span>
+                            <span class="dropdown-item" (click)="onSortOrderChange(sortOrderOption.Highest_Name)">Highest Name</span>
+                            <span class="dropdown-item" (click)="onSortOrderChange(sortOrderOption.Lowest_Name)">Lowest Name</span>
+                        </div>
+                    </li>
+                </div>
+            </div>
+        </div>
+        <div class="form-group threat-triage-rules-list">
+            <div *ngFor="let riskLevelRule of this.visibleRules">
+                <div class="row mx-0 py-0">
+                    <div class="threat-triage-rule-row" style="color: khaki; font-size: 30px; margin-top: -7px" [style.color]="getRuleColor(riskLevelRule)">
+                        <b>I</b>
+                    </div>
+                    <div class="threat-triage-rule-row" style="font-size: small; width: 8%">
+                        {{ riskLevelRule.score }}
+                    </div>
+                    <div class="threat-triage-rule-row threat-triage-rule-str">
+                        {{ getDisplayName(riskLevelRule) }}
+                    </div>
+                    <div class="threat-triage-rule-row" style=""><i class="fa fa-i-cursor" aria-hidden="true"
+                                                                        style="cursor: pointer;" (click)="onEditRule(riskLevelRule)"></i></div>
+
+                    <div class="threat-triage-rule-row" style=""><i  class="fa fa-trash-o" aria-hidden="true" (click)="onDeleteRule(riskLevelRule)" style="cursor: pointer"></i></div>
+                </div>
+                <div class="form-seperator-edit"></div>
+            </div>
+        </div>
+        <div class="form-group mx-0 py-0">
+            <button class="btn form-enable-disable-button add-button" (click)="onNewRule()"><i
+                    aria-hidden="true" class="fa fa-plus fa-4"></i></button>
+        </div>
+    </form>
+
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.scss b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.scss
new file mode 100644
index 0000000..bbc17a0
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.scss
@@ -0,0 +1,137 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../../_variables.scss";
+@import "../../../styles.scss";
+
+textarea
+{
+  height: auto;
+}
+
+.form-title
+{
+  padding-left: 25px;
+}
+
+.form-group
+{
+  padding-left: 25px;
+  padding-right: 20px;
+}
+
+.close-button
+{
+  padding-right: 20px;
+}
+
+.threat-triage-summary
+{
+  background-color: $edit-child-highlight;
+  padding-top: 5px;
+  padding-bottom: 5px;
+}
+
+.filter-button
+{
+  width: 32%;
+  background: $field-background;
+  border: 1px solid $form-button-border;
+  border-radius: .25em;
+  cursor: default;
+  color: $nav-active-color;
+  i {
+    font-size: smaller;
+  }
+}
+
+.filter-button-selected
+{
+  @extend .filter-button;
+  background-color: #006ea0;
+  color: #bdbdbd;
+}
+
+.threat-triage-aggregator
+{
+  padding-bottom: 10px;
+}
+
+.threat-triage-rules-sort
+{
+  padding-top: 5px;
+  font-size: 12px;
+  font-family: Roboto-Regular;
+  .label
+  {
+    display: inline-block;
+    padding-right: 8px;
+  }
+  .dropdown
+  {
+    list-style: none;
+    display: inline-block;
+    color: $field-button-color;
+  }
+}
+
+.rules-summary-title
+{
+  font-size: 15px;
+}
+
+.threat-triage-rules-list
+{
+  padding-top: 10px;
+}
+
+.threat-triage-rule-row
+{
+  display: inline-block;
+  position: relative;
+  vertical-align: top;
+
+  .fa {
+    color: $nav-active-color;
+    font-size: large;
+  }
+}
+
+.add-button
+{
+  font-size: 20px;
+  width: 100%;
+  padding: 2px;
+
+  i {
+    color: $field-button-color;
+  }
+}
+
+.threat-triage-rule-str
+{
+  width: 64%;
+  padding-right: 10px;
+  font-size: small;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+metron-config-sensor-rule-editor
+{
+  @extend .flexbox-row-reverse;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.spec.ts
new file mode 100644
index 0000000..b9e595e
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.spec.ts
@@ -0,0 +1,211 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {SimpleChange, SimpleChanges} from '@angular/core';
+import {Http} from '@angular/http';
+import {async, TestBed, ComponentFixture} from '@angular/core/testing';
+import {SensorThreatTriageComponent, SortOrderOption, ThreatTriageFilter} from './sensor-threat-triage.component';
+import {SensorEnrichmentConfig, ThreatIntelConfig} from '../../model/sensor-enrichment-config';
+import {RiskLevelRule} from '../../model/risk-level-rule';
+import {SensorEnrichmentConfigService} from '../../service/sensor-enrichment-config.service';
+import {Observable} from 'rxjs/Observable';
+import '../../rxjs-operators';
+import {SensorThreatTriageModule} from './sensor-threat-triage.module';
+
+class MockSensorEnrichmentConfigService {
+  public getAvailableThreatTriageAggregators(): Observable<string[]> {
+    return Observable.create(observer => {
+      observer.next(['MAX', 'MIN', 'SUM', 'MEAN', 'POSITIVE_MEAN']);
+      observer.complete();
+    });
+  }
+}
+
+describe('Component: SensorThreatTriageComponent', () => {
+
+  let component: SensorThreatTriageComponent;
+  let fixture: ComponentFixture<SensorThreatTriageComponent>;
+  let sensorEnrichmentConfigService: SensorEnrichmentConfigService;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [SensorThreatTriageModule],
+      providers: [
+        {provide: Http},
+        {provide: SensorEnrichmentConfigService, useClass: MockSensorEnrichmentConfigService},
+      ]
+    }).compileComponents()
+        .then(() => {
+          fixture = TestBed.createComponent(SensorThreatTriageComponent);
+          component = fixture.componentInstance;
+          sensorEnrichmentConfigService = fixture.debugElement.injector.get(SensorEnrichmentConfigService);
+        });
+  }));
+
+  it('should create an instance', () => {
+    expect(component).toBeDefined();
+    fixture.destroy();
+  });
+
+  it('should create an instance', async(() => {
+    spyOn(component, 'init');
+    let changes: SimpleChanges = {'showThreatTriage': new SimpleChange(false, true)};
+
+    component.ngOnChanges(changes);
+    expect(component.init).toHaveBeenCalled();
+
+    changes = {'showStellar': new SimpleChange(true, false)};
+    component.ngOnChanges(changes);
+    expect(component.init['calls'].count()).toEqual(1);
+
+    fixture.destroy();
+  }));
+
+  it('should close panel', async(() => {
+    let numClosed = 0;
+    component.hideThreatTriage.subscribe((closed: boolean) => {
+      numClosed++;
+    });
+
+    component.onClose();
+    expect(numClosed).toEqual(1);
+
+    fixture.destroy();
+  }));
+
+  it('should get color', async(() => {
+    let sensorEnrichmentConfig = new SensorEnrichmentConfig();
+    sensorEnrichmentConfig.threatIntel = Object.assign(new ThreatIntelConfig(), {
+      'triageConfig': {
+        'riskLevelRules': {
+          'ruleA': 15,
+          'ruleB': 95,
+          'ruleC': 50
+        },
+        'aggregator': 'MAX',
+        'aggregationConfig': {}
+      }
+    });
+    component.sensorEnrichmentConfig = sensorEnrichmentConfig;
+
+    let ruleA = {name: 'ruleA', rule: 'rule A', score: 15, comment: ''};
+    let ruleB = {name: 'ruleB', rule: 'rule B', score: 95, comment: ''};
+    let ruleC = {name: 'ruleC', rule: 'rule C', score: 50, comment: ''};
+
+    expect(component.getRuleColor(ruleA)).toEqual('khaki');
+    expect(component.getRuleColor(ruleB)).toEqual('red');
+    expect(component.getRuleColor(ruleC)).toEqual('orange');
+
+    fixture.destroy();
+  }));
+
+  it('should edit rules', async(() => {
+    let ruleA = {name: 'ruleA', rule: 'rule A', score: 15, comment: ''};
+    let ruleB = {name: 'ruleB', rule: 'rule B', score: 95, comment: ''};
+    let ruleC = {name: 'ruleC', rule: 'rule C', score: 50, comment: ''};
+    let ruleD = {name: 'ruleD', rule: 'rule D', score: 85, comment: ''};
+    let ruleE = {name: 'ruleE', rule: 'rule E', score: 5, comment: ''};
+    let ruleF = {name: 'ruleF', rule: 'rule F', score: 21, comment: ''};
+    let ruleG = {name: 'ruleG', rule: 'rule G', score: 100, comment: ''};
+
+    let sensorEnrichmentConfig = new SensorEnrichmentConfig();
+    sensorEnrichmentConfig.threatIntel = Object.assign(new ThreatIntelConfig(), {
+      'triageConfig': {
+        'riskLevelRules': [ruleA, ruleB, ruleC, ruleD, ruleE],
+        'aggregator': 'MAX',
+        'aggregationConfig': {}
+      }
+    });
+    component.sensorEnrichmentConfig = sensorEnrichmentConfig;
+
+
+    let changes: SimpleChanges = {'showThreatTriage': new SimpleChange(false, true)};
+    component.ngOnChanges(changes);
+
+    // sorted by score high to low
+    expect(component.visibleRules).toEqual([ruleB, ruleD, ruleC, ruleA, ruleE]);
+    expect(component.lowAlerts).toEqual(2);
+    expect(component.mediumAlerts).toEqual(1);
+    expect(component.highAlerts).toEqual(2);
+
+    // sorted by name high to low
+    component.onSortOrderChange(SortOrderOption.Highest_Name);
+    expect(component.visibleRules).toEqual([ruleE, ruleD, ruleC, ruleB, ruleA]);
+
+    // sorted by score low to high
+    component.onSortOrderChange(SortOrderOption.Lowest_Score);
+    expect(component.visibleRules).toEqual([ruleE, ruleA, ruleC, ruleD, ruleB]);
+
+    // sorted by name low to high
+    component.onSortOrderChange(SortOrderOption.Lowest_Name);
+    expect(component.visibleRules).toEqual([ruleA, ruleB, ruleC, ruleD, ruleE]);
+
+    component.onNewRule();
+    expect(component.currentRiskLevelRule.name).toEqual('');
+    expect(component.currentRiskLevelRule.rule).toEqual('');
+    expect(component.currentRiskLevelRule.score).toEqual(0);
+    expect(component.showTextEditor).toEqual(true);
+
+    component.currentRiskLevelRule = new RiskLevelRule();
+    component.onCancelTextEditor();
+    expect(component.showTextEditor).toEqual(false);
+    expect(component.visibleRules).toEqual([ruleA, ruleB, ruleC, ruleD, ruleE]);
+
+    component.sortOrder = SortOrderOption.Lowest_Score;
+    component.onNewRule();
+    component.currentRiskLevelRule = ruleF;
+    expect(component.showTextEditor).toEqual(true);
+    component.onSubmitTextEditor(ruleF);
+    expect(component.visibleRules).toEqual([ruleE, ruleA, ruleF, ruleC, ruleD, ruleB]);
+    expect(component.lowAlerts).toEqual(2);
+    expect(component.mediumAlerts).toEqual(2);
+    expect(component.highAlerts).toEqual(2);
+    expect(component.showTextEditor).toEqual(false);
+
+    component.onDeleteRule(ruleE);
+    expect(component.visibleRules).toEqual([ruleA, ruleF, ruleC, ruleD, ruleB]);
+    expect(component.lowAlerts).toEqual(1);
+    expect(component.mediumAlerts).toEqual(2);
+    expect(component.highAlerts).toEqual(2);
+
+    component.onFilterChange(ThreatTriageFilter.LOW);
+    expect(component.visibleRules).toEqual([ruleA]);
+
+    component.onFilterChange(ThreatTriageFilter.MEDIUM);
+    expect(component.visibleRules).toEqual([ruleF, ruleC]);
+
+    component.onFilterChange(ThreatTriageFilter.HIGH);
+    expect(component.visibleRules).toEqual([ruleD, ruleB]);
+
+    component.onFilterChange(ThreatTriageFilter.HIGH);
+    expect(component.visibleRules).toEqual([ruleA, ruleF, ruleC, ruleD, ruleB]);
+
+    component.onEditRule(ruleC);
+    expect(component.currentRiskLevelRule).toEqual(ruleC);
+    expect(component.showTextEditor).toEqual(true);
+    component.onSubmitTextEditor(ruleG);
+    expect(component.visibleRules).toEqual([ruleA, ruleF, ruleD, ruleB, ruleG]);
+    expect(component.lowAlerts).toEqual(1);
+    expect(component.mediumAlerts).toEqual(1);
+    expect(component.highAlerts).toEqual(3);
+    expect(component.showTextEditor).toEqual(false);
+
+    fixture.destroy();
+  }));
+
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.ts b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.ts
new file mode 100644
index 0000000..db32b04
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.ts
@@ -0,0 +1,208 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* tslint:disable:triple-equals */
+import {Component, Input, EventEmitter, Output, OnChanges, SimpleChanges} from '@angular/core';
+import {SensorEnrichmentConfig } from '../../model/sensor-enrichment-config';
+import {RiskLevelRule} from '../../model/risk-level-rule';
+import {SensorEnrichmentConfigService} from '../../service/sensor-enrichment-config.service';
+
+export enum SortOrderOption {
+  Lowest_Score, Highest_Score, Lowest_Name, Highest_Name
+}
+
+export enum ThreatTriageFilter {
+  NONE, LOW, MEDIUM, HIGH
+}
+
+@Component({
+  selector: 'metron-config-sensor-threat-triage',
+  templateUrl: './sensor-threat-triage.component.html',
+  styleUrls: ['./sensor-threat-triage.component.scss']
+})
+
+export class SensorThreatTriageComponent implements OnChanges {
+
+  @Input() showThreatTriage: boolean;
+  @Input() sensorEnrichmentConfig: SensorEnrichmentConfig;
+
+  @Output() hideThreatTriage: EventEmitter<boolean> = new EventEmitter<boolean>();
+  availableAggregators = [];
+  visibleRules: RiskLevelRule[] = [];
+
+  showTextEditor = false;
+  currentRiskLevelRule: RiskLevelRule;
+
+  lowAlerts = 0;
+  mediumAlerts = 0;
+  highAlerts = 0;
+
+  sortOrderOption = SortOrderOption;
+  sortOrder = SortOrderOption.Highest_Score;
+  threatTriageFilter = ThreatTriageFilter;
+  filter: ThreatTriageFilter = ThreatTriageFilter.NONE;
+
+  constructor(private sensorEnrichmentConfigService: SensorEnrichmentConfigService) { }
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (changes['showThreatTriage'] && changes['showThreatTriage'].currentValue) {
+      this.init();
+    }
+  }
+
+  init(): void {
+    this.visibleRules = this.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules;
+    this.sensorEnrichmentConfigService.getAvailableThreatTriageAggregators().subscribe(results => {
+      this.availableAggregators = results;
+    });
+    this.updateBuckets();
+    this.onSortOrderChange(null);
+  }
+
+  onClose(): void {
+    this.hideThreatTriage.emit(true);
+  }
+
+
+  onSubmitTextEditor(riskLevelRule: RiskLevelRule): void {
+    this.deleteRule(this.currentRiskLevelRule);
+    this.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules.push(riskLevelRule);
+    this.showTextEditor = false;
+    this.init();
+  }
+
+  onCancelTextEditor(): void {
+    this.showTextEditor = false;
+  }
+
+  onEditRule(riskLevelRule: RiskLevelRule) {
+    this.currentRiskLevelRule = riskLevelRule;
+    this.showTextEditor = true;
+  }
+
+  onDeleteRule(riskLevelRule: RiskLevelRule) {
+    this.deleteRule(riskLevelRule);
+    this.init();
+  }
+
+  onNewRule(): void {
+    this.currentRiskLevelRule = new RiskLevelRule();
+    this.showTextEditor = true;
+  }
+
+  deleteRule(riskLevelRule: RiskLevelRule) {
+    let index = this.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules.indexOf(riskLevelRule);
+    if (index != -1) {
+      this.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules.splice(index, 1);
+    }
+  }
+
+  updateBuckets() {
+    this.lowAlerts = 0;
+    this.mediumAlerts = 0;
+    this.highAlerts = 0;
+    for (let riskLevelRule of this.visibleRules) {
+      if (riskLevelRule.score <= 20) {
+        this.lowAlerts++;
+      } else if (riskLevelRule.score >= 80) {
+        this.highAlerts++;
+      } else {
+        this.mediumAlerts++;
+      }
+    }
+  }
+
+  getRuleColor(riskLevelRule: RiskLevelRule): string {
+    let color: string;
+    if (riskLevelRule.score <= 20) {
+      color = 'khaki';
+    } else if (riskLevelRule.score >= 80) {
+      color = 'red';
+    } else {
+      color = 'orange';
+    }
+    return color;
+  }
+
+  onSortOrderChange(sortOrder: any) {
+    if (sortOrder !== null) {
+      this.sortOrder = sortOrder;
+    }
+
+    // all comparisons with enums must be == and not ===
+    if (this.sortOrder == this.sortOrderOption.Highest_Score) {
+      this.visibleRules.sort((a, b) => {
+        return b.score - a.score;
+      });
+    } else if (this.sortOrder == SortOrderOption.Lowest_Score) {
+      this.visibleRules.sort((a, b) => {
+        return a.score - b.score;
+      });
+    } else if (this.sortOrder == SortOrderOption.Lowest_Name) {
+      this.visibleRules.sort((a, b) => {
+        let aName = a.name ? a.name : '';
+        let bName = b.name ? b.name : '';
+        if (aName.toLowerCase() >= bName.toLowerCase()) {
+          return 1;
+        } else if (aName.toLowerCase() < bName.toLowerCase()) {
+          return -1;
+        }
+      });
+    } else {
+      this.visibleRules.sort((a, b) => {
+        let aName = a.name ? a.name : '';
+        let bName = b.name ? b.name : '';
+        if (aName.toLowerCase() >= bName.toLowerCase()) {
+          return -1;
+        } else if (aName.toLowerCase() < bName.toLowerCase()) {
+          return 1;
+        }
+      });
+    }
+  }
+
+  onFilterChange(filter: ThreatTriageFilter) {
+    if (filter === this.filter) {
+      this.filter = ThreatTriageFilter.NONE;
+    } else {
+      this.filter = filter;
+    }
+    this.visibleRules = this.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules.filter(riskLevelRule => {
+      if (this.filter === ThreatTriageFilter.NONE) {
+        return true;
+      } else {
+        if (this.filter === ThreatTriageFilter.HIGH) {
+          return riskLevelRule.score >= 80;
+        } else if (this.filter === ThreatTriageFilter.LOW) {
+          return riskLevelRule.score <= 20;
+        } else {
+          return riskLevelRule.score < 80 && riskLevelRule.score > 20;
+        }
+      }
+    });
+    this.onSortOrderChange(null);
+  }
+
+  getDisplayName(riskLevelRule: RiskLevelRule): string {
+    if (riskLevelRule.name) {
+      return riskLevelRule.name;
+    } else {
+      return riskLevelRule.rule ? riskLevelRule.rule : '';
+    }
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.module.ts b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.module.ts
new file mode 100644
index 0000000..66838d9
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.module.ts
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import {SharedModule} from '../../shared/shared.module';
+import {SensorThreatTriageComponent} from './sensor-threat-triage.component';
+import {SensorRuleEditorModule} from './rule-editor/sensor-rule-editor.module';
+
+
+@NgModule ({
+  imports: [ SharedModule, SensorRuleEditorModule ],
+  declarations: [ SensorThreatTriageComponent ],
+  exports: [ SensorThreatTriageComponent ]
+})
+export class SensorThreatTriageModule {}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/authentication.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/authentication.service.spec.ts b/metron-interface/metron-config/src/app/service/authentication.service.spec.ts
new file mode 100644
index 0000000..7f9b296
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/authentication.service.spec.ts
@@ -0,0 +1,190 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Router} from '@angular/router';
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {Observable} from 'rxjs/Observable';
+import {AuthenticationService} from './authentication.service';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+
+class MockRouter {
+
+    navigateByUrl(url: string) {
+
+    }
+}
+
+describe('AuthenticationService', () => {
+
+    beforeEach(async(() => {
+        TestBed.configureTestingModule({
+            imports: [HttpModule],
+            providers: [
+              AuthenticationService,
+              {provide: XHRBackend, useClass: MockBackend},
+              {provide: Router, useClass: MockRouter},
+              {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+            ]
+        })
+            .compileComponents();
+    }));
+
+    describe('when service functions', () => {
+        it('can instantiate service when inject service',
+            inject([AuthenticationService], (service: AuthenticationService) => {
+                expect(service instanceof AuthenticationService).toBe(true);
+        }));
+
+    });
+
+    describe('when service functions', () => {
+        let authenticationService: AuthenticationService;
+        let mockBackend: MockBackend;
+        let userResponse: Response;
+        let userName = 'test';
+        let router: MockRouter;
+
+        beforeEach(inject([Http, XHRBackend, Router, AuthenticationService, APP_CONFIG],
+            (http: Http, be: MockBackend, mRouter: MockRouter, service: AuthenticationService, config: IAppConfig) => {
+            mockBackend = be;
+            router = mRouter;
+            authenticationService = service;
+            userResponse = new Response(new ResponseOptions({status: 200, body: userName}));
+        }));
+
+        it('init', async(inject([], () => {
+            let userResponsesuccess = true;
+            spyOn(authenticationService.onLoginEvent, 'emit');
+            spyOn(authenticationService, 'getCurrentUser').and.callFake(function() {
+                if (userResponsesuccess) {
+                    return Observable.create(observer => {
+                        observer.next(userResponse);
+                        observer.complete();
+                    });
+                }
+
+                return Observable.throw('Error');
+            });
+
+            authenticationService.init();
+            expect(authenticationService.onLoginEvent.emit).toHaveBeenCalledWith(true);
+
+            userResponsesuccess = false;
+            authenticationService.init();
+            expect(authenticationService.onLoginEvent.emit['calls'].count()).toEqual(2);
+
+        })));
+
+        it('login', async(inject([], () => {
+            let responseMessageSuccess = true;
+            mockBackend.connections.subscribe((c: MockConnection) => {
+                if (responseMessageSuccess) {
+                    c.mockRespond(userResponse);
+                } else {
+                    c.mockError(new Error('login failed'));
+                }
+            });
+
+            spyOn(router, 'navigateByUrl');
+            spyOn(authenticationService.onLoginEvent, 'emit');
+            authenticationService.login('test', 'test', error => {
+            });
+
+            expect(router.navigateByUrl).toHaveBeenCalledWith('/sensors');
+            expect(authenticationService.onLoginEvent.emit).toHaveBeenCalled();
+
+            responseMessageSuccess = false;
+            let errorSpy = jasmine.createSpy('error');
+            authenticationService.login('test', 'test', errorSpy);
+            expect(errorSpy).toHaveBeenCalledWith(new Error('login failed'));
+
+        })));
+
+        it('logout', async(inject([], () => {
+            let responseMessageSuccess = true;
+            mockBackend.connections.subscribe((c: MockConnection) => {
+                if (responseMessageSuccess) {
+                    c.mockRespond(userResponse);
+                } else {
+                    c.mockError(new Error('login failed'));
+                }
+            });
+
+            spyOn(router, 'navigateByUrl');
+            spyOn(authenticationService.onLoginEvent, 'emit');
+            authenticationService.logout();
+
+            expect(router.navigateByUrl).toHaveBeenCalledWith('/login');
+            expect(authenticationService.onLoginEvent.emit).toHaveBeenCalled();
+
+            responseMessageSuccess = false;
+            spyOn(console, 'log');
+            authenticationService.logout();
+            expect(console.log).toHaveBeenCalled();
+
+        })));
+
+        it('checkAuthentication', async(inject([], () => {
+            let isAuthenticated = false;
+            spyOn(router, 'navigateByUrl');
+            spyOn(authenticationService, 'isAuthenticated').and.callFake(function() {
+                return isAuthenticated;
+            });
+
+            authenticationService.checkAuthentication();
+            expect(router.navigateByUrl).toHaveBeenCalledWith('/login');
+
+            isAuthenticated = true;
+            authenticationService.checkAuthentication();
+            expect(router.navigateByUrl['calls'].count()).toEqual(1);
+        })));
+
+        it('getCurrentUser', async(inject([], () => {
+            mockBackend.connections.subscribe((c: MockConnection) => userResponse);
+            authenticationService.getCurrentUser(null).subscribe(
+                result => {
+                    expect(result).toEqual('');
+                }, error => console.log(error));
+        })));
+
+        it('isAuthenticationChecked', async(inject([], () => {
+            mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(userResponse));
+
+            expect(authenticationService.isAuthenticationChecked()).toEqual(false);
+
+            authenticationService.login('test', 'test', null);
+            expect(authenticationService.isAuthenticationChecked()).toEqual(true);
+
+        })));
+
+        it('isAuthenticated', async(inject([], () => {
+            mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(userResponse));
+
+            expect(authenticationService.isAuthenticated()).toEqual(false);
+
+            authenticationService.login('test', 'test', null);
+            expect(authenticationService.isAuthenticated()).toEqual(true);
+
+        })));
+    });
+
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/authentication.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/authentication.service.ts b/metron-interface/metron-config/src/app/service/authentication.service.ts
new file mode 100644
index 0000000..5fd50f3
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/authentication.service.ts
@@ -0,0 +1,92 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, EventEmitter, Inject}     from '@angular/core';
+import {Http, Headers, RequestOptions, Response} from '@angular/http';
+import {Router} from '@angular/router';
+import {Observable}     from 'rxjs/Observable';
+import {IAppConfig} from '../app.config.interface';
+import {APP_CONFIG} from '../app.config';
+
+@Injectable()
+export class AuthenticationService {
+
+  private static USER_NOT_VERIFIED: string = 'USER-NOT-VERIFIED';
+  private currentUser: string = AuthenticationService.USER_NOT_VERIFIED;
+  loginUrl: string = this.config.apiEndpoint + '/user';
+  logoutUrl: string = '/logout';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+  onLoginEvent: EventEmitter<boolean> = new EventEmitter<boolean>();
+
+  constructor(private http: Http, private router: Router, @Inject(APP_CONFIG) private config: IAppConfig) {
+    this.init();
+  }
+
+  public init() {
+      this.getCurrentUser(new RequestOptions({headers: new Headers(this.defaultHeaders)})).subscribe((response: Response) => {
+        this.currentUser = response.text();
+        if (this.currentUser) {
+          this.onLoginEvent.emit(true);
+        }
+      }, error => {
+        this.onLoginEvent.emit(false);
+      });
+  }
+
+  public login(username: string, password: string, onError): void {
+    let loginHeaders: Headers = new Headers(this.defaultHeaders);
+    loginHeaders.append('authorization', 'Basic ' + btoa(username + ':' + password));
+    let loginOptions: RequestOptions = new RequestOptions({headers: loginHeaders});
+    this.getCurrentUser(loginOptions).subscribe((response: Response) => {
+        this.currentUser = response.text();
+        this.router.navigateByUrl('/sensors');
+        this.onLoginEvent.emit(true);
+      },
+      error => {
+        onError(error);
+      });
+  }
+
+  public logout(): void {
+    this.http.post(this.logoutUrl, {}, new RequestOptions({headers: new Headers(this.defaultHeaders)})).subscribe(response => {
+        this.currentUser = AuthenticationService.USER_NOT_VERIFIED;
+        this.onLoginEvent.emit(false);
+        this.router.navigateByUrl('/login');
+      },
+      error => {
+        console.log(error);
+      });
+  }
+
+  public checkAuthentication() {
+    if (!this.isAuthenticated()) {
+      this.router.navigateByUrl('/login');
+    }
+  }
+
+  public getCurrentUser(options: RequestOptions): Observable<Response> {
+    return this.http.get(this.loginUrl, options);
+  }
+
+  public isAuthenticationChecked(): boolean {
+    return this.currentUser !== AuthenticationService.USER_NOT_VERIFIED;
+  }
+
+  public isAuthenticated(): boolean {
+    return this.currentUser !== AuthenticationService.USER_NOT_VERIFIED && this.currentUser != null;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/global-config.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/global-config.service.spec.ts b/metron-interface/metron-config/src/app/service/global-config.service.spec.ts
new file mode 100644
index 0000000..f53c3f3
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/global-config.service.spec.ts
@@ -0,0 +1,99 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+import {GlobalConfigService} from './global-config.service';
+
+describe('GlobalConfigService', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpModule],
+      providers: [
+        GlobalConfigService,
+        {provide: XHRBackend, useClass: MockBackend},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    })
+      .compileComponents();
+  }));
+
+  it('can instantiate service when inject service',
+    inject([GlobalConfigService], (service: GlobalConfigService) => {
+      expect(service instanceof GlobalConfigService).toBe(true);
+    }));
+
+  it('can instantiate service with "new"', inject([Http, APP_CONFIG], (http: Http, config: IAppConfig) => {
+    expect(http).not.toBeNull('http should be provided');
+    let service = new GlobalConfigService(http, config);
+    expect(service instanceof GlobalConfigService).toBe(true, 'new service should be ok');
+  }));
+
+
+  it('can provide the mockBackend as XHRBackend',
+    inject([XHRBackend], (backend: MockBackend) => {
+      expect(backend).not.toBeNull('backend should be provided');
+    }));
+
+  describe('when service functions', () => {
+    let globalConfigService: GlobalConfigService;
+    let mockBackend: MockBackend;
+    let globalConfig = {'field': 'value'};
+    let globalConfigResponse: Response;
+    let deleteResponse: Response;
+
+    beforeEach(inject([Http, XHRBackend, APP_CONFIG], (http: Http, be: MockBackend, config: IAppConfig) => {
+      mockBackend = be;
+      globalConfigService = new GlobalConfigService(http, config);
+      globalConfigResponse = new Response(new ResponseOptions({status: 200, body: globalConfig}));
+    }));
+
+    it('post', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(globalConfigResponse));
+
+      globalConfigService.post(globalConfig).subscribe(
+      result => {
+        expect(result).toEqual(globalConfig);
+      }, error => console.log(error));
+    })));
+
+    it('get', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(globalConfigResponse));
+
+      globalConfigService.get().subscribe(
+        result => {
+          expect(result).toEqual(globalConfig);
+        }, error => console.log(error));
+    })));
+
+    it('deleteSensorParserConfigs', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(deleteResponse));
+
+      globalConfigService.delete().subscribe(result => {
+        expect(result.status).toEqual(200);
+      });
+    })));
+  });
+
+});
+
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/global-config.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/global-config.service.ts b/metron-interface/metron-config/src/app/service/global-config.service.ts
new file mode 100644
index 0000000..1ed4325
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/global-config.service.ts
@@ -0,0 +1,75 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, Inject} from '@angular/core';
+import {Http, Headers, RequestOptions, Response, ResponseOptions} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {HttpUtil} from '../util/httpUtil';
+import {IAppConfig} from '../app.config.interface';
+import {APP_CONFIG} from '../app.config';
+
+@Injectable()
+export class GlobalConfigService {
+  url = this.config.apiEndpoint + '/global/config';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+
+  private globalConfig = {
+
+  };
+
+  constructor(private http: Http, @Inject(APP_CONFIG) private config: IAppConfig) {
+    this.globalConfig['solr.collection'] = 'metron';
+    this.globalConfig['storm.indexingWorkers'] = 1;
+    this.globalConfig['storm.indexingExecutors'] = 2;
+    this.globalConfig['hdfs.boltBatchSize'] = 5000;
+    this.globalConfig['hdfs.boltFieldDelimiter'] = '|';
+    this.globalConfig['hdfs.boltFileRotationSize'] = 5;
+    this.globalConfig['hdfs.boltCompressionCodecClass'] = 'org.apache.hadoop.io.compress.SnappyCodec';
+    this.globalConfig['hdfs.indexOutput'] = '/tmp/metron/enriched';
+    this.globalConfig['kafkaWriter.topic'] = 'outputTopic';
+    this.globalConfig['kafkaWriter.keySerializer'] = 'org.apache.kafka.common.serialization.StringSerializer';
+    this.globalConfig['kafkaWriter.valueSerializer'] = 'org.apache.kafka.common.serialization.StringSerializer';
+    this.globalConfig['kafkaWriter.requestRequiredAcks'] = 1;
+    this.globalConfig['solrWriter.indexName'] = 'alfaalfa';
+    this.globalConfig['solrWriter.shards'] = 1;
+    this.globalConfig['solrWriter.replicationFactor'] = 1;
+    this.globalConfig['solrWriter.batchSize'] = 50;
+  }
+
+  public post(globalConfig: {}): Observable<{}> {
+    return this.http.post(this.url, globalConfig, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public get(): Observable<{}> {
+    return this.http.get(this.url , new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public delete(): Observable<Response> {
+    let responseOptions = new ResponseOptions();
+    responseOptions.status = 200;
+    let response = new Response(responseOptions);
+    return Observable.create(observer => {
+      observer.next(response);
+      observer.complete();
+    });
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/grok-validation.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/grok-validation.service.spec.ts b/metron-interface/metron-config/src/app/service/grok-validation.service.spec.ts
new file mode 100644
index 0000000..da45a80
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/grok-validation.service.spec.ts
@@ -0,0 +1,106 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {GrokValidationService} from './grok-validation.service';
+import {GrokValidation} from '../model/grok-validation';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+
+describe('GrokValidationService', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpModule],
+      providers: [
+        GrokValidationService,
+        {provide: XHRBackend, useClass: MockBackend},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    })
+      .compileComponents();
+  }));
+
+  it('can instantiate service when inject service',
+    inject([GrokValidationService], (service: GrokValidationService) => {
+      expect(service instanceof GrokValidationService).toBe(true);
+    }));
+
+  it('can instantiate service with "new"', inject([Http, APP_CONFIG], (http: Http, config: IAppConfig) => {
+    expect(http).not.toBeNull('http should be provided');
+    let service = new GrokValidationService(http, config);
+    expect(service instanceof GrokValidationService).toBe(true, 'new service should be ok');
+  }));
+
+
+  it('can provide the mockBackend as XHRBackend',
+    inject([XHRBackend], (backend: MockBackend) => {
+      expect(backend).not.toBeNull('backend should be provided');
+    }));
+
+  describe('when service functions', () => {
+    let grokValidationService: GrokValidationService;
+    let mockBackend: MockBackend;
+    let grokValidation = new GrokValidation();
+    grokValidation.statement = 'statement';
+    grokValidation.sampleData = 'sampleData';
+    grokValidation.results = {'results': 'results'};
+    let grokList = ['pattern'];
+    let grokStatement = 'grok statement';
+    let grokValidationResponse: Response;
+    let grokListResponse: Response;
+    let grokGetStatementResponse: Response;
+
+    beforeEach(inject([Http, XHRBackend, APP_CONFIG], (http: Http, be: MockBackend, config: IAppConfig) => {
+      mockBackend = be;
+      grokValidationService = new GrokValidationService(http, config);
+      grokValidationResponse = new Response(new ResponseOptions({status: 200, body: grokValidation}));
+      grokListResponse = new Response(new ResponseOptions({status: 200, body: grokList}));
+      grokGetStatementResponse = new Response(new ResponseOptions({status: 200, body: grokStatement}));
+    }));
+
+    it('validate', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(grokValidationResponse));
+
+      grokValidationService.validate(grokValidation).subscribe(
+        result => {
+          expect(result).toEqual(grokValidation);
+        }, error => console.log(error));
+    })));
+
+    it('list', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(grokListResponse));
+      grokValidationService.list().subscribe(
+        results => {
+          expect(results).toEqual(grokList);
+        }, error => console.log(error));
+    })));
+
+    it('getStatement', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(grokGetStatementResponse));
+      grokValidationService.getStatement('/path').subscribe(
+          results => {
+            expect(results).toEqual(grokStatement);
+          }, error => console.log(error));
+    })));
+  });
+
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/grok-validation.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/grok-validation.service.ts b/metron-interface/metron-config/src/app/service/grok-validation.service.ts
new file mode 100644
index 0000000..bcdce82
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/grok-validation.service.ts
@@ -0,0 +1,56 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, Inject} from '@angular/core';
+import {Http, Headers, RequestOptions, URLSearchParams} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {GrokValidation} from '../model/grok-validation';
+import {HttpUtil} from '../util/httpUtil';
+import {IAppConfig} from '../app.config.interface';
+import {APP_CONFIG} from '../app.config';
+
+@Injectable()
+export class GrokValidationService {
+  url = this.config.apiEndpoint + '/grok';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+
+  constructor(private http: Http, @Inject(APP_CONFIG) private config: IAppConfig) {
+
+  }
+
+  public validate(grokValidation: GrokValidation): Observable<GrokValidation> {
+    return this.http.post(this.url + '/validate', JSON.stringify(grokValidation),
+      new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public list(): Observable<string[]> {
+    return this.http.get(this.url + '/list', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public getStatement(path: string): Observable<string> {
+    let params: URLSearchParams = new URLSearchParams();
+    params.set('path', path);
+    return this.http.get(this.url + '/get/statement', new RequestOptions({headers: new Headers(this.defaultHeaders), search: params}))
+        .map(HttpUtil.extractString)
+        .catch(HttpUtil.handleError);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/hdfs.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/hdfs.service.spec.ts b/metron-interface/metron-config/src/app/service/hdfs.service.spec.ts
new file mode 100644
index 0000000..16196ab
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/hdfs.service.spec.ts
@@ -0,0 +1,110 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+import {HdfsService} from './hdfs.service';
+
+describe('HdfsService', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpModule],
+      providers: [
+        HdfsService,
+        {provide: XHRBackend, useClass: MockBackend},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    })
+      .compileComponents();
+  }));
+
+  it('can instantiate service when inject service',
+    inject([HdfsService], (service: HdfsService) => {
+      expect(service instanceof HdfsService).toBe(true);
+    }));
+
+  it('can instantiate service with "new"', inject([Http, APP_CONFIG], (http: Http, config: IAppConfig) => {
+    expect(http).not.toBeNull('http should be provided');
+    let service = new HdfsService(http, config);
+    expect(service instanceof HdfsService).toBe(true, 'new service should be ok');
+  }));
+
+
+  it('can provide the mockBackend as XHRBackend',
+    inject([XHRBackend], (backend: MockBackend) => {
+      expect(backend).not.toBeNull('backend should be provided');
+    }));
+
+  describe('when service functions', () => {
+    let hdfsService: HdfsService;
+    let mockBackend: MockBackend;
+    let fileList = ['file1', 'file2'];
+    let contents = 'file contents';
+    let listResponse: Response;
+    let readResponse: Response;
+    let postResponse: Response;
+    let deleteResponse: Response;
+
+    beforeEach(inject([Http, XHRBackend, APP_CONFIG], (http: Http, be: MockBackend, config: IAppConfig) => {
+      mockBackend = be;
+      hdfsService = new HdfsService(http, config);
+      listResponse = new Response(new ResponseOptions({status: 200, body: fileList}));
+      readResponse = new Response(new ResponseOptions({status: 200, body: contents}));
+      postResponse = new Response(new ResponseOptions({status: 200}));
+      deleteResponse = new Response(new ResponseOptions({status: 200}));
+    }));
+
+    it('list', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(listResponse));
+      hdfsService.list('/path').subscribe(
+        result => {
+          expect(result).toEqual(fileList);
+        }, error => console.log(error));
+    })));
+
+    it('read', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(readResponse));
+      hdfsService.read('/path').subscribe(
+        result => {
+          expect(result).toEqual(contents);
+        }, error => console.log(error));
+    })));
+
+    it('post', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(postResponse));
+      hdfsService.post('/path', contents).subscribe(
+          result => {
+            expect(result.status).toEqual(200);
+          }, error => console.log(error));
+    })));
+
+    it('deleteFile', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(deleteResponse));
+      hdfsService.deleteFile('/path').subscribe(
+          result => {
+            expect(result.status).toEqual(200);
+          }, error => console.log(error));
+    })));
+  });
+
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/hdfs.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/hdfs.service.ts b/metron-interface/metron-config/src/app/service/hdfs.service.ts
new file mode 100644
index 0000000..4e4b808
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/hdfs.service.ts
@@ -0,0 +1,63 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, Inject} from '@angular/core';
+import {Http, Headers, RequestOptions, Response, URLSearchParams} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {HttpUtil} from '../util/httpUtil';
+import {IAppConfig} from '../app.config.interface';
+import {APP_CONFIG} from '../app.config';
+
+@Injectable()
+export class HdfsService {
+  url = this.config.apiEndpoint + '/hdfs';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+
+  constructor(private http: Http, @Inject(APP_CONFIG) private config: IAppConfig) {
+  }
+
+  public list(path: string): Observable<string[]> {
+    let params: URLSearchParams = new URLSearchParams();
+    params.set('path', path);
+    return this.http.get(this.url + '/list', new RequestOptions({headers: new Headers(this.defaultHeaders), search: params}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public read(path: string): Observable<string> {
+    let params: URLSearchParams = new URLSearchParams();
+    params.set('path', path);
+    return this.http.get(this.url , new RequestOptions({headers: new Headers(this.defaultHeaders), search: params}))
+      .map(HttpUtil.extractString)
+      .catch(HttpUtil.handleError);
+  }
+
+  public post(path: string, contents: string): Observable<Response> {
+    let params: URLSearchParams = new URLSearchParams();
+    params.set('path', path);
+    return this.http.post(this.url, contents, new RequestOptions({headers: new Headers(this.defaultHeaders), search: params}))
+        .catch(HttpUtil.handleError);
+  }
+
+  public deleteFile(path: string): Observable<Response> {
+    let params: URLSearchParams = new URLSearchParams();
+    params.set('path', path);
+    return this.http.delete(this.url, new RequestOptions({headers: new Headers(this.defaultHeaders), search: params}))
+        .catch(HttpUtil.handleError);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/kafka.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/kafka.service.spec.ts b/metron-interface/metron-config/src/app/service/kafka.service.spec.ts
new file mode 100644
index 0000000..e6f1d7f
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/kafka.service.spec.ts
@@ -0,0 +1,114 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {KafkaService} from './kafka.service';
+import {KafkaTopic} from '../model/kafka-topic';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+
+describe('KafkaService', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpModule],
+      providers: [
+        KafkaService,
+        {provide: XHRBackend, useClass: MockBackend},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    })
+      .compileComponents();
+  }));
+
+  it('can instantiate service when inject service',
+    inject([KafkaService], (service: KafkaService) => {
+      expect(service instanceof KafkaService).toBe(true);
+    }));
+
+  it('can instantiate service with "new"', inject([Http, APP_CONFIG], (http: Http, config: IAppConfig) => {
+    expect(http).not.toBeNull('http should be provided');
+    let service = new KafkaService(http, config);
+    expect(service instanceof KafkaService).toBe(true, 'new service should be ok');
+  }));
+
+  it('can provide the mockBackend as XHRBackend',
+    inject([XHRBackend], (backend: MockBackend) => {
+      expect(backend).not.toBeNull('backend should be provided');
+    }));
+
+  describe('when service functions', () => {
+    let kafkaService: KafkaService;
+    let mockBackend: MockBackend;
+    let kafkaTopic = new KafkaTopic();
+    kafkaTopic.name = 'bro';
+    kafkaTopic.numPartitions = 1;
+    kafkaTopic.replicationFactor = 1;
+    let sampleMessage = 'sample message';
+    let kafkaResponse: Response;
+    let kafkaListResponse: Response;
+    let sampleMessageResponse: Response;
+
+    beforeEach(inject([Http, XHRBackend, APP_CONFIG], (http: Http, be: MockBackend, config: IAppConfig) => {
+      mockBackend = be;
+      kafkaService = new KafkaService(http, config);
+      kafkaResponse = new Response(new ResponseOptions({status: 200, body: kafkaTopic}));
+      kafkaListResponse = new Response(new ResponseOptions({status: 200, body: [kafkaTopic]}));
+      sampleMessageResponse = new Response(new ResponseOptions({status: 200, body: sampleMessage}));
+    }));
+
+    it('post', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(kafkaResponse));
+
+      kafkaService.post(kafkaTopic).subscribe(
+        result => {
+          expect(result).toEqual(kafkaTopic);
+        }, error => console.log(error));
+    })));
+
+    it('get', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(kafkaResponse));
+
+      kafkaService.get('bro').subscribe(
+        result => {
+          expect(result).toEqual(kafkaTopic);
+        }, error => console.log(error));
+    })));
+
+    it('list', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(kafkaListResponse));
+
+      kafkaService.list().subscribe(
+        result => {
+          expect(result).toEqual([kafkaTopic]);
+        }, error => console.log(error));
+    })));
+
+    it('sample', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sampleMessageResponse));
+      kafkaService.sample('bro').subscribe(
+        result => {
+          expect(result).toEqual(sampleMessage);
+        }, error => console.log(error));
+    })));
+  });
+
+});
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/kafka.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/kafka.service.ts b/metron-interface/metron-config/src/app/service/kafka.service.ts
new file mode 100644
index 0000000..ac02366
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/kafka.service.ts
@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, Inject} from '@angular/core';
+import {Http, Headers, RequestOptions} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {KafkaTopic} from '../model/kafka-topic';
+import {HttpUtil} from '../util/httpUtil';
+import {IAppConfig} from '../app.config.interface';
+import {APP_CONFIG} from '../app.config';
+
+@Injectable()
+export class KafkaService {
+  url = this.config.apiEndpoint + '/kafka/topic';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+
+  constructor(private http: Http, @Inject(APP_CONFIG) private config: IAppConfig) {
+
+  }
+
+  public post(kafkaTopic: KafkaTopic): Observable<KafkaTopic> {
+    return this.http.post(this.url, JSON.stringify(kafkaTopic), new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public get(name: string): Observable<KafkaTopic> {
+    return this.http.get(this.url + '/' + name, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public list(): Observable<string[]> {
+    return this.http.get(this.url, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public sample(name: string): Observable<string> {
+    return this.http.get(this.url + '/' + name + '/sample', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractString)
+      .catch(HttpUtil.handleError);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.spec.ts b/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.spec.ts
new file mode 100644
index 0000000..89863ee
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.spec.ts
@@ -0,0 +1,144 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {SensorEnrichmentConfigService} from './sensor-enrichment-config.service';
+import {SensorEnrichmentConfig, EnrichmentConfig} from '../model/sensor-enrichment-config';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {METRON_REST_CONFIG, APP_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+
+describe('SensorEnrichmentConfigService', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpModule],
+      providers: [
+        SensorEnrichmentConfigService,
+        {provide: XHRBackend, useClass: MockBackend},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    })
+        .compileComponents();
+  }));
+
+  it('can instantiate service when inject service',
+      inject([SensorEnrichmentConfigService], (service: SensorEnrichmentConfigService) => {
+        expect(service instanceof SensorEnrichmentConfigService).toBe(true);
+      }));
+
+  it('can instantiate service with "new"', inject([Http, APP_CONFIG], (http: Http, config: IAppConfig) => {
+    expect(http).not.toBeNull('http should be provided');
+    let service = new SensorEnrichmentConfigService(http, config);
+    expect(service instanceof SensorEnrichmentConfigService).toBe(true, 'new service should be ok');
+  }));
+
+
+  it('can provide the mockBackend as XHRBackend',
+      inject([XHRBackend], (backend: MockBackend) => {
+        expect(backend).not.toBeNull('backend should be provided');
+      }));
+
+  describe('when service functions', () => {
+    let sensorEnrichmentConfigService: SensorEnrichmentConfigService;
+    let mockBackend: MockBackend;
+    let sensorEnrichmentConfig1 = new SensorEnrichmentConfig();
+    let enrichmentConfig1 = new EnrichmentConfig();
+    enrichmentConfig1.fieldMap = {'geo': ['ip_dst_addr'], 'host': ['ip_dst_addr']};
+    sensorEnrichmentConfig1.enrichment.fieldMap = enrichmentConfig1;
+    let sensorEnrichmentConfig2 = new SensorEnrichmentConfig();
+    let enrichmentConfig2 = new EnrichmentConfig();
+    enrichmentConfig1.fieldMap = {'whois': ['ip_dst_addr'], 'host': ['ip_src_addr']};
+    sensorEnrichmentConfig2.enrichment = enrichmentConfig2;
+    let availableEnrichments: string[] = ['geo', 'host', 'whois'];
+    let availableThreatTriageAggregators: string[] = ['MAX', 'MIN', 'SUM', 'MEAN', 'POSITIVE_MEAN'];
+    let sensorEnrichmentConfigResponse: Response;
+    let sensorEnrichmentConfigsResponse: Response;
+    let availableEnrichmentsResponse: Response;
+    let availableThreatTriageAggregatorsResponse: Response;
+    let deleteResponse: Response;
+
+    beforeEach(inject([Http, XHRBackend, APP_CONFIG], (http: Http, be: MockBackend, config: IAppConfig) => {
+      mockBackend = be;
+      sensorEnrichmentConfigService = new SensorEnrichmentConfigService(http, config);
+      sensorEnrichmentConfigResponse = new Response(new ResponseOptions({status: 200, body: sensorEnrichmentConfig1}));
+      sensorEnrichmentConfigsResponse = new Response(new ResponseOptions({status: 200, body: [sensorEnrichmentConfig1,
+        sensorEnrichmentConfig2]}));
+      availableEnrichmentsResponse = new Response(new ResponseOptions({status: 200, body: availableEnrichments}));
+      availableThreatTriageAggregatorsResponse = new Response(new ResponseOptions({status: 200, body: availableThreatTriageAggregators}));
+      deleteResponse = new Response(new ResponseOptions({status: 200}));
+    }));
+
+    it('post', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sensorEnrichmentConfigResponse));
+
+      sensorEnrichmentConfigService.post('bro', sensorEnrichmentConfig1).subscribe(
+          result => {
+            expect(result).toEqual(sensorEnrichmentConfig1);
+          }, error => console.log(error));
+    })));
+
+    it('get', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sensorEnrichmentConfigResponse));
+
+      sensorEnrichmentConfigService.get('bro').subscribe(
+          result => {
+            expect(result).toEqual(sensorEnrichmentConfig1);
+          }, error => console.log(error));
+    })));
+
+    it('getAll', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sensorEnrichmentConfigsResponse));
+
+      sensorEnrichmentConfigService.getAll().subscribe(
+          results => {
+            expect(results).toEqual([sensorEnrichmentConfig1, sensorEnrichmentConfig2]);
+          }, error => console.log(error));
+    })));
+
+    it('getAvailableEnrichments', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(availableEnrichmentsResponse));
+
+      sensorEnrichmentConfigService.getAvailableEnrichments().subscribe(
+          results => {
+            expect(results).toEqual(availableEnrichments);
+          }, error => console.log(error));
+    })));
+
+    it('getAvailableThreatTriageAggregators', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(availableThreatTriageAggregatorsResponse));
+
+      sensorEnrichmentConfigService.getAvailableThreatTriageAggregators().subscribe(
+          results => {
+            expect(results).toEqual(availableThreatTriageAggregators);
+          }, error => console.log(error));
+    })));
+
+    it('deleteSensorEnrichments', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(deleteResponse));
+
+      sensorEnrichmentConfigService.deleteSensorEnrichments('bro').subscribe(result => {
+        expect(result.status).toEqual(200);
+      });
+    })));
+  });
+
+});
+
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.ts b/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.ts
new file mode 100644
index 0000000..90c314b
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.ts
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, Inject} from '@angular/core';
+import {Http, Headers, RequestOptions, Response} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {SensorEnrichmentConfig} from '../model/sensor-enrichment-config';
+import {HttpUtil} from '../util/httpUtil';
+import {IAppConfig} from '../app.config.interface';
+import {APP_CONFIG} from '../app.config';
+
+@Injectable()
+export class SensorEnrichmentConfigService {
+  url = this.config.apiEndpoint + '/sensor/enrichment/config';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+
+  constructor(private http: Http, @Inject(APP_CONFIG) private config: IAppConfig) {
+  }
+
+  public post(name: string, sensorEnrichmentConfig: SensorEnrichmentConfig): Observable<SensorEnrichmentConfig> {
+    return this.http.post(this.url + '/' + name, JSON.stringify(sensorEnrichmentConfig),
+                          new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public get(name: string): Observable<SensorEnrichmentConfig> {
+    return this.http.get(this.url + '/' + name, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public getAll(): Observable<SensorEnrichmentConfig[]> {
+    return this.http.get(this.url, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public deleteSensorEnrichments(name: string): Observable<Response> {
+    return this.http.delete(this.url + '/' + name, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .catch(HttpUtil.handleError);
+  }
+
+  public getAvailableEnrichments(): Observable<string[]> {
+    return this.http.get(this.url + '/list/available/enrichments', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public getAvailableThreatTriageAggregators(): Observable<string[]> {
+    return this.http.get(this.url + '/list/available/threat/triage/aggregators',
+        new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.spec.ts b/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.spec.ts
new file mode 100644
index 0000000..3640162
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.spec.ts
@@ -0,0 +1,118 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {METRON_REST_CONFIG, APP_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+import {SensorIndexingConfigService} from './sensor-indexing-config.service';
+import {IndexingConfigurations} from '../model/sensor-indexing-config';
+
+describe('SensorIndexingConfigService', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpModule],
+      providers: [
+        SensorIndexingConfigService,
+        {provide: XHRBackend, useClass: MockBackend},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    })
+        .compileComponents();
+  }));
+
+  it('can instantiate service when inject service',
+      inject([SensorIndexingConfigService], (service: SensorIndexingConfigService) => {
+        expect(service instanceof SensorIndexingConfigService).toBe(true);
+      }));
+
+  it('can instantiate service with "new"', inject([Http, APP_CONFIG], (http: Http, config: IAppConfig) => {
+    expect(http).not.toBeNull('http should be provided');
+    let service = new SensorIndexingConfigService(http, config);
+    expect(service instanceof SensorIndexingConfigService).toBe(true, 'new service should be ok');
+  }));
+
+
+  it('can provide the mockBackend as XHRBackend',
+      inject([XHRBackend], (backend: MockBackend) => {
+        expect(backend).not.toBeNull('backend should be provided');
+      }));
+
+  describe('when service functions', () => {
+    let sensorIndexingConfigService: SensorIndexingConfigService;
+    let mockBackend: MockBackend;
+    let sensorIndexingConfig1 = new IndexingConfigurations();
+    sensorIndexingConfig1.hdfs.index = 'squid';
+    sensorIndexingConfig1.hdfs.batchSize = 1;
+    let sensorIndexingConfig2 = new IndexingConfigurations();
+    sensorIndexingConfig2.hdfs.index = 'yaf';
+    sensorIndexingConfig2.hdfs.batchSize = 2;
+    let sensorIndexingConfigResponse: Response;
+    let sensorIndexingConfigsResponse: Response;
+    let deleteResponse: Response;
+
+    beforeEach(inject([Http, XHRBackend, APP_CONFIG], (http: Http, be: MockBackend, config: IAppConfig) => {
+      mockBackend = be;
+      sensorIndexingConfigService = new SensorIndexingConfigService(http, config);
+      sensorIndexingConfigResponse = new Response(new ResponseOptions({status: 200, body: sensorIndexingConfig1}));
+      sensorIndexingConfigsResponse = new Response(new ResponseOptions({status: 200, body: [sensorIndexingConfig1,
+        sensorIndexingConfig2]}));
+      deleteResponse = new Response(new ResponseOptions({status: 200}));
+    }));
+
+    it('post', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sensorIndexingConfigResponse));
+
+      sensorIndexingConfigService.post('squid', sensorIndexingConfig1).subscribe(
+          result => {
+            expect(result).toEqual(sensorIndexingConfig1);
+          }, error => console.log(error));
+    })));
+
+    it('get', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sensorIndexingConfigResponse));
+
+      sensorIndexingConfigService.get('squid').subscribe(
+          result => {
+            expect(result).toEqual(sensorIndexingConfig1);
+          }, error => console.log(error));
+    })));
+
+    it('getAll', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sensorIndexingConfigsResponse));
+
+      sensorIndexingConfigService.getAll().subscribe(
+          results => {
+            expect(results).toEqual([sensorIndexingConfig1, sensorIndexingConfig2]);
+          }, error => console.log(error));
+    })));
+
+    it('deleteSensorEnrichments', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(deleteResponse));
+
+      sensorIndexingConfigService.deleteSensorIndexingConfig('squid').subscribe(result => {
+        expect(result.status).toEqual(200);
+      });
+    })));
+  });
+
+});
+
+