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 <ftamas.m...@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); + } +);