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/31 08:57:43 UTC
[metron] branch feature/METRON-1856-parser-aggregation updated:
METRON-2134 Add NgRx reducers to perform parser and group changes in the
store (ruffle1986 via sardell) closes apache/metron#1425
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 687d693 METRON-2134 Add NgRx reducers to perform parser and group changes in the store (ruffle1986 via sardell) closes apache/metron#1425
687d693 is described below
commit 687d69349fa0543999f566b8fddb3b7410d709a2
Author: ruffle1986 <ft...@gmail.com>
AuthorDate: Wed Jul 31 10:57:19 2019 +0200
METRON-2134 Add NgRx reducers to perform parser and group changes in the store (ruffle1986 via sardell) closes apache/metron#1425
---
.../app/sensors/models/parser-meta-info.model.ts | 34 +
.../src/app/sensors/reducers/index.ts | 39 ++
.../app/sensors/reducers/sensors.reducers.spec.ts | 720 +++++++++++++++++++++
.../src/app/sensors/reducers/sensors.reducers.ts | 638 ++++++++++++++++++
4 files changed, 1431 insertions(+)
diff --git a/metron-interface/metron-config/src/app/sensors/models/parser-meta-info.model.ts b/metron-interface/metron-config/src/app/sensors/models/parser-meta-info.model.ts
new file mode 100644
index 0000000..4588789
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/models/parser-meta-info.model.ts
@@ -0,0 +1,34 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { TopologyStatus } from '../../model/topology-status';
+import { ParserModel } from './parser.model';
+
+export interface ParserMetaInfoModel {
+ config: ParserModel;
+ status?: TopologyStatus;
+ isGroup?: boolean;
+ isHighlighted?: boolean;
+ isDraggedOver?: boolean;
+ isPhantom?: boolean;
+ isDirty?: boolean;
+ isDeleted?: boolean;
+ startStopInProgress?: boolean;
+ modifiedByDate?: string;
+ modifiedBy?: string;
+ isRunning?: boolean;
+}
diff --git a/metron-interface/metron-config/src/app/sensors/reducers/index.ts b/metron-interface/metron-config/src/app/sensors/reducers/index.ts
new file mode 100644
index 0000000..fcf50d6
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/reducers/index.ts
@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { ActionReducerMap } from '@ngrx/store';
+import * as fromSensors from './sensors.reducers';
+
+export * from './sensors.reducers';
+
+export interface State {
+ sensors: SensorState
+}
+
+export interface SensorState {
+ parsers: fromSensors.ParserState;
+ groups: fromSensors.GroupState;
+ statuses: fromSensors.StatusState;
+ layout: fromSensors.LayoutState
+}
+
+export const reducers: ActionReducerMap<SensorState> = {
+ parsers: fromSensors.parserConfigsReducer,
+ groups: fromSensors.groupConfigsReducer,
+ statuses: fromSensors.parserStatusReducer,
+ layout: fromSensors.layoutReducer
+}
diff --git a/metron-interface/metron-config/src/app/sensors/reducers/sensors.reducers.spec.ts b/metron-interface/metron-config/src/app/sensors/reducers/sensors.reducers.spec.ts
new file mode 100644
index 0000000..14eb9ab
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/reducers/sensors.reducers.spec.ts
@@ -0,0 +1,720 @@
+/**
+ * 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 * as fromReducers from './';
+import * as fromActionts from '../actions';
+import { ParserConfigModel } from '../models/parser-config.model';
+import { ParserGroupModel } from '../models/parser-group.model';
+import { TopologyStatus } from '../../model/topology-status';
+
+describe('sensors: parsers configs reducer', () => {
+
+ it('should return with the initial state by default', () => {
+ expect(
+ fromReducers.parserConfigsReducer(undefined, { type: '' })
+ ).toBe(fromReducers.initialParserState);
+ });
+
+ it('should return with the previous state', () => {
+ const previousState = { items: [] };
+ expect(
+ fromReducers.parserConfigsReducer(previousState, { type: '' })
+ ).toBe(previousState);
+ });
+
+ it('should set items on LoadSuccess', () => {
+ const parsers = [];
+ const previousState = {
+ items: []
+ };
+ const action = new fromActionts.LoadSuccess({
+ parsers
+ });
+
+ expect(
+ fromReducers.parserConfigsReducer(previousState, action).items
+ ).toBe(parsers);
+ });
+
+ it('should aggregate parsers on AggregateParsers', () => {
+ const groupName = 'Foo group';
+ const parserIds = [
+ 'Parser Config ID 02',
+ ]
+ const previousState: fromReducers.ParserState = {
+ items: [{
+ config: new ParserConfigModel('Parser Config ID 01', { sensorTopic: 'Kafka/Sensor Topic ID 1'})
+ }, {
+ config: new ParserConfigModel('Parser Config ID 02', { sensorTopic: 'Kafka/Sensor Topic ID 2'})
+ }]
+ };
+ const action = new fromActionts.AggregateParsers({
+ groupName,
+ parserIds,
+ });
+
+ const newState = fromReducers.parserConfigsReducer(previousState, action);
+ expect(newState.items[0]).toBe(previousState.items[0]);
+ expect(newState.items[1]).not.toBe(previousState.items[1]);
+ expect(newState.items[1].isDirty).toBe(true);
+ expect(newState.items[1].config.getName()).toBe('Parser Config ID 02');
+ expect(newState.items[1].config.group).toEqual(groupName);
+ });
+
+ it('should set group on AddToGroup', () => {
+ const groupName = 'Foo group';
+ const parserIds = [
+ 'Parser Config ID 02',
+ ]
+ const previousState: fromReducers.ParserState = {
+ items: [{
+ config: new ParserConfigModel('Parser Config ID 01', { sensorTopic: 'Kafka/Sensor Topic ID 1'})
+ }, {
+ config: new ParserConfigModel('Parser Config ID 02', { sensorTopic: 'Kafka/Sensor Topic ID 2'})
+ }]
+ };
+ const action = new fromActionts.AddToGroup({
+ groupName,
+ parserIds,
+ });
+
+ const newState = fromReducers.parserConfigsReducer(previousState, action);
+ expect(newState.items[0]).toBe(previousState.items[0]);
+ expect(newState.items[1]).not.toBe(previousState.items[1]);
+ expect(newState.items[1].isDirty).toBe(true);
+ expect(newState.items[1].config.getName()).toBe('Parser Config ID 02');
+ expect(newState.items[1].config.group).toEqual(groupName);
+ });
+
+ it('should mark items as deleted on MarkAsDeleted', () => {
+ const parserIds = ['Parser Config ID 02'];
+ const previousState: fromReducers.ParserState = {
+ items: [{
+ config: new ParserConfigModel('Parser Config ID 01', { sensorTopic: 'Kafka/Sensor Topic ID 1'})
+ }, {
+ config: new ParserConfigModel('Parser Config ID 02', { sensorTopic: 'Kafka/Sensor Topic ID 2'})
+ }]
+ };
+ const action = new fromActionts.MarkAsDeleted({ parserIds });
+ const newState = fromReducers.parserConfigsReducer(previousState, action);
+ expect(newState.items[0]).toBe(previousState.items[0]);
+ expect(newState.items[1]).not.toBe(previousState.items[1]);
+ expect(newState.items[1].isDeleted).toBe(true);
+ });
+
+ it('should remove group property of items which belong to a group marked as deleted on MarkAsDeleted', () => {
+ const groupName = 'Foo Group';
+ const parserIds = [groupName];
+ const previousState: fromReducers.ParserState = {
+ items: [{
+ config: new ParserConfigModel('Parser Config ID 01', { sensorTopic: 'Kafka/Sensor Topic ID 1'})
+ }, {
+ config: new ParserConfigModel('Parser Config ID 02', { sensorTopic: 'Kafka/Sensor Topic ID 2', group: groupName})
+ }]
+ };
+ const action = new fromActionts.MarkAsDeleted({ parserIds });
+ const newState = fromReducers.parserConfigsReducer(previousState, action);
+ expect(newState.items[0]).toBe(previousState.items[0]);
+ expect(newState.items[1]).not.toBe(previousState.items[1]);
+ expect(newState.items[1].isDeleted).toBeFalsy();
+ expect(newState.items[1].config.group).toBe('');
+ });
+});
+
+describe('sensors: group configs reducer', () => {
+
+ it('should return with the initial state by default', () => {
+ expect(
+ fromReducers.groupConfigsReducer(undefined, { type: '' })
+ ).toBe(fromReducers.initialGroupState);
+ });
+
+ it('should return with the previous state', () => {
+ const previousState = { items: [] };
+ expect(
+ fromReducers.groupConfigsReducer(previousState, { type: '' })
+ ).toBe(previousState);
+ });
+
+ it('should set items on LoadSuccess', () => {
+ const groups = [];
+ const previousState = {
+ items: []
+ };
+ const action = new fromActionts.LoadSuccess({
+ groups
+ });
+
+ expect(
+ fromReducers.groupConfigsReducer(previousState, action).items
+ ).toBe(groups);
+ });
+
+ it('should add a new group on CreateGroup', () => {
+ const previousState: fromReducers.GroupState = {
+ items: [{ config: new ParserGroupModel({ name: 'Existing group' }) }]
+ };
+ const action = new fromActionts.CreateGroup({name: 'New group', description: 'New description'});
+ const newState = fromReducers.groupConfigsReducer(previousState, action);
+
+ expect(newState.items.length).toBe(previousState.items.length + 1);
+ expect(newState.items[0]).toBe(previousState.items[0]);
+ expect(newState.items[1].config.getName()).toBe('New group');
+ expect(newState.items[1].isGroup).toBe(true);
+ expect(newState.items[1].isPhantom).toBe(true);
+ });
+
+ it('should edit an existing group description on UpdateGroupDescription', () => {
+ const previousState: fromReducers.GroupState = {
+ items: [{ config: new ParserGroupModel({ name: 'Existing group', description: 'Existing description' }) }]
+ };
+ const newConfig = {name: 'Existing group', description: 'New description'};
+ const action = new fromActionts.UpdateGroupDescription(newConfig);
+ const newState = fromReducers.groupConfigsReducer(previousState, action);
+
+ expect(newState.items.length).toBe(previousState.items.length);
+ expect(newState.items[0].config.getName()).toBe(newConfig.name);
+ expect(newState.items[0].config.getDescription()).toBe(newConfig.description);
+ expect(newState.items[0].isDirty).toBe(true);
+ });
+
+ it('should mark groups as deleted on MarkAsDeleted', () => {
+ const groupName = 'Existing group';
+ const previousState: fromReducers.GroupState = {
+ items: [{ config: new ParserGroupModel({ name: groupName }) }]
+ };
+ const action = new fromActionts.MarkAsDeleted({
+ parserIds: [groupName]
+ });
+ const newState = fromReducers.groupConfigsReducer(previousState, action);
+
+ expect(newState.items[0].isDeleted).toBe(true);
+ });
+});
+
+describe('sensors: parser statuses reducer', () => {
+
+ it('should return with the initial state by default', () => {
+ expect(
+ fromReducers.parserStatusReducer(undefined, { type: '' })
+ ).toBe(fromReducers.initialStatusState);
+ });
+
+ it('should return with the previous state', () => {
+ const previousState = { items: [] };
+ expect(
+ fromReducers.parserStatusReducer(previousState, { type: '' })
+ ).toBe(previousState);
+ });
+
+ it('should set items on LoadSuccess', () => {
+ const statuses = [];
+ const previousState = {
+ items: []
+ };
+ const action = new fromActionts.LoadSuccess({
+ statuses
+ });
+
+ expect(
+ fromReducers.parserStatusReducer(previousState, action).items
+ ).toBe(statuses);
+ });
+
+ it('should set items on PollStatusSuccess', () => {
+ const statuses = [];
+ const previousState = {
+ items: []
+ };
+ const action = new fromActionts.PollStatusSuccess({
+ statuses
+ });
+
+ expect(
+ fromReducers.parserStatusReducer(previousState, action).items
+ ).toBe(statuses);
+ });
+});
+
+describe('sensors: layout reducer', () => {
+ it('should return with the initial state by default', () => {
+ expect(
+ fromReducers.layoutReducer(undefined, { type: '' })
+ ).toBe(fromReducers.initialLayoutState);
+ });
+
+ it('should return with the previous state', () => {
+ const previousState = { order: [], dnd: {} };
+ expect(
+ fromReducers.layoutReducer(previousState, { type: '' })
+ ).toBe(previousState);
+ });
+
+ it('should set the order on LoadSuccess', () => {
+ const previousState = { order: [], dnd: {} };
+ const action = new fromActionts.LoadSuccess({
+ parsers: [
+ { config: new ParserConfigModel('Parser Config ID 01', { sensorTopic: 'sensor topic 1', group: 'group 2' }) },
+ { config: new ParserConfigModel('Parser Config ID 02', { sensorTopic: 'sensor topic 2', group: 'group 2' }) },
+ { config: new ParserConfigModel('Parser Config ID 03', { sensorTopic: 'sensor topic 3' }) },
+ { config: new ParserConfigModel('Parser Config ID 04', { sensorTopic: 'sensor topic 4', group: 'group 1' }) },
+ ],
+ groups: [
+ { config: new ParserGroupModel({ name: 'group 1' }) },
+ { config: new ParserGroupModel({ name: 'group 2' }) },
+ ]
+ });
+ const newState = fromReducers.layoutReducer(previousState, action);
+ expect(newState.order).not.toBe(previousState.order);
+ expect(newState.order).toEqual([
+ 'group 1',
+ 'Parser Config ID 04',
+ 'group 2',
+ 'Parser Config ID 01',
+ 'Parser Config ID 02',
+ 'Parser Config ID 03',
+ ]);
+ });
+
+ it('should set the draggedId on SetDragged', () => {
+ const previousState = { order: [], dnd: {} };
+ const action = new fromActionts.SetDragged('Foo');
+ const newState = fromReducers.layoutReducer(previousState, action);
+
+ expect(newState.dnd.draggedId).toBe('Foo');
+ });
+
+ it('should set the dropTargetId on SetDropTarget', () => {
+ const previousState = { order: [], dnd: {} };
+ const action = new fromActionts.SetDropTarget('Bar');
+ const newState = fromReducers.layoutReducer(previousState, action);
+
+ expect(newState.dnd.dropTargetId).toBe('Bar');
+ });
+
+ it('should set the targetGroup on SetTargetGroup', () => {
+ const previousState = { order: [], dnd: {} };
+ const action = new fromActionts.SetTargetGroup('Lorem');
+ const newState = fromReducers.layoutReducer(previousState, action);
+
+ expect(newState.dnd.targetGroup).toBe('Lorem');
+ });
+
+ it('should append the group name to the order on CreateGroup', () => {
+ const previousState = { order: [], dnd: {} };
+ const action = new fromActionts.CreateGroup({name: 'Group name', description: 'Group description'});
+ const newState = fromReducers.layoutReducer(previousState, action);
+
+ expect(newState.order[newState.order.length - 1]).toBe('Group name');
+ });
+
+ it('should recalculate the order on AggregateParsers', () => {
+ const previousState = {
+ order: [
+ 'group 1',
+ 'sensor topic 3',
+ 'group 2',
+ 'sensor topic 1',
+ 'sensor topic 2',
+ 'group 4',
+ 'sensor topic 4',
+ ],
+ dnd: {}
+ };
+ const action = new fromActionts.AggregateParsers({
+ groupName: 'group 4',
+ parserIds: ['sensor topic 2', 'sensor topic 1']
+ });
+ const newState = fromReducers.layoutReducer(previousState, action);
+
+ expect(newState.order).not.toBe(previousState.order);
+ expect(newState.order).toEqual([
+ 'group 1',
+ 'sensor topic 3',
+ 'group 2',
+ 'group 4',
+ 'sensor topic 1',
+ 'sensor topic 2',
+ 'sensor topic 4',
+ ]);
+ });
+
+ it('should inject the order item after the reference item on InjectAfter', () => {
+ const previousState = {
+ order: [
+ 'group 1',
+ 'group 2',
+ 'sensor topic 1',
+ 'sensor topic 2',
+ 'sensor topic 3',
+ 'sensor topic 4',
+ ],
+ dnd: {}
+ };
+ const action = new fromActionts.InjectAfter({
+ parserId: 'sensor topic 1',
+ reference: 'sensor topic 3'
+ });
+ const newState = fromReducers.layoutReducer(previousState, action);
+
+ expect(newState.order).not.toBe(previousState.order);
+ expect(newState.order).toEqual([
+ 'group 1',
+ 'group 2',
+ 'sensor topic 2',
+ 'sensor topic 3',
+ 'sensor topic 1',
+ 'sensor topic 4',
+ ]);
+ });
+
+ it('should inject the order item before the reference item on InjectBefore', () => {
+ const previousState = {
+ order: [
+ 'group 1',
+ 'group 2',
+ 'sensor topic 1',
+ 'sensor topic 2',
+ 'sensor topic 3',
+ 'sensor topic 4',
+ ],
+ dnd: {}
+ };
+ const action = new fromActionts.InjectBefore({
+ parserId: 'sensor topic 4',
+ reference: 'sensor topic 2'
+ });
+ const newState = fromReducers.layoutReducer(previousState, action);
+
+ expect(newState.order).not.toBe(previousState.order);
+ expect(newState.order).toEqual([
+ 'group 1',
+ 'group 2',
+ 'sensor topic 1',
+ 'sensor topic 4',
+ 'sensor topic 2',
+ 'sensor topic 3',
+ ]);
+ });
+});
+
+describe('sensors: selectors', () => {
+
+ it('should return with the sensors substate from the store', () => {
+ const sensors = {
+ parsers: { items: [] },
+ groups: { items: [] },
+ statuses: { items: [] },
+ layout: { order: [], dnd: {} }
+ };
+ const state = { sensors };
+
+ expect(fromReducers.getSensorsState(state))
+ .toBe(sensors);
+ });
+
+ it('should return with the parsers substate from the store', () => {
+ const sensors = {
+ parsers: { items: [] },
+ groups: { items: [] },
+ statuses: { items: [] },
+ layout: { order: [], dnd: {} }
+ };
+ const state = { sensors };
+
+ expect(fromReducers.getParsers(state))
+ .toBe(sensors.parsers.items);
+ });
+
+ it('should return with the groups substate from the store', () => {
+ const sensors = {
+ parsers: { items: [] },
+ groups: { items: [] },
+ statuses: { items: [] },
+ layout: { order: [], dnd: {} }
+ };
+ const state = { sensors };
+
+ expect(fromReducers.getGroups(state))
+ .toBe(sensors.groups.items);
+ });
+
+ it('should return with the statuses substate from the store', () => {
+ const sensors = {
+ parsers: { items: [] },
+ groups: { items: [] },
+ statuses: { items: [] },
+ layout: { order: [], dnd: {} }
+ };
+ const state = { sensors };
+
+ expect(fromReducers.getStatuses(state))
+ .toBe(sensors.statuses.items);
+ });
+
+ it('should return with the order substate from the store', () => {
+ const sensors = {
+ parsers: { items: [] },
+ groups: { items: [] },
+ statuses: { items: [] },
+ layout: { order: [], dnd: {} }
+ };
+ const state = { sensors };
+
+ expect(fromReducers.getLayoutOrder(state))
+ .toBe(sensors.layout.order);
+ });
+
+ it('should return with a merged version of groups, parsers and statuses ordered by the order state', () => {
+ const state = {
+ sensors: {
+ parsers: {
+ items: [
+ { config: new ParserConfigModel('Parser Config ID 01', { sensorTopic: 'Kafka/Sensor Topic ID 1' }) },
+ { config: new ParserConfigModel('Parser Config ID 02', { sensorTopic: 'Kafka/Sensor Topic ID 2', group: 'group 1' }) },
+ ]
+ },
+ groups: {
+ items: [
+ { config: new ParserGroupModel({ name: 'group 1' }) },
+ { config: new ParserGroupModel({ name: 'group 2' }) },
+ ]
+ },
+ statuses: {
+ items: [
+ new TopologyStatus({ name: 'Parser Config ID 02' }),
+ new TopologyStatus({ name: 'Parser Config ID 01' }),
+ new TopologyStatus({ name: 'group 2' }),
+ ]
+ },
+ layout: {
+ order: [
+ 'Parser Config ID 02',
+ 'Parser Config ID 01',
+ 'group 2',
+ 'group 1'
+ ],
+ dnd: {}
+ }
+ }
+ };
+
+ const merged = fromReducers.getMergedConfigs(state);
+
+ expect(merged.length).toBe(state.sensors.parsers.items.length + state.sensors.groups.items.length);
+
+ // the reference changes !!
+ expect(merged[0]).not.toBe(state.sensors.parsers.items[1]);
+ expect(merged[1]).not.toBe(state.sensors.parsers.items[0]);
+ expect(merged[2]).not.toBe(state.sensors.groups.items[1]);
+ expect(merged[3]).not.toBe(state.sensors.groups.items[0]);
+
+ // should be ordered by the order state
+ expect(merged[0].config.getName()).toBe(state.sensors.layout.order[0]);
+ expect(merged[1].config.getName()).toBe(state.sensors.layout.order[1]);
+ expect(merged[2].config.getName()).toBe(state.sensors.layout.order[2]);
+ expect(merged[3].config.getName()).toBe(state.sensors.layout.order[3]);
+
+ // make sure they got the status
+ expect(merged[0].status).toEqual(state.sensors.statuses.items[0]);
+ expect(merged[1].status).toEqual(state.sensors.statuses.items[1]);
+ expect(merged[2].status).toEqual(state.sensors.statuses.items[2]);
+
+ // no status belongs to it but got a status instance with no name
+ expect(merged[3].status).toBeTruthy();
+ expect(merged[3].status.name).toBeFalsy();
+ });
+
+ it('should tell if any of the groups or parser configs are dirty', () => {
+ expect(fromReducers.isDirty({
+ sensors: {
+ parsers: {
+ items: [
+ { config: new ParserConfigModel('Parser Config ID 01', { sensorTopic: 'sensor topic 1' }) },
+ { config: new ParserConfigModel('Parser Config ID 01', { sensorTopic: 'sensor topic 2' }) },
+ ]
+ },
+ groups: {
+ items: [
+ { config: new ParserGroupModel({ name: 'group 1' }) },
+ { config: new ParserGroupModel({ name: 'group 2' }) },
+ ]
+ },
+ statuses: { items: [] },
+ layout: { order: [], dnd: {} }
+ }
+ })).toBe(false);
+
+ expect(fromReducers.isDirty({
+ sensors: {
+ parsers: {
+ items: [
+ { config: new ParserConfigModel('Parser Config ID 01', { sensorTopic: 'sensor topic 1' }) },
+ { config: new ParserConfigModel('Parser Config ID 02', { sensorTopic: 'sensor topic 2' }) },
+ ]
+ },
+ groups: {
+ items: [
+ { config: new ParserGroupModel({ name: 'group 1' }), isDeleted: true },
+ { config: new ParserGroupModel({ name: 'group 2' }) },
+ ]
+ },
+ statuses: { items: [] },
+ layout: { order: [], dnd: {} }
+ }
+ })).toBe(true);
+
+ expect(fromReducers.isDirty({
+ sensors: {
+ parsers: {
+ items: [
+ { config: new ParserConfigModel('Parser Config ID 01', { sensorTopic: 'sensor topic 1' }) },
+ { config: new ParserConfigModel('Parser Config ID 02', { sensorTopic: 'sensor topic 2' }), isDeleted: true },
+ ]
+ },
+ groups: {
+ items: [
+ { config: new ParserGroupModel({ name: 'group 1' }) },
+ { config: new ParserGroupModel({ name: 'group 2' }) },
+ ]
+ },
+ statuses: { items: [] },
+ layout: { order: [], dnd: {} }
+ }
+ })).toBe(true);
+
+ expect(fromReducers.isDirty({
+ sensors: {
+ parsers: {
+ items: [
+ { config: new ParserConfigModel('Parser Config ID 01', { sensorTopic: 'sensor topic 1' }) },
+ { config: new ParserConfigModel('Parser Config ID 02', { sensorTopic: 'sensor topic 2' }) },
+ ]
+ },
+ groups: {
+ items: [
+ { config: new ParserGroupModel({ name: 'group 1' }), isDirty: true },
+ { config: new ParserGroupModel({ name: 'group 2' }) },
+ ]
+ },
+ statuses: { items: [] },
+ layout: { order: [], dnd: {} }
+ }
+ })).toBe(true);
+
+ expect(fromReducers.isDirty({
+ sensors: {
+ parsers: {
+ items: [
+ { config: new ParserConfigModel('Parser Config ID 01', { sensorTopic: 'sensor topic 1' }), isDirty: true },
+ { config: new ParserConfigModel('Parser Config ID 02', { sensorTopic: 'sensor topic 2' }) },
+ ]
+ },
+ groups: {
+ items: [
+ { config: new ParserGroupModel({ name: 'group 1' }) },
+ { config: new ParserGroupModel({ name: 'group 2' }) },
+ ]
+ },
+ statuses: { items: [] },
+ layout: { order: [], dnd: {} }
+ }
+ })).toBe(true);
+
+ expect(fromReducers.isDirty({
+ sensors: {
+ parsers: {
+ items: [
+ { config: new ParserConfigModel('Parser Config ID 01', { sensorTopic: 'sensor topic 1' }) },
+ { config: new ParserConfigModel('Parser Config ID 02', { sensorTopic: 'sensor topic 2' }), isPhantom: true },
+ ]
+ },
+ groups: {
+ items: [
+ { config: new ParserGroupModel({ name: 'group 1' }) },
+ { config: new ParserGroupModel({ name: 'group 2' }) },
+ ]
+ },
+ statuses: { items: [] },
+ layout: { order: [], dnd: {} }
+ }
+ })).toBe(true);
+
+ expect(fromReducers.isDirty({
+ sensors: {
+ parsers: {
+ items: [
+ { config: new ParserConfigModel('Parser Config ID 01', { sensorTopic: 'sensor topic 1' }) },
+ { config: new ParserConfigModel('Parser Config ID 02', { sensorTopic: 'sensor topic 2' }) },
+ ]
+ },
+ groups: {
+ items: [
+ { config: new ParserGroupModel({ name: 'group 1' }), isPhantom: true },
+ { config: new ParserGroupModel({ name: 'group 2' }) },
+ ]
+ },
+ statuses: { items: [] },
+ layout: { order: [], dnd: {} }
+ }
+ })).toBe(true);
+ });
+
+ it('should update the parser config in state', () => {
+ const previousState: fromReducers.ParserState = {
+ items: [
+ { config: new ParserConfigModel('bar', { sensorTopic: 'bar' }) },
+ { config: new ParserConfigModel('foo', { sensorTopic: 'foo' }) },
+ ]
+ };
+ const action = new fromActionts.UpdateParserConfig(
+ new ParserConfigModel('foo', { sensorTopic: 'foo updated' })
+ );
+ const newState = fromReducers.parserConfigsReducer(previousState, action);
+ const updated = newState.items.find(item => item.config.getName() === 'foo');
+ expect((updated.config as ParserConfigModel).sensorTopic).toBe('foo updated');
+ });
+
+ it('should add a new parser config', () => {
+ const previousState: fromReducers.ParserState = {
+ items: [
+ { config: new ParserConfigModel('bar', { sensorTopic: 'bar' }) },
+ { config: new ParserConfigModel('foo', { sensorTopic: 'foo' }) },
+ ]
+ };
+ const action = new fromActionts.AddParserConfig(
+ new ParserConfigModel('baz', { sensorTopic: 'baz new' })
+ );
+ const newState = fromReducers.parserConfigsReducer(previousState, action);
+ expect((newState.items[2].config as ParserConfigModel).id).toBe('baz');
+ expect((newState.items[2].config as ParserConfigModel).sensorTopic).toBe('baz new');
+ });
+
+ it('should add a new parser config in the order', () => {
+ const previousState: fromReducers.LayoutState = {
+ order: [
+ 'bar', 'foo'
+ ],
+ dnd: {}
+ };
+ const action = new fromActionts.AddParserConfig(
+ new ParserConfigModel('baz', { sensorTopic: 'baz new' })
+ );
+ const newState = fromReducers.layoutReducer(previousState, action);
+ expect(newState.order[2]).toBe('baz');
+ });
+});
diff --git a/metron-interface/metron-config/src/app/sensors/reducers/sensors.reducers.ts b/metron-interface/metron-config/src/app/sensors/reducers/sensors.reducers.ts
new file mode 100644
index 0000000..800c8f4
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/reducers/sensors.reducers.ts
@@ -0,0 +1,638 @@
+/**
+ * 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 { Action, createSelector, createFeatureSelector } from '@ngrx/store';
+import { TopologyStatus } from '../../model/topology-status';
+import { ParserGroupModel } from '../models/parser-group.model';
+import { ParserMetaInfoModel } from '../models/parser-meta-info.model';
+import * as fromActions from '../actions';
+import { State, SensorState } from './';
+import { ParserConfigModel } from '../models/parser-config.model';
+
+export interface ParserState {
+ items: ParserMetaInfoModel[];
+}
+
+export interface GroupState {
+ items: ParserMetaInfoModel[];
+}
+
+export interface StatusState {
+ items: TopologyStatus[];
+}
+
+export interface DragNDropState {
+ draggedId?: string,
+ dropTargetId?: string,
+ targetGroup?: string,
+}
+
+export interface LayoutState {
+ order: string[],
+ dnd: DragNDropState
+}
+
+export const initialParserState: ParserState = {
+ items: []
+}
+
+export const initialGroupState: GroupState = {
+ items: []
+}
+
+export const initialStatusState: StatusState = {
+ items: []
+}
+
+export const initialLayoutState: LayoutState = {
+ order: [],
+ dnd: {
+ draggedId: '',
+ dropTargetId: '',
+ targetGroup: ''
+ }
+}
+
+export function parserConfigsReducer(state: ParserState = initialParserState, action: Action): ParserState {
+ switch (action.type) {
+ case fromActions.SensorsActionTypes.LoadSuccess:
+ return {
+ ...state,
+ items: (action as fromActions.LoadSuccess).payload.parsers
+ };
+
+ case fromActions.SensorsActionTypes.UpdateParserConfig: {
+ const a = (action as fromActions.UpdateParserConfig);
+ return {
+ ...state,
+ items: state.items.map(item => {
+ if (item.config.getName() === a.payload.getName()) {
+ return {
+ ...item,
+ config: a.payload.clone()
+ };
+ }
+ return item;
+ }),
+ };
+ }
+
+ case fromActions.SensorsActionTypes.AddParserConfig: {
+ const a = (action as fromActions.AddParserConfig);
+ return {
+ ...state,
+ items: [
+ ...state.items,
+ {
+ config: (a.payload as ParserConfigModel).clone(),
+ }
+ ]
+ };
+ }
+
+ case fromActions.SensorsActionTypes.AggregateParsers:
+ case fromActions.SensorsActionTypes.AddToGroup: {
+ const a = (action as fromActions.AggregateParsers);
+ return {
+ ...state,
+ items: state.items.map(item => {
+ if (a.payload.parserIds.includes(item.config.getName())) {
+ if (item.config.group !== a.payload.groupName) {
+ const config = (item.config as ParserConfigModel).clone();
+ config.group = a.payload.groupName;
+ return {
+ ...item,
+ isDirty: true,
+ config,
+ };
+ }
+ }
+ return item;
+ })
+ };
+ }
+
+ case fromActions.SensorsActionTypes.MarkAsDeleted: {
+ const a = (action as fromActions.MarkAsDeleted);
+ return {
+ ...state,
+ items: state.items.map(item => {
+ if (a.payload.parserIds.includes(item.config.getName())) {
+ item = {
+ ...item,
+ isDeleted: true
+ };
+ }
+ if (a.payload.parserIds.includes(item.config.group)) {
+ if (item.config.group) {
+ const config = (item.config as ParserConfigModel).clone();
+ config.group = '';
+ item = {
+ ...item,
+ isDirty: true,
+ config,
+ };
+ }
+ }
+ return item;
+ })
+ }
+ }
+
+ case fromActions.SensorsActionTypes.StartSensor:
+ case fromActions.SensorsActionTypes.StopSensor:
+ case fromActions.SensorsActionTypes.EnableSensor:
+ case fromActions.SensorsActionTypes.DisableSensor: {
+ const a = action as fromActions.SensorControlAction;
+ return {
+ ...state,
+ items: state.items.map((item) => {
+ if (a.payload.parser.config.getName() === item.config.getName()) {
+ return {
+ ...item,
+ startStopInProgress: true
+ };
+ }
+ return item;
+ })
+ };
+ }
+
+ case fromActions.SensorsActionTypes.StartSensorSuccess:
+ case fromActions.SensorsActionTypes.StartSensorFailure:
+ case fromActions.SensorsActionTypes.StopSensorSuccess:
+ case fromActions.SensorsActionTypes.StopSensorFailure:
+ case fromActions.SensorsActionTypes.EnableSensorSuccess:
+ case fromActions.SensorsActionTypes.EnableSensorFailure:
+ case fromActions.SensorsActionTypes.DisableSensorSuccess:
+ case fromActions.SensorsActionTypes.DisableSensorFailure: {
+ const a = action as fromActions.SensorControlResponseAction;
+ return {
+ ...state,
+ items: state.items.map((item) => {
+ if (a.payload.parser.config.getName() === item.config.getName()) {
+ return {
+ ...item,
+ startStopInProgress: false
+ };
+ }
+ return item;
+ })
+ };
+ }
+
+ default:
+ return state;
+ }
+}
+
+export function groupConfigsReducer(state: GroupState = initialGroupState, action: Action): GroupState {
+ switch (action.type) {
+ case fromActions.SensorsActionTypes.LoadSuccess:
+ return {
+ ...state,
+ items: (action as fromActions.LoadSuccess).payload.groups
+ }
+ case fromActions.SensorsActionTypes.CreateGroup: {
+ const a = (action as fromActions.CreateGroup);
+ const group = {
+ config: new ParserGroupModel({ name: a.payload.name, description: a.payload.description }),
+ isGroup: true,
+ isPhantom: true,
+ };
+ return {
+ ...state,
+ items: [
+ ...state.items,
+ group
+ ]
+ }
+ }
+ case fromActions.SensorsActionTypes.AddToGroup: {
+ const a = (action as fromActions.AddToGroup);
+ const groupName = a.payload.groupName;
+ const parserIds = a.payload.parserIds;
+ if (groupName === '') {
+ return {
+ ...state,
+ items: state.items.map(item => {
+ let config = item.config as ParserGroupModel;
+ let changed;
+ parserIds.forEach(id => {
+ if (config.sensors.includes(id)) {
+ config = config.clone({
+ sensors: config.sensors.filter(sensor => sensor !== id),
+ });
+ changed = true;
+ }
+ });
+ return {
+ ...item,
+ config,
+ isDirty: typeof changed === 'undefined' ? item.isDirty : changed,
+ }
+ })
+ };
+ } else {
+ return {
+ ...state,
+ items: state.items.map(item => {
+ const config = item.config as ParserGroupModel;
+ if (config.getName() === groupName) {
+ const newConfig = config.clone({
+ name: groupName,
+ sensors: [...config.sensors, ...parserIds],
+ });
+ return {
+ ...item,
+ config: newConfig,
+ isDirty: true
+ };
+ }
+ return item;
+ })
+ };
+ }
+ }
+ case fromActions.SensorsActionTypes.AggregateParsers: {
+ const a = (action as fromActions.AggregateParsers);
+ const groupName = a.payload.groupName;
+ const parserIds = a.payload.parserIds;
+ return {
+ ...state,
+ items: state.items.map(item => {
+ const config = item.config as ParserGroupModel;
+ if (config.getName() === groupName) {
+ const newConfig = config.clone({
+ name: groupName,
+ sensors: [...config.sensors, ...parserIds]
+ });
+ return {
+ ...item,
+ config: newConfig,
+ isDirty: true
+ };
+ }
+ return item;
+ })
+ };
+ }
+ case fromActions.SensorsActionTypes.UpdateGroupDescription: {
+ const a = (action as fromActions.UpdateGroupDescription);
+ return {
+ ...state,
+ items: state.items.map(item => {
+ if (a.payload.name === item.config.getName()) {
+ const config = (item.config as ParserGroupModel).clone(a.payload);
+ config.setDescription(a.payload.description);
+ return {
+ ...item,
+ config,
+ isDirty: true
+ }
+ }
+ return item;
+ })
+ }
+ }
+ case fromActions.SensorsActionTypes.MarkAsDeleted: {
+ const a = (action as fromActions.MarkAsDeleted);
+ return {
+ ...state,
+ items: state.items.map(item => {
+ if (a.payload.parserIds.includes(item.config.getName())) {
+ return {
+ ...item,
+ isDeleted: true
+ };
+ }
+ return item;
+ })
+ }
+ }
+ case fromActions.SensorsActionTypes.StartSensor:
+ case fromActions.SensorsActionTypes.StopSensor:
+ case fromActions.SensorsActionTypes.EnableSensor:
+ case fromActions.SensorsActionTypes.DisableSensor: {
+ const a = action as fromActions.SensorControlAction;
+ return {
+ ...state,
+ items: state.items.map((item) => {
+ if (a.payload.parser.config.getName() === item.config.getName()) {
+ return {
+ ...item,
+ startStopInProgress: true
+ };
+ }
+ return item;
+ })
+ };
+ }
+
+ case fromActions.SensorsActionTypes.StartSensorSuccess:
+ case fromActions.SensorsActionTypes.StartSensorFailure:
+ case fromActions.SensorsActionTypes.StopSensorSuccess:
+ case fromActions.SensorsActionTypes.StopSensorFailure:
+ case fromActions.SensorsActionTypes.EnableSensorSuccess:
+ case fromActions.SensorsActionTypes.EnableSensorFailure:
+ case fromActions.SensorsActionTypes.DisableSensorSuccess:
+ case fromActions.SensorsActionTypes.DisableSensorFailure: {
+ const a = action as fromActions.SensorControlResponseAction;
+ return {
+ ...state,
+ items: state.items.map((item) => {
+ if (a.payload.parser.config.getName() === item.config.getName()) {
+ return {
+ ...item,
+ startStopInProgress: false
+ };
+ }
+ return item;
+ })
+ };
+ }
+
+ default:
+ return state;
+ }
+}
+
+export function parserStatusReducer(state: StatusState = initialStatusState, action: Action): StatusState {
+ switch (action.type) {
+ case fromActions.SensorsActionTypes.LoadSuccess:
+ case fromActions.SensorsActionTypes.PollStatusSuccess: {
+ return {
+ ...state,
+ items: (action as fromActions.LoadSuccess).payload.statuses
+ }
+ }
+
+ default:
+ return state;
+ }
+}
+
+export function layoutReducer(state: LayoutState = initialLayoutState, action: Action): LayoutState {
+ switch (action.type) {
+ case fromActions.SensorsActionTypes.LoadSuccess: {
+ const payload = (action as fromActions.LoadSuccess).payload;
+ const groups: ParserMetaInfoModel[] = payload.groups;
+ const parsers: ParserMetaInfoModel[] = payload.parsers;
+ let order: string[] = [];
+ groups.forEach((group) => {
+ order = order.concat(group.config.getName());
+ const configsForGroup = parsers
+ .filter(parser => parser.config && parser.config.group === group.config.getName())
+ .map(parser => parser.config.getName());
+ order = order.concat(configsForGroup);
+ });
+
+ order = order.concat(
+ parsers
+ .filter(parser => !parser.config.group)
+ .map(parser => parser.config.getName())
+ );
+
+ return {
+ ...state,
+ order
+ };
+ }
+
+ case fromActions.SensorsActionTypes.SetDragged: {
+
+ return {
+ ...state,
+ dnd: {
+ ...state.dnd,
+ draggedId: (action as fromActions.SetDragged).payload
+ }
+ };
+ }
+
+ case fromActions.SensorsActionTypes.SetDropTarget: {
+
+ return {
+ ...state,
+ dnd: {
+ ...state.dnd,
+ dropTargetId: (action as fromActions.SetDropTarget).payload
+ }
+ };
+ }
+
+ case fromActions.SensorsActionTypes.SetTargetGroup: {
+
+ return {
+ ...state,
+ dnd: {
+ ...state.dnd,
+ targetGroup: (action as fromActions.SetTargetGroup).payload
+ }
+ };
+ }
+
+ case fromActions.SensorsActionTypes.CreateGroup: {
+ const a = (action as fromActions.CreateGroup);
+ return {
+ ...state,
+ order: [
+ ...state.order,
+ a.payload.name
+ ]
+ };
+ }
+
+ case fromActions.SensorsActionTypes.AggregateParsers: {
+ let order = state.order.slice(0);
+ const a = (action as fromActions.AggregateParsers);
+ const reference: string = a.payload.parserIds[0];
+ const referenceIndex = order.indexOf(reference);
+ const dragged: string = a.payload.parserIds[1];
+
+ order = order.map(id => {
+ if (id === a.payload.groupName || id === dragged) {
+ return null;
+ }
+ return id;
+ });
+ order.splice(referenceIndex, 0, a.payload.groupName);
+ order.splice(referenceIndex + 1, 0, dragged);
+
+ order = order.filter(Boolean);
+
+ return {
+ ...state,
+ order,
+ }
+ }
+
+ case fromActions.SensorsActionTypes.InjectAfter: {
+ let order = state.order.slice(0);
+ const a = (action as fromActions.InjectAfter);
+ const referenceIndex = order.indexOf(a.payload.reference);
+
+ order = order.map(id => {
+ if (id === a.payload.parserId) {
+ return null;
+ }
+ return id;
+ });
+
+ order.splice(referenceIndex + 1, 0, a.payload.parserId);
+
+ order = order.filter(Boolean);
+
+ return {
+ ...state,
+ order
+ };
+ }
+
+ case fromActions.SensorsActionTypes.InjectBefore: {
+ let order = state.order.slice(0);
+ const a = (action as fromActions.InjectBefore);
+ const referenceIndex = order.indexOf(a.payload.reference);
+
+ order = order.map(id => {
+ if (id === a.payload.parserId) {
+ return null;
+ }
+ return id;
+ });
+
+ order.splice(referenceIndex, 0, a.payload.parserId);
+
+ order = order.filter(Boolean);
+
+ return {
+ ...state,
+ order
+ };
+ }
+
+ case fromActions.SensorsActionTypes.AddParserConfig: {
+ const a = (action as fromActions.AddParserConfig);
+ return {
+ ...state,
+ order: [
+ ...state.order,
+ a.payload.getName(),
+ ]
+ };
+ }
+
+ default:
+ return state;
+ }
+}
+
+/**
+ * Selectors
+ */
+
+ export const getSensorsState = createFeatureSelector<State, SensorState>('sensors');
+
+export const getGroups = createSelector(
+ getSensorsState,
+ (state: SensorState): ParserMetaInfoModel[] => {
+ return state.groups.items;
+ }
+);
+
+export const getGroupByName = createSelector(
+ getGroups,
+ (groups) => (name: string): ParserMetaInfoModel => {
+ return groups.find((group: ParserMetaInfoModel) => group.config.getName() === name);
+ }
+);
+
+export const getParsers = createSelector(
+ getSensorsState,
+ (state: SensorState): ParserMetaInfoModel[] => {
+ return state.parsers.items;
+ }
+);
+
+export const getStatuses = createSelector(
+ getSensorsState,
+ (state: SensorState): TopologyStatus[] => {
+ return state.statuses.items;
+ }
+);
+
+export const getLayoutOrder = createSelector(
+ getSensorsState,
+ (state: SensorState): string[] => {
+ return state.layout.order;
+ }
+);
+
+export const getMergedConfigs = createSelector(
+ getGroups,
+ getParsers,
+ getStatuses,
+ getLayoutOrder,
+ (
+ groups: ParserMetaInfoModel[],
+ parsers: ParserMetaInfoModel[],
+ statuses: TopologyStatus[],
+ order: string[]
+ ): ParserMetaInfoModel[] => {
+ let result: ParserMetaInfoModel[] = [];
+ result = order.map((id: string) => {
+ const group = groups.find(g => g.config.getName() === id);
+ if (group) {
+ return group;
+ }
+ const parserConfig = parsers.find(p => p.config.getName() === id);
+ if (parserConfig) {
+ return parserConfig;
+ }
+ return null;
+ }).filter(Boolean);
+
+ result = result.map((item) => {
+ let status: TopologyStatus = statuses.find(stat => {
+ return stat.name === item.config.getName();
+ });
+ return {
+ ...item,
+ status: status ? new TopologyStatus(status) : new TopologyStatus(),
+ isRunning: status ? status.status === 'ACTIVE' : false,
+ };
+ });
+
+ return result;
+ }
+);
+
+export const isDirty = createSelector(
+ getGroups,
+ getParsers,
+ (groups: ParserMetaInfoModel[], parsers: ParserMetaInfoModel[]): boolean => {
+ const isChanged = (item) => item.isDeleted || item.isDirty || item.isPhantom;
+ return groups.some(isChanged) || parsers.some(isChanged)
+ }
+);
+
+export const getParserConfig = () => createSelector(
+ getParsers,
+ (parsers: ParserMetaInfoModel[], props) => {
+ return parsers.find(parser => parser.config.getName() === props.id);
+ }
+);