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:17 UTC

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

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 {}