Adds tests for action creators and reducers
This commit is contained in:
@@ -2,7 +2,8 @@ module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
"node": true,
|
||||
"jest": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
|
||||
2
.travis.yml
Normal file
2
.travis.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
language: node_js
|
||||
node_js: "node"
|
||||
4
__mock__/electron.js
Normal file
4
__mock__/electron.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const ipcRenderer = {
|
||||
on: jest.fn(),
|
||||
send: jest.fn()
|
||||
};
|
||||
89
__tests__/actions/actions_plc.test.js
Normal file
89
__tests__/actions/actions_plc.test.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import { setPlcIpAddress, SET_PLC_IPADDRESS, ipcPlcInitializeSend, INITIALIZE_PLC, ipcPlcDetailsReceived, PLC_DATA_RECEIVED, ipcPlcErrorReceived, PLC_ERROR_RECEIVED } from "../../app/src/actions/actions_plc";
|
||||
import { ipcRenderer } from "electron";
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
describe("actions_plc", () => {
|
||||
describe("setPlcIpAddress", () => {
|
||||
let action;
|
||||
|
||||
beforeEach(() => {
|
||||
action = setPlcIpAddress("192.168.1.10");
|
||||
});
|
||||
|
||||
|
||||
it("has the correct type", () => {
|
||||
expect(action.type).toEqual(SET_PLC_IPADDRESS);
|
||||
});
|
||||
|
||||
it("has the correct payload", () => {
|
||||
expect(action.payload).toEqual("192.168.1.10");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ipcPlcInitializeSend", () => {
|
||||
let action;
|
||||
const tagList = {
|
||||
test: { name: "test", value: 100.0 }
|
||||
};
|
||||
beforeEach(() => {
|
||||
action = ipcPlcInitializeSend("192.168.1.10", tagList);
|
||||
});
|
||||
|
||||
it("has the correct type", () => {
|
||||
expect(action.type).toEqual(INITIALIZE_PLC);
|
||||
});
|
||||
|
||||
it("has the correct payload", () => {
|
||||
expect(action.payload).toBeTruthy();
|
||||
});
|
||||
|
||||
it("triggers the ipcRenderer.send function", () => {
|
||||
expect(ipcRenderer.send).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("ipcPlcDetailsReceived", () => {
|
||||
let action;
|
||||
const plcData = {
|
||||
ipAddress: "192.168.1.10",
|
||||
details: {
|
||||
name: "CLX"
|
||||
}
|
||||
};
|
||||
beforeEach(() => {
|
||||
action = ipcPlcDetailsReceived(undefined, plcData);
|
||||
});
|
||||
|
||||
|
||||
it("has the correct type", () => {
|
||||
expect(action.type).toEqual(PLC_DATA_RECEIVED);
|
||||
});
|
||||
|
||||
it("has the correct payload", () => {
|
||||
expect(action.payload).toEqual({
|
||||
ipAddress: "192.168.1.10",
|
||||
details: {
|
||||
name: "CLX"
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ipcPlcErrorReceived", () => {
|
||||
let action;
|
||||
beforeEach(() => {
|
||||
action = ipcPlcErrorReceived(undefined, "ERROR MESSAGE!");
|
||||
});
|
||||
|
||||
it("has the correct type", () => {
|
||||
expect(action.type).toEqual(PLC_ERROR_RECEIVED);
|
||||
});
|
||||
|
||||
it("has the correct payload", () => {
|
||||
expect(action.payload).toEqual("ERROR MESSAGE!");
|
||||
});
|
||||
});
|
||||
});
|
||||
112
__tests__/actions/actions_tags.test.js
Normal file
112
__tests__/actions/actions_tags.test.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { ipcTagUpdate, IPC_TAGSYNC, IPC_TAGUPDATE, ipcTagSync, storeNewTag, STORE_NEW_TAG, deleteTag, DELETE_TAG, writeTag, WRITE_TAG } from "../../app/src/actions/actions_tags";
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
describe("actions_tags", () => {
|
||||
describe("ipcTagUpdate", () => {
|
||||
let action;
|
||||
|
||||
const sampleTag = { state: {
|
||||
tag: {
|
||||
name: "test",
|
||||
value: 100.0
|
||||
}
|
||||
}};
|
||||
|
||||
beforeEach(() => {
|
||||
action = ipcTagUpdate(undefined, sampleTag);
|
||||
});
|
||||
|
||||
it("has the correct type", () => {
|
||||
expect(action.type).toEqual(IPC_TAGUPDATE);
|
||||
});
|
||||
|
||||
it("has the correct payload", () => {
|
||||
expect(action.payload).toEqual({ name: "test", value: 100.0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("ipcTagSync", () => {
|
||||
let action;
|
||||
|
||||
const sampleTag = { test: {
|
||||
name: "test",
|
||||
value: 100.0
|
||||
}};
|
||||
|
||||
beforeEach(() => {
|
||||
action = ipcTagSync("192.168.1.10", sampleTag);
|
||||
});
|
||||
|
||||
it("has the correct type", () => {
|
||||
expect(action.type).toEqual(IPC_TAGSYNC);
|
||||
});
|
||||
|
||||
it("has the correct payload", () => {
|
||||
expect(action.payload).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should execute the ipcRenderer.send function", () => {
|
||||
expect(ipcRenderer.send).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("storeNewTag", () => {
|
||||
let action;
|
||||
|
||||
const sampleTag = {
|
||||
name: "test",
|
||||
value: 100.0
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
action = storeNewTag(sampleTag);
|
||||
});
|
||||
|
||||
it("has the correct type", () => {
|
||||
expect(action.type).toEqual(STORE_NEW_TAG);
|
||||
});
|
||||
|
||||
it("has the correct payload", () => {
|
||||
expect(action.payload).toEqual({ name: "test", value: 100.0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteTag", () => {
|
||||
let action;
|
||||
|
||||
beforeEach(() => {
|
||||
action = deleteTag("test");
|
||||
});
|
||||
|
||||
it("has the correct type", () => {
|
||||
expect(action.type).toEqual(DELETE_TAG);
|
||||
});
|
||||
|
||||
it("has the correct payload", () => {
|
||||
expect(action.payload).toEqual("test");
|
||||
});
|
||||
});
|
||||
|
||||
describe("writeTag", () => {
|
||||
let action;
|
||||
|
||||
beforeEach(() => {
|
||||
action = writeTag("test", 100.0);
|
||||
});
|
||||
|
||||
it("has the correct type", () => {
|
||||
expect(action.type).toEqual(WRITE_TAG);
|
||||
});
|
||||
|
||||
it("has the correct payload", () => {
|
||||
expect(action.payload).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should execute the ipcRenderer.send function", () => {
|
||||
expect(ipcRenderer.send).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
44
__tests__/reducers/reducer_alarm.test.js
Normal file
44
__tests__/reducers/reducer_alarm.test.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
import AlarmReducer from "../../app/src/reducers/reducer_alarm";
|
||||
import { IPC_TAGUPDATE } from "../../app/src/actions/actions_tags";
|
||||
|
||||
describe("reducer_alarm", () => {
|
||||
it("should not change state on unused type", () => {
|
||||
expect(AlarmReducer(["test", "test"], "test")).toEqual(["test", "test"]);
|
||||
});
|
||||
|
||||
it("should return a default empty object", () => {
|
||||
expect(AlarmReducer(undefined, "test")).toEqual([]);
|
||||
});
|
||||
|
||||
describe("IPC_TAGUPDATE", () => {
|
||||
let action = {
|
||||
type: IPC_TAGUPDATE,
|
||||
payload: ""
|
||||
};
|
||||
|
||||
it("should not change active alarms if tag not in alarm list", () => {
|
||||
action.payload = { name: "test", value: false };
|
||||
expect(AlarmReducer([], action)).toEqual([]);
|
||||
});
|
||||
|
||||
it("should add the tag to active alarms if alarm tag value is true", () => {
|
||||
action.payload = { name: "alarm_ESTOP", value: true };
|
||||
expect(AlarmReducer([], action)).toEqual(["alarm_ESTOP"]);
|
||||
});
|
||||
|
||||
it("should not change active alarms if alarm tag value is false", () => {
|
||||
action.payload = { name: "alarm_ESTOP", value: false };
|
||||
expect(AlarmReducer(["test"], action)).toEqual(["test"]);
|
||||
});
|
||||
|
||||
it("should remove the event from the list if alarm tag value is false", () => {
|
||||
action.payload = { name: "alarm_ESTOP", value: false };
|
||||
expect(AlarmReducer(["alarm_ESTOP"], action)).toEqual([]);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
39
__tests__/reducers/reducer_events.test.js
Normal file
39
__tests__/reducers/reducer_events.test.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
import EventsReducer from "../../app/src/reducers/reducer_events";
|
||||
import { IPC_TAGUPDATE } from "../../app/src/actions/actions_tags";
|
||||
|
||||
describe("reducer_events", () => {
|
||||
it("should not change state on unused type", () => {
|
||||
expect(EventsReducer(["test", "test"], "test")).toEqual(["test", "test"]);
|
||||
});
|
||||
|
||||
it("should return a default empty object", () => {
|
||||
expect(EventsReducer(undefined, "test")).toEqual([]);
|
||||
});
|
||||
|
||||
describe("IPC_TAGUPDATE", () => {
|
||||
let action = {
|
||||
type: IPC_TAGUPDATE,
|
||||
payload: ""
|
||||
};
|
||||
|
||||
it("should add an event to the event log for an event tag true", () => {
|
||||
action.payload = { name: "cmd_Start", value: true };
|
||||
const newState = EventsReducer([], action);
|
||||
expect(newState[0].tag).toEqual("cmd_Start");
|
||||
});
|
||||
|
||||
it("should return unaltered event log if not in event log", () => {
|
||||
action.payload = { name: "test", value: true };
|
||||
expect(EventsReducer([], action)).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return unaltered event log if event tag value false", () => {
|
||||
action.payload = { name: "cmd_Start", value: false };
|
||||
expect(EventsReducer([], action)).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
52
__tests__/reducers/reducer_plc.test.js
Normal file
52
__tests__/reducers/reducer_plc.test.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
import PlcReducer from "../../app/src/reducers/reducer_plc";
|
||||
import { SET_PLC_IPADDRESS, PLC_DATA_RECEIVED, PLC_ERROR_RECEIVED } from "../../app/src/actions/actions_plc";
|
||||
|
||||
describe("PlcReducer", () => {
|
||||
it("should not change state on unused type", () => {
|
||||
expect(PlcReducer({test: "test"}, {})).toEqual({test: "test"});
|
||||
});
|
||||
|
||||
it("should return a default empty object", () => {
|
||||
expect(PlcReducer(undefined, "test")).toEqual({});
|
||||
});
|
||||
|
||||
describe("SET_PLC_IPADDRESS", ()=>{
|
||||
const action = {
|
||||
type: SET_PLC_IPADDRESS,
|
||||
payload: "192.168.1.10"
|
||||
};
|
||||
|
||||
it("should add ip address to state", ()=> {
|
||||
const state = PlcReducer({}, action);
|
||||
expect(state.ipAddress).toEqual(action.payload);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PLC_DATA_RECEIVED", ()=>{
|
||||
const action = {
|
||||
type: PLC_DATA_RECEIVED,
|
||||
payload: {name: "test"}
|
||||
};
|
||||
|
||||
it("should add details to state", ()=> {
|
||||
const state = PlcReducer({}, action);
|
||||
expect(state).toEqual(action.payload);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PLC_ERROR_RECEIVED", ()=>{
|
||||
const action = {
|
||||
type: PLC_ERROR_RECEIVED,
|
||||
payload: "PLC ERROR!"
|
||||
};
|
||||
|
||||
it("should add details to state", ()=> {
|
||||
const state = PlcReducer({}, action);
|
||||
expect(state.error).toEqual("PLC ERROR!");
|
||||
});
|
||||
});
|
||||
});
|
||||
41
__tests__/reducers/reducer_taghistory.test.js
Normal file
41
__tests__/reducers/reducer_taghistory.test.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import _ from "lodash";
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
import TagHistoryReducer from "../../app/src/reducers/reducer_taghistory";
|
||||
import { IPC_TAGUPDATE } from "../../app/src/actions/actions_tags";
|
||||
|
||||
describe("PlcReducer", () => {
|
||||
it("should not change state on unused type", () => {
|
||||
expect(TagHistoryReducer({test: "test"}, "test")).toEqual({test: "test"});
|
||||
});
|
||||
|
||||
it("should return a default empty object", () => {
|
||||
expect(TagHistoryReducer(undefined, "test")).toEqual({});
|
||||
});
|
||||
|
||||
describe("IPC_TAGUPDATE", () => {
|
||||
const action = {
|
||||
type: IPC_TAGUPDATE,
|
||||
payload: ""
|
||||
};
|
||||
|
||||
it("should add a history tag to an empty state", () => {
|
||||
action.payload = { name: "val_IntakePressure", value: 100 };
|
||||
expect(_.map(TagHistoryReducer({}, action), (x) => x)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should add another value to an existing history state", () => {
|
||||
action.payload = { name: "val_IntakePressure", value: 100 };
|
||||
const stateAfterAdd = TagHistoryReducer({}, action);
|
||||
expect(_.map(TagHistoryReducer(stateAfterAdd, action), (x) => x)[0]).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should not add to the history if the tag is not a historical tag", () => {
|
||||
action.payload = { name: "test", value: 100 };
|
||||
expect(_.map(TagHistoryReducer({}, action), (x) => x)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
63
__tests__/reducers/reducer_tags.test.js
Normal file
63
__tests__/reducers/reducer_tags.test.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
import TagsReducer from "../../app/src/reducers/reducer_tags";
|
||||
import { IPC_TAGUPDATE, STORE_NEW_TAG, DELETE_TAG } from "../../app/src/actions/actions_tags";
|
||||
|
||||
describe("PlcReducer", () => {
|
||||
it("should not change state on unused type", () => {
|
||||
expect(TagsReducer({test: "test"}, "test")).toEqual({test: "test"});
|
||||
});
|
||||
|
||||
it("should return a default empty object", () => {
|
||||
expect(TagsReducer(undefined, "test")).toEqual({});
|
||||
});
|
||||
|
||||
describe("IPC_TAGUPDATE", () => {
|
||||
const action = {
|
||||
type: IPC_TAGUPDATE,
|
||||
payload: { name: "test", value: 111.111 }
|
||||
};
|
||||
|
||||
it("should store a new value for a new tag", () => {
|
||||
const newState = TagsReducer({}, action);
|
||||
expect(newState).toEqual({ test: { name: "test", value: 111.111 }});
|
||||
});
|
||||
|
||||
it("should store a new value for an existing tag", () => {
|
||||
const existingState = { test: { name: "test", value: 0.00 }};
|
||||
const newState = TagsReducer(existingState, action);
|
||||
expect(newState).toEqual({test: { name: "test", value: 111.111 }});
|
||||
});
|
||||
});
|
||||
|
||||
describe("STORE_NEW_TAG", () => {
|
||||
const action = {
|
||||
type: STORE_NEW_TAG,
|
||||
payload: "test"
|
||||
};
|
||||
|
||||
it("should create a new object for a new tag", () => {
|
||||
const newState = TagsReducer({}, action);
|
||||
expect(newState).toEqual({ test: { name: "test" }});
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE_TAG", () => {
|
||||
const action = {
|
||||
type: DELETE_TAG,
|
||||
payload: "test"
|
||||
};
|
||||
|
||||
it("should remove the object in state with the key in the payload", () => {
|
||||
const initialState = {
|
||||
test: { name: "test", value: 10.0 },
|
||||
remain: { name: "remain", value: 1000 }
|
||||
};
|
||||
const newState = TagsReducer(initialState, action);
|
||||
expect(newState).toEqual({ remain: { name: "remain", value: 1000 } });
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
1384
app/build/bundle.js
1384
app/build/bundle.js
File diff suppressed because one or more lines are too long
@@ -54,3 +54,17 @@ body {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.alarms-active {
|
||||
animation: pulse 0.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
background-color: #FFFFFF !important;
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
100% {
|
||||
background-color: #dc3545 !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ipcRenderer } from "electron";
|
||||
export const SET_PLC_IPADDRESS = "SET_PLC_IPADDRESS";
|
||||
export const INITIALIZE_PLC = "INITIALIZE_PLC";
|
||||
export const PLC_DATA_RECEIVED = "PLC_DATA_RECEIVED";
|
||||
export const PLC_ERROR_RECEIVED = "PLC_ERROR_RECEIVED";
|
||||
|
||||
|
||||
export function setPlcIpAddress(ipAddress){
|
||||
@@ -21,10 +22,16 @@ export function ipcPlcInitializeSend(ipAddress, tagList){
|
||||
}
|
||||
|
||||
export function ipcPlcDetailsReceived(event, plcData){
|
||||
console.log("action creator got PLC data", plcData);
|
||||
return {
|
||||
type: PLC_DATA_RECEIVED,
|
||||
payload: plcData
|
||||
};
|
||||
}
|
||||
|
||||
export function ipcPlcErrorReceived(event, plcError){
|
||||
return {
|
||||
type: PLC_ERROR_RECEIVED,
|
||||
payload: plcError
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -251,6 +251,13 @@ class Controls extends Component {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Redux state to React props
|
||||
*
|
||||
* @param {Object} state
|
||||
*
|
||||
* @returns {Object} mapped state
|
||||
*/
|
||||
function mapStateToProps(state){
|
||||
return {
|
||||
tags: state.tags
|
||||
|
||||
75
app/src/components/EventLog.js
Normal file
75
app/src/components/EventLog.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import _ from "lodash";
|
||||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import {Timeline, TimelineEvent } from "react-event-timeline";
|
||||
import FontAwesomeIcon from "@fortawesome/react-fontawesome";
|
||||
import { faExclamationTriangle, faCalendar } from "@fortawesome/fontawesome-pro-regular";
|
||||
|
||||
import { writeTag } from "../actions/actions_tags";
|
||||
|
||||
class EventLog extends Component {
|
||||
|
||||
renderTimelineEvents(){
|
||||
return _.map(this.props.events, (event, key) => {
|
||||
let icon = <FontAwesomeIcon icon={faCalendar} />;
|
||||
let cardHeaderStyle = {backgroundColor: "#007bff"};
|
||||
if (event.eventType === "alarm"){
|
||||
icon = <FontAwesomeIcon icon={faExclamationTriangle} />;
|
||||
cardHeaderStyle = {backgroundColor: "#dc3545"};
|
||||
}
|
||||
|
||||
return (
|
||||
<TimelineEvent
|
||||
title={event.tag}
|
||||
createdAt={event.timestamp.toString()}
|
||||
icon={icon}
|
||||
iconColor={event.eventType === "alarm" ? "#dc3545" : "#007bff"}
|
||||
container="card"
|
||||
key={key}
|
||||
cardHeaderStyle={cardHeaderStyle}
|
||||
>
|
||||
{event.text}
|
||||
</TimelineEvent>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
render(){
|
||||
if (!this.props.events){
|
||||
return(
|
||||
<div>
|
||||
<h1>Loading...</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="container">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => this.props.writeTag("cmd_ResetAlarms", true)}
|
||||
style={{marginTop: "10px", marginBottom: "10px"}}
|
||||
>
|
||||
Reset Alarms
|
||||
</button>
|
||||
<hr />
|
||||
<Timeline>{this.renderTimelineEvents()}</Timeline>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Redux state to React props
|
||||
*
|
||||
* @param {Object} state
|
||||
*
|
||||
* @returns {Object} mapped state
|
||||
*/
|
||||
function mapStateToProps(state){
|
||||
return {
|
||||
events: state.events
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, { writeTag })(EventLog);
|
||||
@@ -3,8 +3,19 @@ import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import FontAwesomeIcon from "@fortawesome/react-fontawesome";
|
||||
import { faTint, faCog } from "@fortawesome/fontawesome-pro-regular";
|
||||
|
||||
/**
|
||||
* Class to render Header navigation bar
|
||||
*
|
||||
* @extends React.Component
|
||||
*/
|
||||
class Header extends Component {
|
||||
|
||||
|
||||
/**
|
||||
* renders the running state indicator button
|
||||
*
|
||||
* @returns {button} inactive running state button
|
||||
*/
|
||||
renderRunningState(){
|
||||
if(!this.props.tags ){
|
||||
return <span></span>;
|
||||
@@ -46,6 +57,36 @@ class Header extends Component {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* renders the alarm button
|
||||
*
|
||||
* @returns {button} button showing alarm status with link to alarm log
|
||||
*/
|
||||
renderAlarmButton(){
|
||||
if (!this.props.alarms){
|
||||
return <span></span>;
|
||||
}
|
||||
|
||||
const btnClassName = this.props.alarms.length > 0 ? "btn btn-danger alarms-active" : "btn btn-light";
|
||||
const btnText = this.props.alarms.length > 0 ? `Alarms Active: ${this.props.alarms.length}` : "No Alarms";
|
||||
|
||||
return (
|
||||
<Link
|
||||
style={{marginRight: "10px"}}
|
||||
to="/events"
|
||||
className={btnClassName}
|
||||
>{btnText}
|
||||
</Link>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* render method
|
||||
*
|
||||
* @returns markup
|
||||
*/
|
||||
render(){
|
||||
return (
|
||||
<nav className="navbar navbar-light navbar-expand-sm bg-light">
|
||||
@@ -58,6 +99,7 @@ class Header extends Component {
|
||||
<Link className="nav-item nav-link" to="/alltags">All Tags</Link>
|
||||
</div>
|
||||
<div className="navbar-nav ml-auto">
|
||||
<span className="navbar-text">{this.renderAlarmButton()}</span>
|
||||
<span className="navbar-text">{this.renderRunningState()}</span>
|
||||
<Link className="nav-item nav-link" to="/settings" id="settings-button"><FontAwesomeIcon icon={faCog} /></Link>
|
||||
</div>
|
||||
@@ -66,9 +108,17 @@ class Header extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Redux state to React props
|
||||
*
|
||||
* @param {Object} state
|
||||
*
|
||||
* @returns {Object} mapped state
|
||||
*/
|
||||
function mapStateToProps(state){
|
||||
return {
|
||||
tags: state.tags
|
||||
tags: state.tags,
|
||||
alarms: state.alarms
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,17 +2,167 @@ import React, { Component } from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
import { FlexibleWidthXYPlot, LineSeries, VerticalGridLines, HorizontalGridLines, XAxis, YAxis, DiscreteColorLegend } from "react-vis";
|
||||
import "../../../node_modules/react-vis/dist/style.css";
|
||||
// import "../../../node_modules/react-vis/dist/style.css";
|
||||
|
||||
import { color } from "d3-color";
|
||||
import { interpolateRgb } from "d3-interpolate";
|
||||
import LiquidFillGauge from "react-liquid-gauge";
|
||||
import { RadialGauge } from "react-canvas-gauges";
|
||||
|
||||
/** Class for Main Page
|
||||
*
|
||||
* @extends React.Component
|
||||
*/
|
||||
class Main extends Component {
|
||||
|
||||
getChartValues(values){
|
||||
/**
|
||||
* Map an array of objects to a graph-usable array of objects
|
||||
*
|
||||
* @param {Array} values - list of objects with timestame and value properties
|
||||
*
|
||||
* @returns {Array} list of objects with x and y properties
|
||||
*/
|
||||
mapTimestampAndValuePropToXY(values){
|
||||
return _.map(values, (val) => {
|
||||
return {x: val.timestamp, y: val.value};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a Liquid Gauge.
|
||||
*
|
||||
* @param {object} tag - the tag structure
|
||||
* @param {string} units - units for the tag
|
||||
* @param {number} maxValue - maximum value to be displayed
|
||||
* @param {number} label - label for the value (typically the tag name)
|
||||
* @returns {ReactElement} liquidGauge
|
||||
*/
|
||||
renderLiquidGauge(tag, units, maxValue, label){
|
||||
if (!tag){
|
||||
return <span></span>;
|
||||
}
|
||||
const tagValue = tag.value;
|
||||
const endColor = "#1F4788";
|
||||
const startColor = "#dc143c";
|
||||
|
||||
const radius = 100;
|
||||
const interpolate = interpolateRgb(startColor, endColor);
|
||||
const fillColor = interpolate(tagValue / maxValue);
|
||||
const gradientStops = [
|
||||
{
|
||||
key: "0%",
|
||||
stopColor: color(fillColor).darker(0.5).toString(),
|
||||
stopOpacity: 1,
|
||||
offset: "0%"
|
||||
},
|
||||
{
|
||||
key: "50%",
|
||||
stopColor: fillColor,
|
||||
stopOpacity: 0.75,
|
||||
offset: "50%"
|
||||
},
|
||||
{
|
||||
key: "100%",
|
||||
stopColor: color(fillColor).brighter(0.5).toString(),
|
||||
stopOpacity: 0.5,
|
||||
offset: "100%"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="col" style={{textAlign: "center"}}>
|
||||
<h3>{label}</h3>
|
||||
<LiquidFillGauge
|
||||
style={{ margin: "0 auto" }}
|
||||
width={radius * 2}
|
||||
height={radius * 2}
|
||||
value={tagValue / maxValue * 100}
|
||||
percent={units}
|
||||
textSize={1}
|
||||
textOffsetX={0}
|
||||
textOffsetY={0}
|
||||
textRenderer={(props) => {
|
||||
const value = Math.round(tagValue);
|
||||
const radius = Math.min(props.height / 2, props.width / 2);
|
||||
const textPixels = (props.textSize * radius / 2);
|
||||
const valueStyle = {
|
||||
fontSize: textPixels
|
||||
};
|
||||
const percentStyle = {
|
||||
fontSize: textPixels * 0.6
|
||||
};
|
||||
|
||||
return (
|
||||
<tspan>
|
||||
<tspan className="value" style={valueStyle}>{value}</tspan>
|
||||
<tspan style={percentStyle}>{props.percent}</tspan>
|
||||
</tspan>
|
||||
);
|
||||
}}
|
||||
riseAnimation
|
||||
waveAnimation
|
||||
waveFrequency={3}
|
||||
waveAmplitude={2}
|
||||
gradient
|
||||
gradientStops={gradientStops}
|
||||
circleStyle={{
|
||||
fill: fillColor
|
||||
}}
|
||||
waveStyle={{
|
||||
fill: fillColor
|
||||
}}
|
||||
textStyle={{
|
||||
fill: color("#444").toString(),
|
||||
fontFamily: "Arial"
|
||||
}}
|
||||
waveTextStyle={{
|
||||
fill: color("#fff").toString(),
|
||||
fontFamily: "Arial"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a radial gauge.
|
||||
*
|
||||
* @param {Object} tag - the tag structure
|
||||
* @param {string} units - units for the tag
|
||||
* @param {number} maxValue - maximum value to be displayed
|
||||
* @param {number} label - label for the value (typically the tag name)
|
||||
*/
|
||||
renderRadialGauge(tag, units, maxValue, label){
|
||||
if (!tag){
|
||||
return <span></span>;
|
||||
}
|
||||
|
||||
const ticks = [ 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0 ];
|
||||
|
||||
return (
|
||||
<div className="col" style={{textAlign: "center"}}>
|
||||
<h3>{label}</h3>
|
||||
<RadialGauge
|
||||
height={200}
|
||||
width={200}
|
||||
units={units}
|
||||
title={label}
|
||||
value={tag.value}
|
||||
minValue={0}
|
||||
maxValue={maxValue}
|
||||
majorTicks={ticks.map((t) => t * maxValue)}
|
||||
minorTicks={2}
|
||||
></RadialGauge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* render
|
||||
*
|
||||
* @returns {ReactElement} markup
|
||||
*/
|
||||
render(){
|
||||
if (!this.props.tagHistory){
|
||||
return(
|
||||
@@ -22,10 +172,33 @@ class Main extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.plc.error){
|
||||
return(
|
||||
<div className="container plc-error">
|
||||
<h1>PLC Error</h1>
|
||||
<h3>{this.props.plc.error}</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (Object.keys(this.props.tags).length === 0){
|
||||
return(
|
||||
<div className="container">
|
||||
<h1>Waiting for data...</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<h3>Process Values</h3>
|
||||
<div className="row" style={{marginBottom: "20px"}}>
|
||||
{this.renderLiquidGauge(this.props.tags.val_FluidLevel, "ft.", 500, "Level")}
|
||||
{this.renderRadialGauge(this.props.tags.val_Flowmeter_BarrelsPerDay, "BPD", 5000, "Flow Rate")}
|
||||
{this.renderRadialGauge(this.props.tags.val_TubingPressure, "PSI", 400, "Tubing Pressure")}
|
||||
</div>
|
||||
|
||||
<FlexibleWidthXYPlot
|
||||
height={300}
|
||||
xType="time"
|
||||
@@ -35,26 +208,38 @@ class Main extends Component {
|
||||
<XAxis />
|
||||
<YAxis />
|
||||
<LineSeries
|
||||
data={this.getChartValues(this.props.tagHistory.val_IntakePressure)}
|
||||
data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.val_IntakePressure)}
|
||||
/>
|
||||
<LineSeries
|
||||
data={this.getChartValues(this.props.tagHistory.val_Flowmeter)}
|
||||
data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.val_Flowmeter)}
|
||||
/>
|
||||
<LineSeries
|
||||
data={this.getChartValues(this.props.tagHistory.val_FluidLevel)}
|
||||
data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.val_FluidLevel)}
|
||||
/>
|
||||
<LineSeries
|
||||
data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.val_IntakeTemperature)}
|
||||
/>
|
||||
<LineSeries
|
||||
data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.val_TubingPressure)}
|
||||
/>
|
||||
</FlexibleWidthXYPlot>
|
||||
<DiscreteColorLegend
|
||||
items={[
|
||||
{title: "Intake Pressure"},
|
||||
{title: "Flowmeter"},
|
||||
{title: "Fluid Level"}
|
||||
{title: "Fluid Level"},
|
||||
{title: "Intake Temp"},
|
||||
{title: "Tubing Pressure"}
|
||||
]}
|
||||
orientation="horizontal"
|
||||
/>
|
||||
<hr />
|
||||
|
||||
<h3>VFD Data</h3>
|
||||
<div className="row" style={{marginBottom: "20px"}}>
|
||||
{this.renderRadialGauge(this.props.tags.VFD_OutCurrent, "A.", 100, "Current")}
|
||||
{this.renderRadialGauge(this.props.tags.VFD_SpeedFdbk, "Hz", 60, "Frequency")}
|
||||
</div>
|
||||
<FlexibleWidthXYPlot
|
||||
height={300}
|
||||
xType="time"
|
||||
@@ -64,16 +249,16 @@ class Main extends Component {
|
||||
<XAxis />
|
||||
<YAxis />
|
||||
<LineSeries
|
||||
data={this.getChartValues(this.props.tagHistory.VFD_OutCurrent)}
|
||||
data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.VFD_OutCurrent)}
|
||||
/>
|
||||
<LineSeries
|
||||
data={this.getChartValues(this.props.tagHistory.VFD_SpeedFdbk)}
|
||||
data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.VFD_SpeedFdbk)}
|
||||
/>
|
||||
<LineSeries
|
||||
data={this.getChartValues(this.props.tagHistory.VFD_OutPower)}
|
||||
data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.VFD_OutPower)}
|
||||
/>
|
||||
<LineSeries
|
||||
data={this.getChartValues(this.props.tagHistory.VFD_Temp)}
|
||||
data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.VFD_Temp)}
|
||||
/>
|
||||
</FlexibleWidthXYPlot>
|
||||
<DiscreteColorLegend
|
||||
@@ -91,10 +276,18 @@ class Main extends Component {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Redux state to React props
|
||||
*
|
||||
* @param {Object} state
|
||||
*
|
||||
* @returns {Object} mapped state
|
||||
*/
|
||||
function mapStateToProps(state){
|
||||
return {
|
||||
tags: state.tags,
|
||||
tagHistory: state.tagHistory
|
||||
tagHistory: state.tagHistory,
|
||||
plc: state.plc
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,13 @@ class Permissives extends Component {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Redux state to React props
|
||||
*
|
||||
* @param {Object} state
|
||||
*
|
||||
* @returns {Object} mapped state
|
||||
*/
|
||||
function mapStateToProps(state){
|
||||
return {
|
||||
tags: state.tags
|
||||
|
||||
@@ -3,6 +3,8 @@ import { connect } from "react-redux";
|
||||
import _ from "lodash";
|
||||
import { setPlcIpAddress, ipcPlcInitializeSend } from "../actions/actions_plc";
|
||||
import { storeNewTag, deleteTag } from "../actions/actions_tags";
|
||||
import FontAwesomeIcon from "@fortawesome/react-fontawesome";
|
||||
import { faTimesSquare } from "@fortawesome/fontawesome-pro-regular";
|
||||
|
||||
class Settings extends Component {
|
||||
constructor(props){
|
||||
@@ -17,30 +19,19 @@ class Settings extends Component {
|
||||
getTagList = () => {
|
||||
const { tags } = this.props;
|
||||
|
||||
let tableMiddle = _.map(tags, (tag) => {
|
||||
return (<tr key={tag.name}>
|
||||
<td>{tag.name}</td>
|
||||
<td>
|
||||
return _.map(tags, (tag) => {
|
||||
return (
|
||||
<li key={tag.name} className="list-group-item">
|
||||
{tag.name}
|
||||
<button
|
||||
className="btn red"
|
||||
className="btn btn-outline-danger float-right"
|
||||
onClick={(e) => this.onDeleteClick(e, tag.name)}
|
||||
><i className="material-icons">clear</i></button></td>
|
||||
</tr>);
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimesSquare} />
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tag</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tableMiddle}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
@@ -76,75 +67,70 @@ class Settings extends Component {
|
||||
}
|
||||
|
||||
onSave = (e) => {
|
||||
console.log(this.props);
|
||||
e.preventDefault();
|
||||
this.props.ipcPlcInitializeSend(this.props.plc.ipAddress, this.props.tags);
|
||||
this.props.history.push("/");
|
||||
}
|
||||
|
||||
render() {
|
||||
const ipAddressBtnClass = ((this.state.ipAddress === this.props.plc.ipAddress) || (this.state.ipAddress.length === 0)) ? "btn disabled right" : "btn right";
|
||||
const initializeBtnClass = (this.props.plc.ipAddress && _.map(this.props.tags, (t)=>t).length > 0) ? "btn" : "btn disabled";
|
||||
const ipAddressBtnClass = ((this.state.ipAddress === this.props.plc.ipAddress) || (this.state.ipAddress.length === 0)) ? "btn btn-primary disabled" : "btn btn-primary";
|
||||
const initializeBtnClass = (this.props.plc.ipAddress && _.map(this.props.tags, (t)=>t).length > 0) ? "btn btn-success" : "btn sbtn-success disabled";
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<ul className="collection with-header">
|
||||
<li className="collection-header">
|
||||
Settings
|
||||
</li>
|
||||
<form>
|
||||
<li className="collection-item">
|
||||
<p>IP Address</p>
|
||||
<input
|
||||
value={this.state.ipAddress}
|
||||
placeholder="PLC IP Address"
|
||||
onChange={this.onIpAddressInputChange}
|
||||
/>
|
||||
<button
|
||||
className={ipAddressBtnClass}
|
||||
onClick={(e) => this.sendIpAddress(e)}>
|
||||
Set IP Address
|
||||
</button>
|
||||
</li>
|
||||
</form>
|
||||
<div className="container">
|
||||
<h1>Settings</h1>
|
||||
<form className="form-inline mb-2">
|
||||
<div className="form-group">
|
||||
<label htmlFor="ipAddress">IP Address</label>
|
||||
<input
|
||||
id="ipAddress"
|
||||
className="form-control "
|
||||
value={this.state.ipAddress}
|
||||
placeholder="PLC IP Address"
|
||||
onChange={this.onIpAddressInputChange}
|
||||
/>
|
||||
<button
|
||||
className={ipAddressBtnClass}
|
||||
onClick={(e) => this.sendIpAddress(e)}>
|
||||
Set IP Address
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form>
|
||||
<li className="collection-item">
|
||||
<h4>Tag List</h4>
|
||||
{this.getTagList()}
|
||||
<input
|
||||
value={this.state.newTag}
|
||||
onChange={this.onNewTagChange}
|
||||
placeholder="New Tag Name..."
|
||||
/>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={(e) => this.onNewTagSubmit(e)}
|
||||
>Add Tag</button>
|
||||
</li>
|
||||
<li className="collection-item right">
|
||||
<button
|
||||
className={initializeBtnClass}
|
||||
onClick={(e) => this.onSave(e)}
|
||||
>Save</button>
|
||||
</li>
|
||||
</form>
|
||||
|
||||
<h4>Tag List</h4>
|
||||
<ul className="list-group">
|
||||
{this.getTagList()}
|
||||
</ul>
|
||||
<form>
|
||||
<input
|
||||
value={this.state.newTag}
|
||||
onChange={this.onNewTagChange}
|
||||
placeholder="New Tag Name..."
|
||||
/>
|
||||
<button
|
||||
className="btn btn-success float-right"
|
||||
onClick={(e) => this.onNewTagSubmit(e)}
|
||||
>Add Tag</button>
|
||||
|
||||
<button
|
||||
className={initializeBtnClass}
|
||||
onClick={(e) => this.onSave(e)}
|
||||
>Save</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
},
|
||||
pointer: {
|
||||
cursor: "pointer"
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Map Redux state to React props
|
||||
*
|
||||
* @param {Object} state
|
||||
*
|
||||
* @returns {Object} mapped state
|
||||
*/
|
||||
function mapStateToProps(state){
|
||||
return{
|
||||
tags: state.tags,
|
||||
|
||||
@@ -120,6 +120,13 @@ const styles = {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Map Redux state to React props
|
||||
*
|
||||
* @param {Object} state
|
||||
*
|
||||
* @returns {Object} mapped state
|
||||
*/
|
||||
function mapStateToProps(state){
|
||||
return {
|
||||
tags: state.tags,
|
||||
|
||||
@@ -51,3 +51,18 @@
|
||||
:global .control-button {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
:global .alarms-active {
|
||||
animation: pulse 0.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
background-color: #FFFFFF !important;
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
100% {
|
||||
background-color: #dc3545 !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,15 @@ import { combineReducers } from "redux";
|
||||
import TagsReducer from "./reducer_tags";
|
||||
import PlcReducer from "./reducer_plc";
|
||||
import TagHistoryReducer from "./reducer_taghistory";
|
||||
import AlarmsReducer from "./reducer_alarm";
|
||||
import EventsReducer from "./reducer_events";
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
tags: TagsReducer,
|
||||
tagHistory: TagHistoryReducer,
|
||||
plc: PlcReducer
|
||||
plc: PlcReducer,
|
||||
alarms: AlarmsReducer,
|
||||
events: EventsReducer
|
||||
});
|
||||
|
||||
|
||||
|
||||
33
app/src/reducers/reducer_alarm.js
Normal file
33
app/src/reducers/reducer_alarm.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import _ from "lodash";
|
||||
import { IPC_TAGUPDATE } from "../actions/actions_tags";
|
||||
import {event_tags as eventTags} from "../../../tagList.json";
|
||||
|
||||
|
||||
|
||||
export default function(state = [], action){
|
||||
switch (action.type) {
|
||||
case IPC_TAGUPDATE:
|
||||
// console.log(eventTags);
|
||||
const { name, value } = action.payload;
|
||||
const alarmTagList = _.map(_.filter(eventTags, (tag) => {
|
||||
return tag.eventType === "alarm";
|
||||
}), (ftag) => {
|
||||
return ftag.tag;
|
||||
});
|
||||
if (alarmTagList.includes(name)){
|
||||
if(value){
|
||||
return _.uniq(_.concat(state, name));
|
||||
} else {
|
||||
const newActiveState = _.filter(state, (tag) => {
|
||||
return tag !== name;
|
||||
});
|
||||
return newActiveState;
|
||||
}
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
28
app/src/reducers/reducer_events.js
Normal file
28
app/src/reducers/reducer_events.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import _ from "lodash";
|
||||
import { IPC_TAGUPDATE } from "../actions/actions_tags";
|
||||
import {event_tags as eventTags} from "../../../tagList.json";
|
||||
|
||||
|
||||
|
||||
export default function(state = [], action){
|
||||
switch (action.type) {
|
||||
case IPC_TAGUPDATE:
|
||||
const { name, value } = action.payload;
|
||||
const eventTagList = _.map(eventTags, (ftag) => {
|
||||
return ftag.tag;
|
||||
});
|
||||
if (eventTagList.includes(name)){
|
||||
if(value){
|
||||
const thisEvent = _.find(eventTags, (tag) => {
|
||||
return tag.tag === name;
|
||||
});
|
||||
const newHistory = _.concat([{...thisEvent, timestamp: new Date()}], state);
|
||||
return newHistory;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SET_PLC_IPADDRESS, PLC_DATA_RECEIVED } from "../actions/actions_plc";
|
||||
import { SET_PLC_IPADDRESS, PLC_DATA_RECEIVED, PLC_ERROR_RECEIVED } from "../actions/actions_plc";
|
||||
|
||||
export default function(state = {}, action){
|
||||
switch(action.type){
|
||||
@@ -6,9 +6,11 @@ export default function(state = {}, action){
|
||||
return { ...state, ipAddress: action.payload };
|
||||
|
||||
case PLC_DATA_RECEIVED:
|
||||
console.log(action.payload);
|
||||
return { ...state, ...action.payload };
|
||||
|
||||
case PLC_ERROR_RECEIVED:
|
||||
return { ...state, error: action.payload };
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import _ from "lodash";
|
||||
import { IPC_TAGUPDATE } from "../actions/actions_tags";
|
||||
import { historyTags } from "../renderer_process";
|
||||
import {history_tags as historyTags} from "../../../tagList.json";
|
||||
|
||||
|
||||
export default function(state = {}, action){
|
||||
|
||||
@@ -14,15 +14,17 @@ import Header from "./components/Header";
|
||||
import Permissives from "./components/Permissives";
|
||||
import Main from "./components/Main";
|
||||
import Controls from "./components/Controls";
|
||||
import EventLog from "./components/EventLog";
|
||||
|
||||
import { ipcTagUpdate } from "./actions/actions_tags";
|
||||
import { ipcPlcDetailsReceived } from "./actions/actions_plc";
|
||||
import { ipcPlcDetailsReceived, ipcPlcErrorReceived } from "./actions/actions_plc";
|
||||
|
||||
export const { history_tags: historyTags } = require("../../tagList.json");
|
||||
export const { history_tags: historyTags, event_tags: eventTags } = require("../../tagList.json");
|
||||
|
||||
const ipc = createIpc({
|
||||
"tag:valueupdate": ipcTagUpdate,
|
||||
"plc:connected": ipcPlcDetailsReceived
|
||||
"plc:connected": ipcPlcDetailsReceived,
|
||||
"plc:error": ipcPlcErrorReceived
|
||||
});
|
||||
|
||||
const createStoreWithMiddleware = applyMiddleware(ipc)(createStore);
|
||||
@@ -37,6 +39,7 @@ ReactDOM.render(
|
||||
<Route path="/permissives" component={Permissives} />
|
||||
<Route path="/alltags" component={TagsIndex} />
|
||||
<Route path="/controls" component={Controls} />
|
||||
<Route path="/events" component={EventLog} />
|
||||
<Route path="/" component={Main} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
@@ -4,18 +4,26 @@ const { Controller, Tag } = require("ethernet-ip");
|
||||
const _ = require("lodash");
|
||||
const { app, BrowserWindow, ipcMain } = electron;
|
||||
|
||||
const tagList = require("./tagList.json");
|
||||
|
||||
// To avoid being garbage collected
|
||||
let mainWindow;
|
||||
let PLC;
|
||||
|
||||
const tagList = require("./tagList.json");
|
||||
|
||||
app.on("ready", () => {
|
||||
|
||||
mainWindow = new BrowserWindow({});
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 1000
|
||||
});
|
||||
mainWindow.loadURL(`file://${__dirname}/app/index.html`);
|
||||
|
||||
initPLC("10.20.4.36", tagList.scan_list);
|
||||
// Wait for allowing react app to fully load
|
||||
// before starting to send initalized data.
|
||||
setTimeout(() => {
|
||||
initPLC("10.20.4.36", tagList.scan_list);
|
||||
}, 2000);
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -41,9 +49,11 @@ function initPLC(ipAddress, tagList){
|
||||
const properties = { ...PLC.properties, ipAddress};
|
||||
mainWindow.webContents.send("plc:connected", properties);
|
||||
PLC.scan().catch((err) => {
|
||||
mainWindow.webContents.send("plc:error", err.message);
|
||||
console.log(err);
|
||||
});
|
||||
}).catch((err) => {
|
||||
mainWindow.webContents.send("plc:error", err.message);
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
@@ -62,6 +72,7 @@ function initPLC(ipAddress, tagList){
|
||||
}
|
||||
|
||||
|
||||
|
||||
ipcMain.on("plc:initialize", (event, ipAddress, tagList) =>{
|
||||
// console.log("plc:initialize", ipAddress, tagList);
|
||||
initPLC(ipAddress, tagList);
|
||||
|
||||
26
package.json
26
package.json
@@ -9,7 +9,19 @@
|
||||
"serve": "electron .",
|
||||
"start": "npm-run-all --parallel wpackserve serve",
|
||||
"pack": "electron-builder --dir",
|
||||
"dist": "electron-builder -mwl"
|
||||
"dist": "electron-builder -mwl",
|
||||
"docgen": "jsdoc ./app/src/*/*.js -d ./app/out",
|
||||
"test": "jest --verbose --coverage",
|
||||
"test:watch": "npm run test -- --watch"
|
||||
},
|
||||
"jest": {
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(electron)/)"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"electron": "<rootDir>/__mock__/electron.js"
|
||||
},
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"author": "Patrick J. McDonagh",
|
||||
"repository": "HenryPump/MaxWaterSystem-Electron",
|
||||
@@ -28,20 +40,29 @@
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-plugin-react": "^7.7.0",
|
||||
"file-loader": "^1.1.10",
|
||||
"jest": "^22.4.3",
|
||||
"mini-css-extract-plugin": "^0.4.0",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"webpack": "^4.1.1",
|
||||
"webpack-cli": "^2.0.11"
|
||||
"webpack-cli": "^2.0.11",
|
||||
"enzyme": "^3.3.0",
|
||||
"enzyme-adapter-react-16": "^1.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome": "^1.1.5",
|
||||
"@fortawesome/fontawesome-pro-regular": "^5.0.9",
|
||||
"@fortawesome/fontawesome-pro-webfonts": "^1.0.5",
|
||||
"@fortawesome/react-fontawesome": "0.0.18",
|
||||
"d3-color": "^1.0.3",
|
||||
"d3-interpolate": "^1.1.6",
|
||||
"esdoc": "^1.0.4",
|
||||
"ethernet-ip": "^1.1.4",
|
||||
"lodash": "^4.17.5",
|
||||
"react": "^16.2.0",
|
||||
"react-canvas-gauges": "^1.2.1",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-event-timeline": "^1.5.1",
|
||||
"react-liquid-gauge": "^1.2.4",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-router": "^4.2.0",
|
||||
"react-router-dom": "^4.2.2",
|
||||
@@ -49,6 +70,7 @@
|
||||
"react-vis": "^1.9.2",
|
||||
"redux": "^3.7.2",
|
||||
"redux-electron-ipc": "^1.1.12",
|
||||
"redux-persist": "^5.9.1",
|
||||
"victory": "^0.25.7"
|
||||
},
|
||||
"build": {
|
||||
|
||||
77
tagList.json
77
tagList.json
@@ -90,16 +90,73 @@
|
||||
"VFD_SpeedRef",
|
||||
"VFD_Temp"
|
||||
],
|
||||
"alarm_tags":
|
||||
"event_tags":
|
||||
[
|
||||
"alarm_ESTOP",
|
||||
"alarm_Flowmeter",
|
||||
"alarm_FluidLevel",
|
||||
"alarm_IntakePressure",
|
||||
"alarm_IntakeTemperature",
|
||||
"alarm_Lockout",
|
||||
"alarm_MinSpeed",
|
||||
"alarm_TubingPressure",
|
||||
"alarm_VFD"
|
||||
{
|
||||
"tag": "alarm_ESTOP",
|
||||
"text": "E-Stop has been pressed",
|
||||
"valueTag": "",
|
||||
"eventType": "alarm"
|
||||
},
|
||||
{
|
||||
"tag": "alarm_Flowmeter",
|
||||
"text": "Flowmeter Alarm",
|
||||
"valueTag": "val_Flowmeter",
|
||||
"eventType": "alarm"
|
||||
},
|
||||
{
|
||||
"tag": "alarm_FluidLevel",
|
||||
"text": "Fluid Level Alarm",
|
||||
"valueTag": "val_FluidLevel",
|
||||
"eventType": "alarm"
|
||||
},
|
||||
{
|
||||
"tag": "alarm_IntakePressure",
|
||||
"text": "Intake Pressure Alarm",
|
||||
"valueTag": "val_IntakePressure",
|
||||
"eventType": "alarm"
|
||||
},
|
||||
{
|
||||
"tag": "alarm_IntakeTemperature",
|
||||
"text": "Intake Temperature Alarm",
|
||||
"valueTag": "val_IntakeTemperature",
|
||||
"eventType": "alarm"
|
||||
},
|
||||
{
|
||||
"tag": "alarm_MinSpeed",
|
||||
"text": "Minimum Speed Alarm",
|
||||
"valueTag": "VFD_SpeedFdbk",
|
||||
"eventType": "alarm"
|
||||
},
|
||||
{
|
||||
"tag": "alarm_TubingPressure",
|
||||
"text": "Tubing Pressure Alarm",
|
||||
"valueTag": "val_TubingPressure",
|
||||
"eventType": "alarm"
|
||||
},
|
||||
{
|
||||
"tag": "alarm_VFD",
|
||||
"text": "VFD Alarm",
|
||||
"valueTag": "sts_CurrentVFDFaultCode",
|
||||
"eventType": "alarm"
|
||||
},
|
||||
{
|
||||
"tag": "cmd_Start",
|
||||
"text": "Start button pressed",
|
||||
"valueTag": "",
|
||||
"eventType": "event"
|
||||
},
|
||||
{
|
||||
"tag": "cmd_Stop",
|
||||
"text": "Stop button pressed",
|
||||
"valueTag": "",
|
||||
"eventType": "event"
|
||||
},
|
||||
{
|
||||
"tag": "cmd_Restart",
|
||||
"text": "System restarted automatically",
|
||||
"valueTag": "",
|
||||
"eventType": "event"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user