You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@metron.apache.org by sa...@apache.org on 2019/07/15 11:41:30 UTC

[metron] branch feature/METRON-1856-parser-aggregation updated: METRON-2115 [UI] Aligning UI to the parser aggregation API (tiborm via sardell) closes apache/metron#1414

This is an automated email from the ASF dual-hosted git repository.

sardell pushed a commit to branch feature/METRON-1856-parser-aggregation
in repository https://gitbox.apache.org/repos/asf/metron.git


The following commit(s) were added to refs/heads/feature/METRON-1856-parser-aggregation by this push:
     new 130f97f  METRON-2115 [UI] Aligning UI to the parser aggregation API (tiborm via sardell) closes apache/metron#1414
130f97f is described below

commit 130f97f39e7b9e223e8a431ceec1b45096bd38fa
Author: tiborm <ti...@gmail.com>
AuthorDate: Mon Jul 15 13:40:42 2019 +0200

    METRON-2115 [UI] Aligning UI to the parser aggregation API (tiborm via sardell) closes apache/metron#1414
---
 ...sensor-parser-config-readonly.component.spec.ts |   4 +-
 .../sensor-parser-config-readonly.component.ts     |   2 +-
 .../sensor-parser-config-history.service.spec.ts   |   8 +-
 .../service/sensor-parser-config.service.spec.ts   | 374 ++++++++++++++++++++-
 .../app/service/sensor-parser-config.service.ts    | 172 ++++++++--
 5 files changed, 513 insertions(+), 47 deletions(-)

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
index 16628d1..4ebfb6d 100644
--- 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
@@ -645,7 +645,7 @@ describe('Component: SensorParserConfigReadonly', () => {
   it('onDeleteSensor should delete the sensor', async(() => {
     spyOn(
       sensorParserConfigService,
-      'deleteSensorParserConfig'
+      'deleteConfig'
     ).and.returnValue(
       Observable.create(observer => {
         observer.next({});
@@ -662,7 +662,7 @@ describe('Component: SensorParserConfigReadonly', () => {
     component.onDeleteSensor();
 
     expect(
-      sensorParserConfigService.deleteSensorParserConfig
+      sensorParserConfigService.deleteConfig
     ).toHaveBeenCalledWith('abc');
     expect(alerts.showSuccessMessage).toHaveBeenCalledWith(
       'Deleted sensor abc'
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
index bbc04f4..04c8c69 100644
--- 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
@@ -444,7 +444,7 @@ export class SensorParserConfigReadonlyComponent implements OnInit {
     this.toggleStartStopInProgress();
 
     let name = this.selectedSensorName;
-    this.sensorParserConfigService.deleteSensorParserConfig(name).subscribe(
+    this.sensorParserConfigService.deleteConfig(name).subscribe(
       result => {
         this.metronAlerts.showSuccessMessage('Deleted sensor ' + name);
         this.toggleStartStopInProgress();
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
index 4816ae5..ae7de24 100644
--- 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
@@ -16,15 +16,15 @@
  * limitations under the License.
  */
 import { TestBed } from '@angular/core/testing';
-import { SensorParserConfig } from '../model/sensor-parser-config';
+import { ParserConfigModel } from '../sensors/models/parser-config.model';
 import { SensorParserConfigHistoryService } from './sensor-parser-config-history.service';
 import { SensorParserConfigHistory } from '../model/sensor-parser-config-history';
 import {
   HttpTestingController,
   HttpClientTestingModule
 } from '@angular/common/http/testing';
-import {AppConfigService} from './app-config.service';
-import {MockAppConfigService} from './mock.app-config.service';
+import { AppConfigService } from './app-config.service';
+import { MockAppConfigService } from './mock.app-config.service';
 
 describe('SensorParserConfigHistoryService', () => {
   let mockBackend: HttpTestingController;
@@ -50,7 +50,7 @@ describe('SensorParserConfigHistoryService', () => {
 
   describe('when service functions', () => {
     let sensorParserConfigHistory = new SensorParserConfigHistory();
-    sensorParserConfigHistory.config = new SensorParserConfig();
+    sensorParserConfigHistory.config = new ParserConfigModel('TestConfigId01');
 
     it('get', () => {
       sensorParserConfigHistoryService.get('bro').subscribe(
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
index 6e63113..b138017 100644
--- 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
@@ -67,7 +67,7 @@ describe('SensorParserConfigService', () => {
 
   it('post', () => {
     sensorParserConfigService
-      .post('bro', sensorParserConfig)
+      .saveConfig('bro', sensorParserConfig)
       .subscribe(result => {
         expect(result).toEqual(sensorParserConfig);
       });
@@ -78,7 +78,7 @@ describe('SensorParserConfigService', () => {
   });
 
   it('get', () => {
-    sensorParserConfigService.get('bro').subscribe(result => {
+    sensorParserConfigService.getConfig('bro').subscribe(result => {
       expect(result).toEqual(sensorParserConfig);
     });
     const req = mockBackend.expectOne('/api/v1/sensor/parser/config/bro');
@@ -87,7 +87,7 @@ describe('SensorParserConfigService', () => {
   });
 
   it('getAll', () => {
-    sensorParserConfigService.getAll().subscribe(results => {
+    sensorParserConfigService.getAllConfig().subscribe(results => {
       expect(results).toEqual([sensorParserConfig]);
     });
     const req = mockBackend.expectOne('/api/v1/sensor/parser/config');
@@ -122,7 +122,7 @@ describe('SensorParserConfigService', () => {
   it('deleteSensorParserConfigs', () => {
     let req = [];
     sensorParserConfigService
-      .deleteSensorParserConfigs(['bro1', 'bro2'])
+      .deleteConfigs(['bro1', 'bro2'])
       .subscribe(result => {
         expect(result.success.length).toEqual(2);
       });
@@ -133,4 +133,370 @@ describe('SensorParserConfigService', () => {
       r.flush(parsedMessage);
     });
   });
+
+  describe('REST Calls for Parser Grouping', () => {
+
+    it('getting list of parser groups', () => {
+      sensorParserConfigService.getAllGroups().subscribe((result: ParserGroupModel[]) => {
+        expect(result.length).toBe(2);
+        expect(result[0].name).toBe('TestGroupName1');
+        expect(result[0].description).toBe('TestDesc1');
+      });
+
+      const request = mockBackend.expectOne('/api/v1/sensor/parser/group');
+      request.flush([
+        {
+          name: 'TestGroupName1',
+          description: 'TestDesc1'
+        },
+        {
+          name: 'TestGroupName2',
+          description: 'TestDesc2'
+        }
+      ]);
+    });
+
+    it('getting single parser group by name', () => {
+      sensorParserConfigService.getGroup('TestGroup').subscribe((result: ParserGroupModel) => {
+        expect(result.name).toBe('TestGroupName1');
+        expect(result.description).toBe('TestDesc1');
+      });
+
+      const request = mockBackend.expectOne('/api/v1/sensor/parser/group/TestGroup');
+      request.flush({
+          name: 'TestGroupName1',
+          description: 'TestDesc1'
+        });
+    });
+
+    it('creating/editing single parser group by name', () => {
+      sensorParserConfigService.saveGroup('TestGroup', new ParserGroupModel({
+        name: 'TestGroupName1',
+        description: 'TestDesc1',
+        sensors: ['foo']
+      })).subscribe();
+
+      const request = mockBackend.expectOne('/api/v1/sensor/parser/group');
+      expect(request.request.method).toEqual('POST');
+      expect(request.request.body.name).toBe('TestGroupName1');
+      expect(request.request.body.description).toBe('TestDesc1');
+      expect(request.request.body.sensors).toEqual(['foo']);
+    });
+
+    it('deleting single parser group by name', () => {
+      sensorParserConfigService.deleteGroup('TestGroup').subscribe();
+
+      const request = mockBackend.expectOne('/api/v1/sensor/parser/group/TestGroup');
+      expect(request.request.method).toEqual('DELETE');
+    });
+
+    it('deleting multiple parser groups by name', () => {
+      sensorParserConfigService.deleteGroups(['TestGroup1', 'TestGroup2', 'TestGroup3'])
+      .subscribe((result) => {
+        expect(result.success.length).toBe(2);
+        expect(result.failure.length).toBe(1);
+      });
+
+      const request: Array<TestRequest> = [];
+      request.push(mockBackend.expectOne('/api/v1/sensor/parser/group/TestGroup1'));
+      request.push(mockBackend.expectOne('/api/v1/sensor/parser/group/TestGroup2'));
+      request.push(mockBackend.expectOne('/api/v1/sensor/parser/group/TestGroup3'));
+
+      expect(request[0].request.method).toEqual('DELETE');
+      expect(request[1].request.method).toEqual('DELETE');
+      expect(request[2].request.method).toEqual('DELETE');
+
+      request[0].flush({});
+      request[1].flush('Invalid request parameters', { status: 404, statusText: 'Bad Request' });
+      request[2].flush({});
+    });
+
+    function getTestGroups() {
+      return [
+        { config: new ParserGroupModel({ name: 'TestGroup01', description: '' }) },
+        { config: new ParserGroupModel({ name: 'TestGroup02', description: '' }) },
+        { config: new ParserGroupModel({ name: 'TestGroup03', description: '' }) },
+        { config: new ParserGroupModel({ name: 'TestGroup04', description: '' }) },
+      ];
+    }
+
+    function getTestConfigs() {
+      return [
+        { config: new ParserConfigModel('Parser_Config_ID_01', { sensorTopic: 'Kafka/Sensor Topic ID 01' }) },
+        { config: new ParserConfigModel('Parser_Config_ID_02', { sensorTopic: 'Kafka/Sensor Topic ID 02' }) },
+        { config: new ParserConfigModel('Parser_Config_ID_03', { sensorTopic: 'Kafka/Sensor Topic ID 03' }) },
+        { config: new ParserConfigModel('Parser_Config_ID_04', { sensorTopic: 'Kafka/Sensor Topic ID 04' }) },
+      ];
+    }
+
+    function markElementOnIndexAs(testData: ParserMetaInfoModel[], indexes: number[], flag: string) {
+      indexes.forEach((index) => {
+        testData[index][flag] = true;
+      })
+    }
+
+    class DirtyFlags {
+      static NEW = 'isPhantom';
+      static CHANGED = 'isDirty';
+      static DELETED = 'isDeleted';
+    }
+
+    it('syncronizing list of parser GROUPS with the backend - SINGLE DELETE', () => {
+      const testData = getTestGroups();
+
+      markElementOnIndexAs(testData, [1], DirtyFlags.DELETED);
+
+      sensorParserConfigService.syncGroups(testData).subscribe();
+
+      const request = mockBackend.expectOne('/api/v1/sensor/parser/group/TestGroup02');
+      expect(request.request.method).toEqual('DELETE');
+    });
+
+    it('syncronizing list of parser GROUPS with the backend - MULTIPLE DELETE', () => {
+      const testData = getTestGroups();
+
+      markElementOnIndexAs(testData, [0, 2, 3], DirtyFlags.DELETED);
+
+      sensorParserConfigService.syncGroups(testData).subscribe();
+
+      const requests = [];
+      requests.push(mockBackend.expectOne('/api/v1/sensor/parser/group/TestGroup01'));
+      requests.push(mockBackend.expectOne('/api/v1/sensor/parser/group/TestGroup04'));
+      requests.push(mockBackend.expectOne('/api/v1/sensor/parser/group/TestGroup03'));
+      expect(requests[0].request.method).toEqual('DELETE');
+      expect(requests[1].request.method).toEqual('DELETE');
+      expect(requests[2].request.method).toEqual('DELETE');
+    });
+
+    it('syncronizing list of parser GROUPS with the backend - SINGLE NEW', () => {
+      const testData = getTestGroups();
+
+      markElementOnIndexAs(testData, [0], DirtyFlags.NEW);
+
+      sensorParserConfigService.syncGroups(testData).subscribe();
+
+      const request = mockBackend.expectOne('/api/v1/sensor/parser/group');
+      expect(request.request.method).toEqual('POST');
+      expect(request.request.body.name).toEqual('TestGroup01');
+      expect(request.request.body.description).toEqual('');
+    });
+
+    it('syncronizing list of parser GROUPS with the backend - MULTIPLE NEW', () => {
+      const testData = getTestGroups();
+
+      markElementOnIndexAs(testData, [0, 2], DirtyFlags.NEW);
+
+      sensorParserConfigService.syncGroups(testData).subscribe();
+
+      const calls = mockBackend.match((request) => {
+        return request.url.match(/\/api\/v1\/sensor\/parser\/group/)
+          && request.method === 'POST';
+      });
+
+      expect(calls.length).toBe(2);
+      expect(calls[0].request.body.name).toEqual('TestGroup01');
+      expect(calls[1].request.body.name).toEqual('TestGroup03');
+    });
+
+    it('syncronizing list of parser GROUPS with the backend - SINGLE CHANGED', () => {
+      const testData = getTestGroups();
+
+      markElementOnIndexAs(testData, [3], DirtyFlags.CHANGED);
+
+      sensorParserConfigService.syncGroups(testData).subscribe();
+
+      const request = mockBackend.expectOne('/api/v1/sensor/parser/group');
+      expect(request.request.method).toEqual('POST');
+      expect(request.request.body.name).toEqual('TestGroup04');
+      expect(request.request.body.description).toEqual('');
+    });
+
+    it('syncronizing list of parser GROUPS with the backend - MULTIPLE CHANGED', () => {
+      const testData = getTestGroups();
+
+      markElementOnIndexAs(testData, [0, 2], DirtyFlags.CHANGED);
+
+      sensorParserConfigService.syncGroups(testData).subscribe();
+
+      const calls = mockBackend.match(request => {
+        return request.url.match(/\/api\/v1\/sensor\/parser\/group/)
+          && request.method === 'POST';
+      });
+
+      expect(calls.length).toBe(2);
+      expect(calls[0].request.body.name).toEqual('TestGroup01');
+      expect(calls[1].request.body.name).toEqual('TestGroup03');
+    });
+
+    it('syncronizing list of PARSER CONFIGS with the backend - SINGLE DELETE', () => {
+      const testData = getTestConfigs();
+
+      markElementOnIndexAs(testData, [1], DirtyFlags.DELETED);
+
+      sensorParserConfigService.syncConfigs(testData).subscribe();
+
+      const request = mockBackend.expectOne('/api/v1/sensor/parser/config/Parser_Config_ID_02');
+      expect(request.request.method).toEqual('DELETE');
+    });
+
+    it('syncronizing list of PARSER CONFIGS with the backend - MULTIPLE DELETE', () => {
+      const testData = getTestConfigs();
+
+      markElementOnIndexAs(testData, [0, 2, 3], DirtyFlags.DELETED);
+
+      sensorParserConfigService.syncConfigs(testData).subscribe();
+
+      const requests = [];
+      requests.push(mockBackend.expectOne('/api/v1/sensor/parser/config/Parser_Config_ID_01'));
+      requests.push(mockBackend.expectOne('/api/v1/sensor/parser/config/Parser_Config_ID_03'));
+      requests.push(mockBackend.expectOne('/api/v1/sensor/parser/config/Parser_Config_ID_04'));
+      expect(requests[0].request.method).toEqual('DELETE');
+      expect(requests[1].request.method).toEqual('DELETE');
+      expect(requests[2].request.method).toEqual('DELETE');
+    });
+
+    it('syncronizing list of PARSER CONFIGS with the backend - SINGLE NEW', () => {
+      const testData = getTestConfigs();
+
+      markElementOnIndexAs(testData, [0], DirtyFlags.NEW);
+
+      sensorParserConfigService.syncConfigs(testData).subscribe();
+
+      const request = mockBackend.expectOne('/api/v1/sensor/parser/config/Parser_Config_ID_01');
+      expect(request.request.method).toEqual('POST');
+      expect(JSON.parse(request.request.body).sensorTopic).toEqual('Kafka/Sensor Topic ID 01');
+    });
+
+    it('syncronizing list of PARSER CONFIGS with the backend - MULTIPLE NEW', () => {
+      const testData = getTestConfigs();
+
+      markElementOnIndexAs(testData, [0, 2], DirtyFlags.NEW);
+
+      sensorParserConfigService.syncConfigs(testData).subscribe();
+
+      const requests = [];
+      requests.push(mockBackend.expectOne('/api/v1/sensor/parser/config/Parser_Config_ID_01'));
+      requests.push(mockBackend.expectOne('/api/v1/sensor/parser/config/Parser_Config_ID_03'));
+      expect(requests[0].request.method).toEqual('POST');
+      expect(requests[1].request.method).toEqual('POST');
+
+      expect(JSON.parse(requests[0].request.body).sensorTopic).toEqual('Kafka/Sensor Topic ID 01');
+      expect(JSON.parse(requests[1].request.body).sensorTopic).toEqual('Kafka/Sensor Topic ID 03');
+    });
+
+    it('syncronizing list of PARSER CONFIGS with the backend - SINGLE CHANGED', () => {
+      const testData = getTestConfigs();
+
+      markElementOnIndexAs(testData, [3], DirtyFlags.CHANGED);
+
+      sensorParserConfigService.syncConfigs(testData).subscribe();
+
+      const request = mockBackend.expectOne('/api/v1/sensor/parser/config/Parser_Config_ID_04');
+      expect(request.request.method).toEqual('POST');
+      expect(JSON.parse(request.request.body).sensorTopic).toEqual('Kafka/Sensor Topic ID 04');
+    });
+
+    it('syncronizing list of PARSER CONFIGS with the backend - MULTIPLE CHANGED', () => {
+      const testData = getTestConfigs();
+
+      markElementOnIndexAs(testData, [0, 2], DirtyFlags.CHANGED);
+
+      sensorParserConfigService.syncConfigs(testData).subscribe();
+
+      const requests = [];
+      requests.push(mockBackend.expectOne('/api/v1/sensor/parser/config/Parser_Config_ID_01'));
+      requests.push(mockBackend.expectOne('/api/v1/sensor/parser/config/Parser_Config_ID_03'));
+      expect(requests[0].request.method).toEqual('POST');
+      expect(requests[1].request.method).toEqual('POST');
+
+      expect(JSON.parse(requests[0].request.body).sensorTopic).toEqual('Kafka/Sensor Topic ID 01');
+      expect(JSON.parse(requests[1].request.body).sensorTopic).toEqual('Kafka/Sensor Topic ID 03');
+    });
+
+    it('syncronization of PARSER CONFIGS should return with an Observable array of successful/unsuccessful requests', () => {
+      const testData = getTestConfigs();
+
+      markElementOnIndexAs(testData, [0, 2], DirtyFlags.CHANGED);
+
+      sensorParserConfigService.syncConfigs(testData)
+        .subscribe((syncResults: any[]) => {
+          expect(syncResults.length === 2);
+        });
+
+        const requests = [];
+        requests.push(mockBackend.expectOne('/api/v1/sensor/parser/config/Parser_Config_ID_01'));
+        requests.push(mockBackend.expectOne('/api/v1/sensor/parser/config/Parser_Config_ID_03'));
+        requests[0].flush(requests[0].request.body);
+        requests[1].flush(requests[1].request.body);
+    });
+
+    it('error throwing in syncConfigs()', () => {
+      const testData = getTestConfigs();
+
+      markElementOnIndexAs(testData, [2], DirtyFlags.CHANGED);
+
+      sensorParserConfigService.syncConfigs(testData)
+        .subscribe(
+          noop,
+          (error) => {
+            expect(error).toBeDefined();
+          }
+        );
+
+        const request = mockBackend.expectOne('/api/v1/sensor/parser/config/Parser_Config_ID_03');
+        request.flush('Invalid request parameters', { status: 404, statusText: 'Bad Request' });
+    });
+
+    it('error throwing in syncConfigs()', () => {
+      const testData = getTestGroups();
+
+      markElementOnIndexAs(testData, [1], DirtyFlags.CHANGED);
+
+      sensorParserConfigService.syncGroups(testData)
+        .subscribe(
+          noop,
+          (error) => {
+            expect(error).toBeDefined();
+          }
+        );
+
+        const request = mockBackend.expectOne('/api/v1/sensor/parser/group');
+        request.flush('Invalid request parameters', { status: 404, statusText: 'Bad Request' });
+    });
+
+    it('syncConfigs() should complete even if no changed item passed', () => {
+      const testData = getTestConfigs();
+
+      sensorParserConfigService.syncConfigs(testData)
+        .subscribe(
+          (value) => {
+            // we expect sync to return an empty array of result if no changed item
+            expect(value).toEqual([]);
+          },
+          noop,
+          () => {
+            // complete has to be called
+            expect(true).toBeTruthy();
+          }
+        );
+    });
+
+    it('syncGroups() should complete even if no changed item passed', () => {
+      const testData = getTestGroups();
+
+      sensorParserConfigService.syncGroups(testData)
+        .subscribe(
+          (value) => {
+            // we expect sync to return an empty array of result if no changed item
+            expect(value).toEqual([]);
+          },
+          noop,
+          () => {
+            // complete has to be called
+            expect(true).toBeTruthy();
+          }
+        );
+    });
+
+  })
 });
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
index add7cb5..7fb83a6 100644
--- 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
@@ -15,20 +15,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Injectable, Inject } from '@angular/core';
+import { Injectable } from '@angular/core';
 import { HttpClient } from '@angular/common/http';
-import { Observable, Subject } from 'rxjs';
-import { catchError, map } from 'rxjs/operators';
-import { SensorParserConfig } from '../model/sensor-parser-config';
+import { Observable, Subject, from, of } from 'rxjs';
+import { catchError, map, take, mergeMap, finalize, filter, reduce } from 'rxjs/operators';
+import { ParserConfigModel } from '../sensors/models/parser-config.model';
 import { HttpUtil } from '../util/httpUtil';
 import { ParseMessageRequest } from '../model/parse-message-request';
 import { RestError } from '../model/rest-error';
-import {AppConfigService} from './app-config.service';
+import { ParserGroupModel } from '../sensors/models/parser-group.model';
+import { ParserModel } from 'app/sensors/models/parser.model';
+import { ParserMetaInfoModel } from '../sensors/models/parser-meta-info.model';
+import { AppConfigService } from './app-config.service';
 
 @Injectable()
 export class SensorParserConfigService {
-  url = this.appConfigService.getApiRoot() + '/sensor/parser/config';
-  selectedSensorParserConfig: SensorParserConfig;
 
   dataChangedSource = new Subject<string[]>();
   dataChanged$ = this.dataChangedSource.asObservable();
@@ -38,57 +39,140 @@ export class SensorParserConfigService {
     private appConfigService: AppConfigService
   ) {}
 
-  public post(
-    name: string,
-    sensorParserConfig: SensorParserConfig
-  ): Observable<SensorParserConfig> {
-    return this.http
-      .post(this.url + '/' + name, JSON.stringify(sensorParserConfig))
-      .pipe(
-        map(HttpUtil.extractData),
-        catchError(HttpUtil.handleError)
-      );
+  private getParserConfigSvcUrl(): string {
+    return this.appConfigService.getApiRoot() + '/sensor/parser/config';
   }
 
-  public get(name: string): Observable<SensorParserConfig> {
-    return this.http.get(this.url + '/' + name).pipe(
-      map(HttpUtil.extractData),
+  private getParserGroupSvcUrl(): string {
+    return this.appConfigService.getApiRoot() + '/sensor/parser/group';
+  }
+
+  public getAllGroups(): Observable<ParserGroupModel[] | RestError> {
+    function extractParserGroups(raw) {
+      return Object.keys(raw).map((groupName) => {
+        return new ParserGroupModel({
+          ...raw[groupName]
+        })
+      });
+    }
+    return this.http.get(this.getParserGroupSvcUrl()).pipe(
+      map(extractParserGroups),
       catchError(HttpUtil.handleError)
     );
   }
 
-  public getAll(): Observable<{}> {
-    return this.http.get(this.url).pipe(
+  public getGroup(name: string): Observable<RestError | ParserGroupModel> {
+    return this.http.get(`${this.getParserGroupSvcUrl()}/${name}`).pipe(
+      map(group => new ParserGroupModel(group)),
+      catchError(HttpUtil.handleError)
+    );
+  }
+
+  public saveGroup(name: string, group: ParserGroupModel | ParserModel): Observable<RestError | ParserGroupModel> {
+    return this.http.post(`${this.getParserGroupSvcUrl()}`, group).pipe(
       map(HttpUtil.extractData),
       catchError(HttpUtil.handleError)
     );
   }
 
-  public deleteSensorParserConfig(
-    name: string
-  ): Observable<Object | RestError> {
-    return this.http
-      .delete(this.url + '/' + name)
-      .pipe(catchError(HttpUtil.handleError));
+  public deleteGroup(groupName: string): Observable<{ groupName: string, isSuccess: boolean }> {
+    return this.http.delete(`${this.getParserGroupSvcUrl()}/${groupName}`).pipe(
+      map((result) => { return { groupName, isSuccess: true } }),
+      catchError((error) => { return of({ groupName, isSuccess: false }) })
+    );
   }
 
-  public getAvailableParsers(): Observable<{}> {
-    return this.http.get(this.url + '/list/available').pipe(
+  public deleteGroups(
+    groupNames: string[]
+  ): 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(groupNames);
+      };
+      from(groupNames).pipe(
+        mergeMap(this.deleteGroup.bind(this)),
+        take(groupNames.length),
+        map((deleteResult: { groupName: string, isSuccess: boolean}) => {
+          (deleteResult.isSuccess ? result.success : result.failure).push(deleteResult.groupName);
+        }),
+        finalize(completed)
+        ).subscribe();
+    });
+
+    return observable;
+  }
+
+  syncConfigs(configs: ParserMetaInfoModel[]): Observable<{}> {
+    return this.sync(configs, this.saveConfig, this.deleteConfig);
+  }
+
+  syncGroups(groups: ParserMetaInfoModel[]): Observable<{}> {
+    return this.sync(groups, this.saveGroup, this.deleteGroup);
+  }
+
+  private sync(
+    items: ParserMetaInfoModel[],
+    saveFn: Function, deleteFn: Function
+  ) {
+    return from(items).pipe(
+      filter(item => !!(item.isDeleted || item.isDirty || item.isPhantom)),
+      mergeMap((changedItem: ParserMetaInfoModel) => {
+        if (changedItem.isDeleted) {
+          return deleteFn.call(this, changedItem.config.getName());
+        } else {
+          return saveFn.call(this, changedItem.config.getName(), changedItem.config);
+        }
+      }),
+      catchError(HttpUtil.handleError),
+      reduce((acc, value) => {
+        return acc.concat(value);
+      }, [])
+    )
+  }
+
+  public getConfig(name: string): Observable<ParserConfigModel> {
+    return this.http.get(this.getParserConfigSvcUrl() + '/' + name).pipe(
       map(HttpUtil.extractData),
       catchError(HttpUtil.handleError)
     );
   }
 
-  public parseMessage(
-    parseMessageRequest: ParseMessageRequest
-  ): Observable<{}> {
-    return this.http.post(this.url + '/parseMessage', parseMessageRequest).pipe(
+  public getAllConfig(): Observable<{}> {
+    return this.http.get(this.getParserConfigSvcUrl()).pipe(
       map(HttpUtil.extractData),
       catchError(HttpUtil.handleError)
     );
   }
 
-  public deleteSensorParserConfigs(
+  public saveConfig(
+    name: string,
+    sensorParserConfig: ParserModel
+  ): Observable<ParserConfigModel> {
+    return this.http
+      .post(this.getParserConfigSvcUrl() + '/' + name, JSON.stringify(sensorParserConfig))
+      .pipe(
+        map(HttpUtil.extractData),
+        catchError(HttpUtil.handleError)
+      );
+  }
+
+  public deleteConfig(
+    name: string
+  ): Observable<Object | RestError> {
+    return this.http
+      .delete(this.getParserConfigSvcUrl() + '/' + name)
+      .pipe(catchError(HttpUtil.handleError));
+  }
+
+  public deleteConfigs(
     sensorNames: string[]
   ): Observable<{ success: Array<string>; failure: Array<string> }> {
     let result: { success: Array<string>; failure: Array<string> } = {
@@ -105,7 +189,7 @@ export class SensorParserConfigService {
         this.dataChangedSource.next(sensorNames);
       };
       for (let i = 0; i < sensorNames.length; i++) {
-        this.deleteSensorParserConfig(sensorNames[i]).subscribe(
+        this.deleteConfig(sensorNames[i]).subscribe(
           results => {
             result.success.push(sensorNames[i]);
             if (
@@ -130,4 +214,20 @@ export class SensorParserConfigService {
 
     return observable;
   }
+
+  public getAvailableParsers(): Observable<{}> {
+    return this.http.get(this.getParserConfigSvcUrl() + '/list/available').pipe(
+      map(HttpUtil.extractData),
+      catchError(HttpUtil.handleError)
+    );
+  }
+
+  public parseMessage(
+    parseMessageRequest: ParseMessageRequest
+  ): Observable<{}> {
+    return this.http.post(this.getParserConfigSvcUrl() + '/parseMessage', parseMessageRequest).pipe(
+      map(HttpUtil.extractData),
+      catchError(HttpUtil.handleError)
+    );
+  }
 }