mirror of
https://github.com/openmhz/trunk-server.git
synced 2026-11-12 16:57:39 -05:00
First Commit
This commit is contained in:
258
admin/src/client/Group/GroupModal.js
Normal file
258
admin/src/client/Group/GroupModal.js
Normal file
@@ -0,0 +1,258 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Form,
|
||||
Grid,
|
||||
Modal,
|
||||
Rail,
|
||||
Segment,
|
||||
List,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Icon,
|
||||
Table
|
||||
} from "semantic-ui-react";
|
||||
|
||||
// ----------------------------------------------------
|
||||
const requestMessageStyle = {
|
||||
color: "red"
|
||||
};
|
||||
|
||||
// ----------------------------------------------------
|
||||
class GroupModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
|
||||
// Set initial state
|
||||
this.state = {
|
||||
editGroup: false,
|
||||
groupName: "",
|
||||
group: {
|
||||
name: "",
|
||||
items: []
|
||||
},
|
||||
talkgroups: this.props.talkgroups ? this.props.talkgroups : []
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
//this.props.groupActions.fetchGroups(this.props.shortName);
|
||||
|
||||
this.props.talkgroupActions.fetchTalkgroups(this.props.shortName);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.talkgroups && nextProps.groups && nextProps.editGroupId) {
|
||||
var editGroup = nextProps.groups.find(
|
||||
group => group._id === nextProps.editGroupId
|
||||
);
|
||||
var items = [];
|
||||
|
||||
var talkgroups = nextProps.talkgroups;
|
||||
for (var i = 0; i < editGroup.talkgroups.length; i++) {
|
||||
const talkgroupId = editGroup.talkgroups[i];
|
||||
const index = talkgroups.findIndex(tg => tg.num === talkgroupId);
|
||||
if (index >= 0) {
|
||||
const item = {
|
||||
num: talkgroups[index].num,
|
||||
description: talkgroups[index].description
|
||||
};
|
||||
items.push(item);
|
||||
talkgroups = talkgroups
|
||||
.slice(0, index)
|
||||
.concat(talkgroups.slice(index + 1));
|
||||
}
|
||||
}
|
||||
const group = {name: editGroup.groupName, items: items}
|
||||
this.setState({group: group, talkgroups: talkgroups, groupName: editGroup.groupName});
|
||||
} else {
|
||||
this.setState({ talkgroups: nextProps.talkgroups })
|
||||
|
||||
}
|
||||
|
||||
if (!nextProps.editGroupId && (this.props.editGroupId != nextProps.editGroupId)) {
|
||||
this.setState({ groupName: "", group: {
|
||||
name: "",
|
||||
items: []
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
removeTalkgroup(event, index) {
|
||||
const item = {
|
||||
num: this.state.group.items[index].num,
|
||||
description: this.state.group.items[index].description
|
||||
};
|
||||
|
||||
const talkgroups = this.state.talkgroups.slice(0); // copy
|
||||
|
||||
for (var i = 0, len = talkgroups.length; i < len; i++) {
|
||||
if (item.num < talkgroups[i].num) {
|
||||
talkgroups.splice(i, 0, item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
group: {
|
||||
items: this.state.group.items
|
||||
.slice(0, index)
|
||||
.concat(this.state.group.items.slice(index + 1))
|
||||
},
|
||||
talkgroups: talkgroups
|
||||
});
|
||||
}
|
||||
|
||||
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
|
||||
|
||||
addTalkgroup(event, index) {
|
||||
const item = {
|
||||
num: this.state.talkgroups[index].num,
|
||||
description: this.state.talkgroups[index].description
|
||||
};
|
||||
|
||||
this.setState({
|
||||
group: { items: this.state.group.items.concat(item) },
|
||||
talkgroups: this.state.talkgroups
|
||||
.slice(0, index)
|
||||
.concat(this.state.talkgroups.slice(index + 1))
|
||||
});
|
||||
}
|
||||
handleClose = () => this.props.onClose(false);
|
||||
handleSubmit() {
|
||||
var talkgroupNums = this.state.group.items.map(a => a.num);
|
||||
const data = {
|
||||
shortName: this.props.shortName,
|
||||
groupName: this.state.groupName,
|
||||
talkgroups: JSON.stringify(talkgroupNums)
|
||||
};
|
||||
if (this.props.editGroupId) {
|
||||
const data = {
|
||||
_id: this.props.editGroupId,
|
||||
shortName: this.props.shortName,
|
||||
groupName: this.state.groupName,
|
||||
talkgroups: JSON.stringify(talkgroupNums)
|
||||
};
|
||||
|
||||
this.props.groupActions.updateGroup(data).then(requestMessage => {
|
||||
if (requestMessage) {
|
||||
// report to the user is there was a problem during registration
|
||||
this.setState({
|
||||
requestMessage
|
||||
});
|
||||
this.props.onClose(false);
|
||||
} else {
|
||||
this.props.onClose(true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const data = {
|
||||
shortName: this.props.shortName,
|
||||
groupName: this.state.groupName,
|
||||
talkgroups: JSON.stringify(talkgroupNums)
|
||||
};
|
||||
|
||||
this.props.groupActions.createGroup(data).then(requestMessage => {
|
||||
if (requestMessage) {
|
||||
// report to the user is there was a problem during registration
|
||||
this.setState({
|
||||
requestMessage
|
||||
});
|
||||
this.props.onClose(false);
|
||||
} else {
|
||||
this.props.onClose(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const talkgroups = this.state.talkgroups;
|
||||
const group = this.state.group.items;
|
||||
const railStyle = {
|
||||
top: "15px",
|
||||
minHeight: "200px"
|
||||
};
|
||||
return (
|
||||
<Modal open={this.props.open} onClose={this.handleClose} centered={false}>
|
||||
<Modal.Header>
|
||||
{this.props.editGroupId ? "Create Group" : "Edit Group"}
|
||||
</Modal.Header>
|
||||
<Modal.Content scrolling>
|
||||
<Modal.Description>
|
||||
<Grid container columns={2}>
|
||||
<Grid.Column>
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Number</Table.HeaderCell>
|
||||
|
||||
<Table.HeaderCell>Description</Table.HeaderCell>
|
||||
<Table.HeaderCell />
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{talkgroups &&
|
||||
talkgroups.map((talkgroup, i) => (
|
||||
<Table.Row link="true" key={talkgroup.num + "-" + i}>
|
||||
<Table.Cell>{talkgroup.num}</Table.Cell>
|
||||
|
||||
<Table.Cell>{talkgroup.description}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Icon
|
||||
link
|
||||
name="plus"
|
||||
onClick={e => this.addTalkgroup(e, i)}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
<Rail position="right">
|
||||
<Segment style={railStyle}>
|
||||
<Input
|
||||
name="groupName"
|
||||
onChange={this.handleInputChange}
|
||||
placeholder="Group Name"
|
||||
value={this.state.groupName}
|
||||
/>
|
||||
<List divided relaxed>
|
||||
{group.map((talkgroup, i) => (
|
||||
<List.Item key={"Group-" + i}>
|
||||
<List.Icon
|
||||
name="remove"
|
||||
verticalAlign="middle"
|
||||
link={true}
|
||||
onClick={e => this.removeTalkgroup(e, i)}
|
||||
/>
|
||||
<List.Content>{talkgroup.description}</List.Content>
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
<Button onClick={this.handleSubmit}>
|
||||
<Icon name="checkmark" />{" "}
|
||||
{this.props.editGroupId ? "Update" : "Save"}
|
||||
</Button>
|
||||
</Segment>
|
||||
</Rail>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={this.handleSubmit}>
|
||||
<Icon name="checkmark" />{" "}
|
||||
{this.props.editGroupId ? "Update" : "Save"}
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GroupModal;
|
||||
26
admin/src/client/Group/GroupModalContainer.js
Normal file
26
admin/src/client/Group/GroupModalContainer.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as groupActionCreators from "./group-actions"
|
||||
import * as talkgroupActionCreators from "../Talkgroups/talkgroup-actions"
|
||||
import { bindActionCreators } from 'redux'
|
||||
import GroupModal from "./GroupModal"
|
||||
|
||||
//groups: state.group.items[props.match.params.shortName],
|
||||
function mapStateToProps(state,props) {
|
||||
return {
|
||||
talkgroups: state.talkgroup.items[props.shortName],
|
||||
groups: state.group.items[props.shortName],
|
||||
shortName: props.shortName
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
groupActions: bindActionCreators(groupActionCreators,dispatch),
|
||||
talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(GroupModal)
|
||||
82
admin/src/client/Group/ListGroups.js
Normal file
82
admin/src/client/Group/ListGroups.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Form,
|
||||
Grid,
|
||||
Segment,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Icon,
|
||||
Table
|
||||
} from "semantic-ui-react";
|
||||
|
||||
// ----------------------------------------------------
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------------------
|
||||
class ListGroups extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
}
|
||||
|
||||
|
||||
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
|
||||
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
|
||||
render() {
|
||||
const groups = this.props.groups;
|
||||
if (groups) {
|
||||
return (
|
||||
|
||||
<Table >
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Name</Table.HeaderCell>
|
||||
<Table.HeaderCell>Talkgroup Count</Table.HeaderCell>
|
||||
<Table.HeaderCell></Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{groups.map((group, i) => (
|
||||
<Table.Row key={ "Group-" + i}>
|
||||
<Table.Cell>{group.groupName}</Table.Cell>
|
||||
<Table.Cell>{group.talkgroups.length}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Icon
|
||||
name="pencil alternate"
|
||||
link={true}
|
||||
onClick={e => this.props.editGroup(group._id)}
|
||||
/><Icon
|
||||
name="up arrow"
|
||||
link={true}
|
||||
onClick={e => this.props.reorderGroup(i, i-1)}
|
||||
/><Icon
|
||||
name="down arrow"
|
||||
link={true}
|
||||
onClick={e => this.props.reorderGroup(i, i+1)}
|
||||
/><Icon
|
||||
name="remove"
|
||||
link={true}
|
||||
onClick={e => this.props.deleteGroup(group._id)}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ListGroups;
|
||||
244
admin/src/client/Group/group-actions.js
Normal file
244
admin/src/client/Group/group-actions.js
Normal file
@@ -0,0 +1,244 @@
|
||||
import axios from "axios";
|
||||
//import { browserHistory } from "react-router"
|
||||
import * as types from "./group-constants";
|
||||
import { push } from 'connected-react-router';
|
||||
|
||||
// "Log In" action creators
|
||||
function beginCreateGroup() {
|
||||
return { type: types.CREATE_GROUP };
|
||||
}
|
||||
|
||||
function createGroupSuccess(data) {
|
||||
return {
|
||||
type: types.CREATE_SUCCESS_GROUP,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
function createGroupError(data) {
|
||||
return {
|
||||
type: types.CREATE_ERROR_GROUP,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function beginUpdateGroup() {
|
||||
return { type: types.UPDATE_GROUP };
|
||||
}
|
||||
|
||||
function updateGroupSuccess(data) {
|
||||
return { type: types.UPDATE_SUCCESS_GROUP, data };
|
||||
}
|
||||
|
||||
function updateGroupError() {
|
||||
return { type: types.UPDATE_ERROR_GROUP };
|
||||
}
|
||||
|
||||
|
||||
function beginDeleteGroup() {
|
||||
return { type: types.DELETE_GROUP };
|
||||
}
|
||||
|
||||
function deleteGroupSuccess(data) {
|
||||
return { type: types.DELETE_SUCCESS_GROUP, data };
|
||||
}
|
||||
|
||||
function deleteGroupError() {
|
||||
return { type: types.DELETE_ERROR_GROUP };
|
||||
}
|
||||
|
||||
function beginFetchGroup() {
|
||||
return { type: types.FETCH_GROUP };
|
||||
}
|
||||
|
||||
function fetchGroupSuccess(data) {
|
||||
return {
|
||||
type: types.FETCH_SUCCESS_GROUP,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
function fetchGroupError(data) {
|
||||
return {
|
||||
type: types.FETCH_ERROR_GROUP,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
function saveGroupOrderSuccess() {
|
||||
return { type: types.SAVE_ORDER_SUCCESS_GROUP };
|
||||
}
|
||||
|
||||
function saveGroupOrderError() {
|
||||
return { type: types.SAVE_ORDER_ERROR_GROUP };
|
||||
}
|
||||
|
||||
function makeUserRequest(method, data, api = "/login") {
|
||||
// returns a Promise
|
||||
return axios({
|
||||
method: method,
|
||||
url: api,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
export function changeUrl(url) {
|
||||
return dispatch => {
|
||||
dispatch(push(url));
|
||||
};
|
||||
}
|
||||
|
||||
// Example of an Async Action Creator
|
||||
// http://redux.js.org/docs/advanced/AsyncActions.html
|
||||
|
||||
export function updateGroup(data) {
|
||||
return dispatch => {
|
||||
dispatch(beginUpdateGroup());
|
||||
|
||||
return makeUserRequest("post", data, "/groups/" + data.shortName + "/" + data._id)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(updateGroupSuccess(response.data.group));
|
||||
} else {
|
||||
dispatch(updateGroupError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function shouldFetchGroups(state,shortName) {
|
||||
if (!state.group.items || !state.group.items[shortName]) {
|
||||
return true;
|
||||
} else if (state.group.isWaiting) {
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function reorderGroup(shortName, oldIndex, newIndex) {
|
||||
return (dispatch, getState) => {
|
||||
const data = {
|
||||
shortName: shortName,
|
||||
oldIndex: oldIndex,
|
||||
newIndex: newIndex
|
||||
};
|
||||
dispatch({
|
||||
type: types.REORDER_GROUP,
|
||||
data
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchGroups(shortName) {
|
||||
return (dispatch, getState) => {
|
||||
if (shouldFetchGroups(getState(), shortName)) {
|
||||
dispatch(beginFetchGroup());
|
||||
return axios
|
||||
.get("/groups/" + shortName)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
const data = { shortName: shortName, groups: response.data.groups };
|
||||
dispatch(fetchGroupSuccess(data));
|
||||
} else {
|
||||
dispatch(fetchGroupError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function saveGroupOrder(shortName, data) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: types.SAVE_ORDER_GROUP,
|
||||
data
|
||||
});
|
||||
|
||||
return makeUserRequest(
|
||||
"post",
|
||||
data,
|
||||
"/groups/" + shortName + "/reorder"
|
||||
)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(saveGroupOrderSuccess());
|
||||
} else {
|
||||
dispatch(saveGroupOrderError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteGroup(shortName, groupId) {
|
||||
return dispatch => {
|
||||
dispatch(beginDeleteGroup());
|
||||
|
||||
return makeUserRequest("delete", null, "/groups/" + shortName + "/" + groupId)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
const data = {shortName: shortName, groupId: groupId}
|
||||
dispatch(deleteGroupSuccess(data));
|
||||
} else {
|
||||
dispatch(deleteGroupError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function createGroup(data) {
|
||||
return dispatch => {
|
||||
dispatch(beginCreateGroup());
|
||||
|
||||
return makeUserRequest("post", data, "/groups/" + data.shortName)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(createGroupSuccess(response.data.group));
|
||||
} else {
|
||||
dispatch(createGroupError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
16
admin/src/client/Group/group-constants.js
Normal file
16
admin/src/client/Group/group-constants.js
Normal file
@@ -0,0 +1,16 @@
|
||||
export const CREATE_GROUP = "CREATE_GROUP"
|
||||
export const CREATE_SUCCESS_GROUP = "CREATE_SUCCESS_GROUP"
|
||||
export const CREATE_ERROR_GROUP = "CREATE_ERROR_GROUP"
|
||||
export const UPDATE_GROUP = "UPDATE_GROUP"
|
||||
export const UPDATE_SUCCESS_GROUP = "UPDATE_SUCCESS_GROUP"
|
||||
export const UPDATE_ERROR_GROUP = "UPDATE_ERROR_GROUP"
|
||||
export const DELETE_GROUP = "DELETE_GROUP"
|
||||
export const DELETE_SUCCESS_GROUP = "DELETE_SUCCESS_GROUP"
|
||||
export const DELETE_ERROR_GROUP = "DELETE_ERROR_GROUP"
|
||||
export const FETCH_GROUP = "FETCH_GROUP"
|
||||
export const FETCH_SUCCESS_GROUP = "FETCH_SUCCESS_GROUP"
|
||||
export const FETCH_ERROR_GROUP = "FETCH_ERROR_GROUP"
|
||||
export const REORDER_GROUP = "REORDER_GROUP"
|
||||
export const SAVE_ORDER_GROUP = "SAVE_ORDER_GROUP"
|
||||
export const SAVE_ORDER_SUCCESS_GROUP = "SAVE_ORDER_SUCCESS_GROUP"
|
||||
export const SAVE_ORDER_ERROR_GROUP = "SAVE_ORDER_ERROR_GROUP"
|
||||
128
admin/src/client/Group/group-reducer.js
Normal file
128
admin/src/client/Group/group-reducer.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
CREATE_GROUP,
|
||||
CREATE_SUCCESS_GROUP,
|
||||
CREATE_ERROR_GROUP,
|
||||
DELETE_GROUP,
|
||||
DELETE_SUCCESS_GROUP,
|
||||
DELETE_ERROR_GROUP,
|
||||
UPDATE_GROUP,
|
||||
UPDATE_SUCCESS_GROUP,
|
||||
UPDATE_ERROR_GROUP,
|
||||
FETCH_GROUP,
|
||||
FETCH_SUCCESS_GROUP,
|
||||
FETCH_ERROR_GROUP,
|
||||
REORDER_GROUP,
|
||||
SAVE_ORDER_GROUP,
|
||||
SAVE_ORDER_ERROR_GROUP,
|
||||
SAVE_ORDER_SUCCESS_GROUP
|
||||
} from "./group-constants";
|
||||
|
||||
function updateObject(oldObject, newValues) {
|
||||
// Encapsulate the idea of passing a new object as the first parameter
|
||||
// to Object.assign to ensure we correctly copy data instead of mutating
|
||||
return Object.assign({}, oldObject, newValues);
|
||||
}
|
||||
|
||||
function updateItemInArray(array, updatedItem) {
|
||||
const updatedItems = array.map(item => {
|
||||
if(item._id !== updatedItem._id) {
|
||||
// Since we only want to update one item, preserve all others as they are now
|
||||
return item;
|
||||
}
|
||||
return updatedItem;
|
||||
});
|
||||
|
||||
return updatedItems;
|
||||
}
|
||||
|
||||
const group = (
|
||||
state = {
|
||||
orderChange: false,
|
||||
isWaiting: false,
|
||||
items: {}},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
|
||||
case SAVE_ORDER_GROUP:
|
||||
case CREATE_GROUP:
|
||||
case FETCH_GROUP:
|
||||
case DELETE_GROUP:
|
||||
case UPDATE_GROUP:
|
||||
return Object.assign({}, state, { isWaiting: true});
|
||||
|
||||
case UPDATE_SUCCESS_GROUP:
|
||||
const updatedGroups = updateItemInArray(state.items[action.data.shortName], action.data);
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
items: {
|
||||
...state.items,
|
||||
[action.data.shortName]: updatedGroups
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
case CREATE_SUCCESS_GROUP:
|
||||
const newGroups = state.items[action.data.shortName].concat(action.data);
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
items: {
|
||||
...state.items,
|
||||
[action.data.shortName]: newGroups
|
||||
}
|
||||
});
|
||||
|
||||
case DELETE_SUCCESS_GROUP:
|
||||
const delGroups = state.items[action.data.shortName].filter(x => x._id != action.data.groupId);
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
items: {
|
||||
...state.items,
|
||||
[action.data.shortName]: delGroups
|
||||
}
|
||||
});
|
||||
|
||||
case FETCH_SUCCESS_GROUP:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
items: {
|
||||
...state.items,
|
||||
[action.data.shortName]: action.data.groups
|
||||
}
|
||||
});
|
||||
|
||||
case SAVE_ORDER_SUCCESS_GROUP:
|
||||
return Object.assign({}, state, {
|
||||
orderChange: false,
|
||||
isWaiting: false
|
||||
});
|
||||
|
||||
case SAVE_ORDER_ERROR_GROUP:
|
||||
case FETCH_ERROR_GROUP:
|
||||
case CREATE_ERROR_GROUP:
|
||||
case DELETE_ERROR_GROUP:
|
||||
case UPDATE_ERROR_GROUP:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false
|
||||
});
|
||||
|
||||
case REORDER_GROUP:
|
||||
if ((action.data.newIndex<0)||(action.data.newIndex>(state.items[action.data.shortName].length-1))) {
|
||||
return state
|
||||
}
|
||||
const moveGroups = state.items[action.data.shortName].slice();
|
||||
moveGroups.splice(action.data.newIndex, 0, moveGroups.splice(action.data.oldIndex, 1)[0]);
|
||||
return Object.assign({}, state, {
|
||||
orderChange: true,
|
||||
items: {
|
||||
...state.items,
|
||||
[action.data.shortName]: moveGroups
|
||||
}
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default group;
|
||||
150
admin/src/client/Permission/AddPermissionModal.js
Normal file
150
admin/src/client/Permission/AddPermissionModal.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {
|
||||
Container,
|
||||
Checkbox,
|
||||
Header,
|
||||
Form,
|
||||
Grid,
|
||||
Modal,
|
||||
Rail,
|
||||
Segment,
|
||||
List,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Icon,
|
||||
Table
|
||||
} from "semantic-ui-react";
|
||||
|
||||
// ----------------------------------------------------
|
||||
const requestMessageStyle = {
|
||||
color: "red"
|
||||
};
|
||||
|
||||
// ----------------------------------------------------
|
||||
class AddPermissionModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
// Set initial state
|
||||
this.state = {
|
||||
role: 5,
|
||||
email: "",
|
||||
error: ""
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
||||
}
|
||||
|
||||
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
|
||||
handleChange = (e, { newRole }) => this.setState({ newRole })
|
||||
|
||||
|
||||
|
||||
|
||||
handleClose = () => {
|
||||
this.setState({
|
||||
error: ""
|
||||
});
|
||||
this.props.onClose(false);
|
||||
}
|
||||
handleSubmit() {
|
||||
//if (this.props.updatePermissionId) {
|
||||
const data = {email: this.state.email, shortName: this.props.shortName, role: this.state.role }
|
||||
|
||||
this.props.permissionActions.addPermission(data).then(requestMessage => {
|
||||
if (requestMessage) {
|
||||
// report to the user is there was a problem during registration
|
||||
this.setState({
|
||||
error: requestMessage
|
||||
});
|
||||
//this.props.onClose(false);
|
||||
} else {
|
||||
this.setState({
|
||||
error: ""
|
||||
});
|
||||
this.props.onClose(true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
//}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const railStyle = {
|
||||
top: "15px",
|
||||
minHeight: "200px"
|
||||
};
|
||||
var errorMessage = "";
|
||||
|
||||
if (this.state.error) {
|
||||
errorMessage = (
|
||||
<Message negative>
|
||||
<Message.Header>That didn't work...</Message.Header>
|
||||
<p>{this.state.error}</p>
|
||||
</Message>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal size='tiny' open={this.props.open} onClose={this.handleClose} centered={false}>
|
||||
<Modal.Header>Select Role</Modal.Header>
|
||||
<Modal.Content >
|
||||
<Modal.Description>
|
||||
<Form>
|
||||
<Form.Field>
|
||||
Add User Access to System: {this.props.shortName}
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Input label="User's email" placeholder='user@domain.com' name='email' onChange={this.handleInputChange} />
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Checkbox
|
||||
radio
|
||||
label='User'
|
||||
name='role'
|
||||
value={5}
|
||||
checked={(this.state.role >= 0) && (this.state.role < 10)}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Checkbox
|
||||
radio
|
||||
label='Admin'
|
||||
name='role'
|
||||
value={15}
|
||||
checked={(this.state.role >= 10) && (this.state.role < 20)}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form>
|
||||
{errorMessage}
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={this.handleClose} >
|
||||
<Icon name='remove' /> Cancel
|
||||
</Button>
|
||||
<Button onClick={this.handleSubmit} >
|
||||
<Icon name='plus' /> Add
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AddPermissionModal;
|
||||
23
admin/src/client/Permission/AddPermissionModalContainer.js
Normal file
23
admin/src/client/Permission/AddPermissionModalContainer.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as permissionActionCreators from "./permission-actions"
|
||||
import { bindActionCreators } from 'redux'
|
||||
import AddPermissionModal from "./AddPermissionModal"
|
||||
|
||||
//groups: state.group.items[props.match.params.shortName],
|
||||
function mapStateToProps(state,props) {
|
||||
return {
|
||||
permissions: state.permission.items,
|
||||
shortName: props.shortName
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
permissionActions: bindActionCreators(permissionActionCreators,dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(AddPermissionModal)
|
||||
76
admin/src/client/Permission/ListPermissions.js
Normal file
76
admin/src/client/Permission/ListPermissions.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Form,
|
||||
Grid,
|
||||
Segment,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Icon,
|
||||
Table
|
||||
} from "semantic-ui-react";
|
||||
|
||||
// ----------------------------------------------------
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------------------
|
||||
class ListPermissions extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
}
|
||||
|
||||
|
||||
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
|
||||
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
|
||||
render() {
|
||||
const permissions = this.props.permissions;
|
||||
if (permissions) {
|
||||
return (
|
||||
|
||||
<Table >
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Name</Table.HeaderCell>
|
||||
<Table.HeaderCell>Email</Table.HeaderCell>
|
||||
<Table.HeaderCell>Role</Table.HeaderCell>
|
||||
<Table.HeaderCell></Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{permissions.map((permission, i) => (
|
||||
<Table.Row key={ "Group-" + i}>
|
||||
<Table.Cell>{permission.firstName} {permission.lastName}</Table.Cell>
|
||||
<Table.Cell>{permission.email}</Table.Cell>
|
||||
<Table.Cell>{permission.role}</Table.Cell>
|
||||
<Table.Cell><Icon
|
||||
name="pencil alternate"
|
||||
link={true}
|
||||
onClick={e => this.props.updatePermission(permission._id)}
|
||||
/>
|
||||
<Icon
|
||||
name="remove"
|
||||
link={true}
|
||||
onClick={e => this.props.deletePermission(permission)}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ListPermissions;
|
||||
134
admin/src/client/Permission/UpdatePermissionModal.js
Normal file
134
admin/src/client/Permission/UpdatePermissionModal.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {
|
||||
Container,
|
||||
Checkbox,
|
||||
Header,
|
||||
Form,
|
||||
Grid,
|
||||
Modal,
|
||||
Rail,
|
||||
Segment,
|
||||
List,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Icon,
|
||||
Table
|
||||
} from "semantic-ui-react";
|
||||
|
||||
// ----------------------------------------------------
|
||||
const requestMessageStyle = {
|
||||
color: "red"
|
||||
};
|
||||
|
||||
// ----------------------------------------------------
|
||||
class UpdatePermissionModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
// Set initial state
|
||||
this.state = {
|
||||
newRole: false,
|
||||
permission: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
||||
if (nextProps.permissions && nextProps.updatePermissionId) {
|
||||
var updatePermission = nextProps.permissions.find(permission=> permission._id === nextProps.updatePermissionId);
|
||||
|
||||
if (updatePermission) {
|
||||
this.setState({permission: updatePermission, newRole: updatePermission.role});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = (e, { name, value }) => this.setState({ newRole: value })
|
||||
|
||||
|
||||
|
||||
|
||||
handleClose = () => this.props.onClose(false);
|
||||
handleSubmit() {
|
||||
if (this.props.updatePermissionId) {
|
||||
const data = {permissionId: this.props.updatePermissionId, shortName: this.props.shortName, role: this.state.newRole }
|
||||
|
||||
this.props.permissionActions.update(data).then(requestMessage => {
|
||||
if (requestMessage) {
|
||||
// report to the user is there was a problem during registration
|
||||
this.setState({
|
||||
requestMessage
|
||||
});
|
||||
this.props.onClose(false);
|
||||
} else {
|
||||
this.props.onClose(true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const railStyle = {
|
||||
top: "15px",
|
||||
minHeight: "200px"
|
||||
};
|
||||
var user = ""
|
||||
if (this.state.permission) {
|
||||
user = this.state.permission.email
|
||||
}
|
||||
return (
|
||||
<Modal size='tiny' open={this.props.open} onClose={this.handleClose} centered={false}>
|
||||
<Modal.Header>Select Role</Modal.Header>
|
||||
<Modal.Content >
|
||||
<Modal.Description>
|
||||
<Form>
|
||||
<Form.Field>
|
||||
Set role for <b>{user}</b>:
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Checkbox
|
||||
radio
|
||||
label='User'
|
||||
name='checkboxRadioGroup'
|
||||
value={5}
|
||||
checked={(this.state.newRole >= 0) && (this.state.newRole < 10)}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Checkbox
|
||||
radio
|
||||
label='Admin'
|
||||
name='checkboxRadioGroup'
|
||||
value={15}
|
||||
checked={(this.state.newRole >= 10) && (this.state.newRole < 20)}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form>
|
||||
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={this.handleSubmit} >
|
||||
<Icon name='checkmark' /> Update
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UpdatePermissionModal;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as permissionActionCreators from "./permission-actions"
|
||||
import { bindActionCreators } from 'redux'
|
||||
import UpdatePermissionModal from "./UpdatePermissionModal"
|
||||
|
||||
//groups: state.group.items[props.match.params.shortName],
|
||||
function mapStateToProps(state,props) {
|
||||
return {
|
||||
permissions: state.permission.items,
|
||||
shortName: props.shortName
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
permissionActions: bindActionCreators(permissionActionCreators,dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(UpdatePermissionModal)
|
||||
166
admin/src/client/Permission/permission-actions.js
Normal file
166
admin/src/client/Permission/permission-actions.js
Normal file
@@ -0,0 +1,166 @@
|
||||
import axios from "axios";
|
||||
//import { browserHistory } from "react-router"
|
||||
import * as types from "./permission-constants";
|
||||
|
||||
function beginAddPermission() {
|
||||
return { type: types.ADD_PERMISSION };
|
||||
}
|
||||
|
||||
function addPermissionSuccess(data) {
|
||||
return { type: types.ADD_PERMISSION_SUCCESS, data };
|
||||
}
|
||||
|
||||
function addPermissionError() {
|
||||
return { type: types.ADD_PERMISSION_ERROR };
|
||||
}
|
||||
|
||||
function beginDeletePermission() {
|
||||
return { type: types.DELETE_PERMISSION };
|
||||
}
|
||||
|
||||
function deletePermissionSuccess(data) {
|
||||
return { type: types.DELETE_PERMISSION_SUCCESS, data };
|
||||
}
|
||||
|
||||
function deletePermissionError() {
|
||||
return { type: types.DELETE_PERMISSION_ERROR };
|
||||
}
|
||||
|
||||
|
||||
function beginUpdateRole() {
|
||||
return { type: types.UPDATE_ROLE };
|
||||
}
|
||||
|
||||
function updateRoleSuccess(data) {
|
||||
return { type: types.UPDATE_ROLE_SUCCESS, data };
|
||||
}
|
||||
|
||||
function updateRoleError() {
|
||||
return { type: types.UPDATE_ROLE_ERROR };
|
||||
}
|
||||
|
||||
// "Log In" action creators
|
||||
function beginFetchPermission() {
|
||||
return { type: types.FETCH_PERMISSION };
|
||||
}
|
||||
|
||||
function fetchPermissionError(data) {
|
||||
return { type: types.FETCH_ERROR_PERMISSION,
|
||||
data };
|
||||
}
|
||||
|
||||
function fetchPermissionSuccess(data) {
|
||||
return { type: types.FETCH_SUCCESS_PERMISSION,
|
||||
data };
|
||||
}
|
||||
|
||||
function shouldFetch(state) {
|
||||
if ((!state.permission.items) || (state.permission.items.length <1)) {
|
||||
return true
|
||||
} else if (state.permission.isWaiting) {
|
||||
return false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function fetch(shortName) {
|
||||
return (dispatch, getState) => {
|
||||
if (shouldFetch(getState())) {
|
||||
dispatch(beginFetchPermission());
|
||||
|
||||
return axios
|
||||
.get("/permissions/"+shortName)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(fetchPermissionSuccess(response.data.permissions));
|
||||
} else {
|
||||
dispatch(fetchPermissionError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function update(data) {
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch(beginUpdateRole());
|
||||
var url = "/permissions/" + data.shortName + "/" + data.permissionId;
|
||||
return axios.post( url, data)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(updateRoleSuccess(response.data.permission));
|
||||
} else {
|
||||
dispatch(updateRoleError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
export function addPermission(data) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(beginAddPermission());
|
||||
var url = "/permissions/" + data.shortName;
|
||||
return axios.post( url, data)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(addPermissionSuccess(response.data.permission));
|
||||
} else {
|
||||
dispatch(addPermissionError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function deletePermission(data) {
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch(beginDeletePermission());
|
||||
var url = "/permissions/" + data.shortName + "/" + data.permissionId;
|
||||
return axios.delete( url)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(deletePermissionSuccess(data.permissionId));
|
||||
} else {
|
||||
dispatch(deletePermissionError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
12
admin/src/client/Permission/permission-constants.js
Normal file
12
admin/src/client/Permission/permission-constants.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export const ADD_PERMISSION = "ADD_PERMISSION"
|
||||
export const ADD_PERMISSION_ERROR = "ADD_PERMISSION_ERROR"
|
||||
export const ADD_PERMISSION_SUCCESS = "ADD_PERMISSION_SUCCESS"
|
||||
export const FETCH_PERMISSION = "FETCH_PERMISSION"
|
||||
export const FETCH_ERROR_PERMISSION = "FETCH_ERROR_PERMISSION"
|
||||
export const FETCH_SUCCESS_PERMISSION = "FETCH_SUCCESS_PERMISSION"
|
||||
export const UPDATE_ROLE = "UPDATE_ROLE"
|
||||
export const UPDATE_ROLE_ERROR = "UPDATE_ROLE_ERROR"
|
||||
export const UPDATE_ROLE_SUCCESS = "UPDATE_ROLE SUCCESS"
|
||||
export const DELETE_PERMISSION = "DELETE_PERMISSION"
|
||||
export const DELETE_PERMISSION_ERROR = "DELETE_PERMISSION_ERROR"
|
||||
export const DELETE_PERMISSION_SUCCESS = "DELETE_PERMISSION_SUCCESS"
|
||||
87
admin/src/client/Permission/permission-reducer.js
Normal file
87
admin/src/client/Permission/permission-reducer.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
ADD_PERMISSION,
|
||||
ADD_PERMISSION_SUCCESS,
|
||||
ADD_PERMISSION_ERROR,
|
||||
DELETE_PERMISSION,
|
||||
DELETE_PERMISSION_SUCCESS,
|
||||
DELETE_PERMISSION_ERROR,
|
||||
FETCH_PERMISSION,
|
||||
FETCH_SUCCESS_PERMISSION,
|
||||
FETCH_ERROR_PERMISSION,
|
||||
UPDATE_ROLE,
|
||||
UPDATE_ROLE_ERROR,
|
||||
UPDATE_ROLE_SUCCESS
|
||||
} from "./permission-constants";
|
||||
|
||||
function updateRoleInArray(array, updatedItem) {
|
||||
const updatedItems = array.map(item => {
|
||||
if(item._id !== updatedItem._id) {
|
||||
// Since we only want to update one item, preserve all others as they are now
|
||||
return item;
|
||||
}
|
||||
item.role = updatedItem.role;
|
||||
return item;
|
||||
});
|
||||
|
||||
return updatedItems;
|
||||
}
|
||||
|
||||
function deleteItemInArray(array, _id) {
|
||||
const updatedItems = array.filter(function (item) {
|
||||
return item._id !== _id;
|
||||
});
|
||||
|
||||
return updatedItems;
|
||||
}
|
||||
|
||||
const permission = (
|
||||
state = {
|
||||
isWaiting: false,
|
||||
items: []
|
||||
},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
|
||||
|
||||
case ADD_PERMISSION_SUCCESS:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
items: [...state.items, action.data]
|
||||
});
|
||||
case DELETE_PERMISSION_SUCCESS:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
items: deleteItemInArray(state.items, action.data)
|
||||
});
|
||||
case UPDATE_ROLE_SUCCESS:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
items: updateRoleInArray(state.items, action.data)
|
||||
});
|
||||
case FETCH_SUCCESS_PERMISSION:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
items: action.data
|
||||
});
|
||||
|
||||
case DELETE_PERMISSION_ERROR:
|
||||
case ADD_PERMISSION_ERROR:
|
||||
case UPDATE_ROLE_ERROR:
|
||||
case FETCH_ERROR_PERMISSION:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false
|
||||
});
|
||||
|
||||
case DELETE_PERMISSION:
|
||||
case ADD_PERMISSION:
|
||||
case UPDATE_ROLE:
|
||||
case FETCH_PERMISSION:
|
||||
return Object.assign({}, state, { isWaiting: true});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default permission;
|
||||
100
admin/src/client/System/CallChart.js
Normal file
100
admin/src/client/System/CallChart.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import React, { Component } from "react";
|
||||
import { Group } from '@vx/group';
|
||||
import { curveStep} from '@vx/curve';
|
||||
import { LinePath } from '@vx/shape';
|
||||
import { scaleLinear, scaleBand, scaleTime } from '@vx/scale';
|
||||
import { timeParse, timeFormat } from 'd3-time-format';
|
||||
import { AxisBottom, AxisLeft } from '@vx/axis';
|
||||
import { Dimmer, Loader } from "semantic-ui-react";
|
||||
|
||||
|
||||
class CallChart extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
var callData = this.props.data;
|
||||
if (!callData) {
|
||||
return (
|
||||
|
||||
<Loader size='large'>Loading</Loader>
|
||||
|
||||
)
|
||||
}
|
||||
var callTotals = [];
|
||||
var errorTotals = [];
|
||||
|
||||
// Define the graph dimensions and margins
|
||||
const width = this.props.width;
|
||||
const height = 500;
|
||||
const margin = { top: 20, bottom: 40, left: 40, right: 20 };
|
||||
|
||||
// Then we'll create some bounds
|
||||
const xMax = width - margin.left - margin.right;
|
||||
const yMax = height - margin.top - margin.bottom;
|
||||
|
||||
// accessors
|
||||
const x = d => d.x;
|
||||
const y = d => d.y;
|
||||
|
||||
|
||||
|
||||
// Compose together the scale and accessor functions to get point functions
|
||||
const dotColor = n => {
|
||||
var colores_g = ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395", "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac"];
|
||||
return colores_g[n % colores_g.length];
|
||||
}
|
||||
|
||||
const compose = (scale, accessor) => (data) => scale(accessor(data));
|
||||
var xScale, yScale, xPoint, yPoint;
|
||||
if (callData) {
|
||||
// We'll use some mock data from `@vx/mock-data` for this.
|
||||
callTotals = callData.callTotals;
|
||||
errorTotals = callData.errorTotals;
|
||||
// scales
|
||||
xScale = scaleTime({
|
||||
range: [0, xMax],
|
||||
domain: [callData.minDate, callData.maxDate],
|
||||
tickFormat: () => (val) => formatDate(val)
|
||||
});
|
||||
yScale = scaleLinear({
|
||||
range: [yMax, 0],
|
||||
domain: [0, callData.maxValue],
|
||||
});
|
||||
xPoint = compose(xScale, x);
|
||||
yPoint = compose(yScale, y);
|
||||
}
|
||||
|
||||
return (
|
||||
<svg width={width} height={height}>
|
||||
<Group left={margin.left} top={margin.top} >
|
||||
<AxisBottom top={yMax} scale={xScale} numTicks={width > 520 ? 10 : 5} />
|
||||
<AxisLeft scale={yScale} />
|
||||
<LinePath
|
||||
data={callTotals}
|
||||
xScale={xScale}
|
||||
yScale={yScale}
|
||||
x={x}
|
||||
y={y}
|
||||
strokeWidth={2}
|
||||
curve={curveStep}
|
||||
/>
|
||||
<LinePath
|
||||
data={errorTotals}
|
||||
xScale={xScale}
|
||||
yScale={yScale}
|
||||
x={x}
|
||||
y={y}
|
||||
strokeWidth={2}
|
||||
stroke="red"
|
||||
curve={curveStep}
|
||||
/>
|
||||
</Group>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
export default CallChart;
|
||||
82
admin/src/client/System/CreateSystem.js
Normal file
82
admin/src/client/System/CreateSystem.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Form,
|
||||
Grid,
|
||||
Segment,
|
||||
Input,
|
||||
Button,
|
||||
Divider,
|
||||
Message,
|
||||
Icon
|
||||
} from "semantic-ui-react";
|
||||
|
||||
import SystemForm from "./SystemForm";
|
||||
|
||||
// ----------------------------------------------------
|
||||
const requestMessageStyle = {
|
||||
color: "red"
|
||||
};
|
||||
|
||||
// ----------------------------------------------------
|
||||
class CreateSystem extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
}
|
||||
|
||||
state = {
|
||||
requestMessage: ""
|
||||
}
|
||||
|
||||
|
||||
|
||||
handleSubmit(system) {
|
||||
this.props
|
||||
.createSystem(system)
|
||||
.then(requestMessage => {
|
||||
if (requestMessage) {
|
||||
// report to the user is there was a problem during registration
|
||||
this.setState({
|
||||
requestMessage
|
||||
});
|
||||
} else {
|
||||
this.props.changeUrl("/list-systems")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Container text>
|
||||
<Header as="h2">Join Us!</Header>
|
||||
<p>
|
||||
Record the your community's radio systems and share them! All it
|
||||
takes is a spare computer and a cheap SDR. The software you need,
|
||||
along with an explanation of how to set it up, is available{" "}
|
||||
<a href="https://github.com/robotastic/trunk-recorder">here</a>.
|
||||
</p>
|
||||
<p>
|
||||
After you have the Trunk Recorder software up and running, come
|
||||
back here and sign-up for an account so you can share your
|
||||
recordings
|
||||
</p>
|
||||
<p> - Luke</p>
|
||||
<Divider />
|
||||
<p></p>
|
||||
</Container>
|
||||
|
||||
<Container text>
|
||||
<Header as="h1">Create System</Header>
|
||||
<SystemForm onSubmit={this.handleSubmit} requestMessage={this.state.requestMessage} screenName={this.props.user.screenName}/>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CreateSystem;
|
||||
14
admin/src/client/System/CreateSystemContainer.js
Normal file
14
admin/src/client/System/CreateSystemContainer.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as systemActions from "./system-actions"
|
||||
import CreateSystem from "./CreateSystem"
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
user: state.user
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
systemActions
|
||||
)(CreateSystem)
|
||||
48
admin/src/client/System/EditSystem.js
Normal file
48
admin/src/client/System/EditSystem.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { Component } from "react"
|
||||
import SystemForm from "./SystemForm";
|
||||
import {
|
||||
Container,
|
||||
Header
|
||||
} from "semantic-ui-react";
|
||||
|
||||
class EditSystem extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSystemSubmit = this.handleSystemSubmit.bind(this);
|
||||
}
|
||||
|
||||
state = {
|
||||
requestMessage: ""
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props
|
||||
this.props.fetchSystems()
|
||||
}
|
||||
|
||||
handleSystemSubmit(system) {
|
||||
this.props
|
||||
.updateSystem(system)
|
||||
.then(requestMessage => {
|
||||
if (requestMessage) {
|
||||
// report to the user is there was a problem during registration
|
||||
this.setState({
|
||||
requestMessage
|
||||
});
|
||||
} else {
|
||||
this.props.changeUrl("/system/"+system.shortName)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return(
|
||||
<Container text>
|
||||
<Header as="h1">Update System</Header>
|
||||
<SystemForm onSubmit={this.handleSystemSubmit} system={this.props.system} isEditing={true} requestMessage={this.state.requestMessage} screenName={this.props.user.screenName}/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default EditSystem
|
||||
18
admin/src/client/System/EditSystemContainer.js
Normal file
18
admin/src/client/System/EditSystemContainer.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { connect } from "react-redux"
|
||||
import EditSystem from "./EditSystem"
|
||||
import * as systemActions from "./system-actions"
|
||||
|
||||
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
|
||||
return {
|
||||
system: state.system.items.find((item) => item.shortName === props.match.params.shortName),
|
||||
user: state.user
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
systemActions
|
||||
)(EditSystem)
|
||||
133
admin/src/client/System/ErrorChart.js
Normal file
133
admin/src/client/System/ErrorChart.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import React, { Component } from "react";
|
||||
import { Group } from '@vx/group';
|
||||
import { GlyphCircle } from '@vx/glyph';
|
||||
import { scaleLinear, scaleBand, scaleTime, scaleOrdinal } from '@vx/scale';
|
||||
import { timeParse, timeFormat } from 'd3-time-format';
|
||||
import { AxisBottom, AxisLeft } from '@vx/axis';
|
||||
import { Dimmer, Loader } from "semantic-ui-react";
|
||||
import {
|
||||
LegendOrdinal
|
||||
|
||||
} from '@vx/legend';
|
||||
class ErrorChart extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
var errorData = this.props.data;
|
||||
if (!errorData) {
|
||||
return (
|
||||
<Loader size='large'>Loading</Loader>
|
||||
)
|
||||
}
|
||||
var data = [];
|
||||
const parseDate = timeParse("%Y%m%d");
|
||||
const format = timeFormat("%b %d");
|
||||
const formatDate = (date) => format(parseDate(date));
|
||||
|
||||
// Define the graph dimensions and margins
|
||||
const width = this.props.width;
|
||||
const height = 500;
|
||||
const margin = { top: 20, bottom: 40, left: 40, right: 20 };
|
||||
|
||||
// Then we'll create some bounds
|
||||
const xMax = width - margin.left - margin.right;
|
||||
const yMax = height - margin.top - margin.bottom;
|
||||
|
||||
// accessors
|
||||
const x = d => d.x;
|
||||
const y = d => d.y;
|
||||
|
||||
|
||||
|
||||
// Compose together the scale and accessor functions to get point functions
|
||||
const dotColor = n => {
|
||||
var colores_g = ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395", "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac"];
|
||||
return colores_g[n % colores_g.length];
|
||||
}
|
||||
|
||||
const compose = (scale, accessor) => (data) => scale(accessor(data));
|
||||
var zScale = scaleOrdinal({
|
||||
domain: errorData.legend,
|
||||
range: ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395", "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac"]
|
||||
});
|
||||
var xScale, yScale, xPoint, yPoint;
|
||||
if (errorData) {
|
||||
// We'll use some mock data from `@vx/mock-data` for this.
|
||||
data = errorData.data;
|
||||
// scales
|
||||
xScale = scaleTime({
|
||||
range: [0, xMax],
|
||||
domain: [errorData.minDate, errorData.maxDate],
|
||||
tickFormat: () => (val) => formatDate(val)
|
||||
});
|
||||
yScale = scaleLinear({
|
||||
range: [yMax, 0],
|
||||
domain: [0, errorData.maxValue],
|
||||
});
|
||||
zScale = scaleOrdinal({
|
||||
domain: errorData.legend,
|
||||
range: ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395", "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac"]
|
||||
});
|
||||
xPoint = compose(xScale, x);
|
||||
yPoint = compose(yScale, y);
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
width: 900
|
||||
}}>
|
||||
<svg width={width} height={height} style={{
|
||||
float: "left"
|
||||
}}>
|
||||
<Group left={margin.left} top={margin.top} >
|
||||
<AxisBottom top={yMax} scale={xScale} numTicks={width > 520 ? 10 : 5} />
|
||||
<AxisLeft scale={yScale} />
|
||||
{data.map((band, i) => {
|
||||
return (
|
||||
<Group key={`band-${i}`}>
|
||||
{band.data.map((point, j) => {
|
||||
return (
|
||||
<GlyphCircle
|
||||
className="dot"
|
||||
key={`point-${point.x}-${j}`}
|
||||
fill={dotColor(i)}
|
||||
left={xScale(x(point))}
|
||||
top={yScale(y(point))}
|
||||
size={6.0}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
</svg>
|
||||
<div
|
||||
style={{
|
||||
float: "right"
|
||||
}}
|
||||
>
|
||||
<LegendOrdinal
|
||||
scale={zScale}
|
||||
labelMargin="0 15px 0 0"
|
||||
onMouseOver={data => event => {
|
||||
console.log(
|
||||
`mouse over: ${data.text}`,
|
||||
`index: ${data.index}`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
export default ErrorChart;
|
||||
70
admin/src/client/System/ListSystems.js
Normal file
70
admin/src/client/System/ListSystems.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Link } from 'react-router-dom'
|
||||
import SystemCard from "./SystemCard";
|
||||
import {
|
||||
Container,
|
||||
Divider,
|
||||
Header,
|
||||
Card,
|
||||
Form,
|
||||
Grid,
|
||||
Segment,
|
||||
Input,
|
||||
Icon,
|
||||
Button,
|
||||
Message
|
||||
} from "semantic-ui-react";
|
||||
|
||||
// ----------------------------------------------------
|
||||
const requestMessageStyle = {
|
||||
color: "red"
|
||||
};
|
||||
|
||||
function SystemList(props) {
|
||||
const systems = props.systems;
|
||||
const listItems = numbers.map((number) =>
|
||||
<li>{number}</li>
|
||||
);
|
||||
return (
|
||||
<ul>{listItems}</ul>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
class ListSystems extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
}
|
||||
|
||||
state = {
|
||||
requestMessage: "",
|
||||
|
||||
}
|
||||
componentDidMount() {
|
||||
this.props.fetchSystems();
|
||||
}
|
||||
|
||||
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
|
||||
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
|
||||
render() {
|
||||
const systems = this.props.system.items;
|
||||
return (
|
||||
<div>
|
||||
<Container>
|
||||
<Header as="h1">Radio Systems</Header>
|
||||
<Button onClick={(e) => this.props.changeUrl("/create-system")}><Icon name='plus' />Add System</Button>
|
||||
<Divider />
|
||||
<Card.Group itemsPerRow={4}>
|
||||
{systems.map((system) =>
|
||||
<SystemCard system={system} key={system.shortName} onClick={(e) => this.props.changeUrl("/system/" + system.shortName)}/>
|
||||
)}
|
||||
</Card.Group>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ListSystems;
|
||||
16
admin/src/client/System/ListSystemsContainer.js
Normal file
16
admin/src/client/System/ListSystemsContainer.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as systemActions from "./system-actions"
|
||||
import ListSystems from "./ListSystems"
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
system: state.system
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
systemActions
|
||||
)(ListSystems)
|
||||
72
admin/src/client/System/PreCreateSystem.js
Normal file
72
admin/src/client/System/PreCreateSystem.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Form,
|
||||
Grid,
|
||||
Segment,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Step,
|
||||
Icon
|
||||
} from "semantic-ui-react";
|
||||
|
||||
import SystemForm from "./SystemForm";
|
||||
|
||||
// ----------------------------------------------------
|
||||
const requestMessageStyle = {
|
||||
color: "red"
|
||||
};
|
||||
|
||||
// ----------------------------------------------------
|
||||
class PreCreateSystem extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
}
|
||||
|
||||
state = {
|
||||
requestMessage: ""
|
||||
}
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Step.Group attached='top' widths={3}>
|
||||
<Step>
|
||||
<Icon name='wifi' />
|
||||
<Step.Content>
|
||||
<Step.Title>Trunk Recorder</Step.Title>
|
||||
<Step.Description>Choose your shipping options</Step.Description>
|
||||
</Step.Content>
|
||||
</Step>
|
||||
|
||||
<Step active>
|
||||
<Icon name='payment' />
|
||||
<Step.Content>
|
||||
<Step.Title>Billing</Step.Title>
|
||||
<Step.Description>Enter billing information</Step.Description>
|
||||
</Step.Content>
|
||||
</Step>
|
||||
|
||||
<Step disabled>
|
||||
<Icon name='info' />
|
||||
<Step.Content>
|
||||
<Step.Title>Confirm Order</Step.Title>
|
||||
</Step.Content>
|
||||
</Step>
|
||||
</Step.Group>
|
||||
|
||||
<Segment attached>
|
||||
<Image src='https://react.semantic-ui.com/images/wireframe/paragraph.png' />
|
||||
</Segment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PreCreateSystem;
|
||||
27
admin/src/client/System/ResponsiveCallChart.js
Normal file
27
admin/src/client/System/ResponsiveCallChart.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React, { Component } from "react"
|
||||
import CallChart from "./CallChart"
|
||||
import { ParentSize } from "@vx/responsive";
|
||||
|
||||
class ResponsiveCallChart extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return(
|
||||
<ParentSize className="graph-container">
|
||||
{({ width: w, height: h }) => {
|
||||
return (
|
||||
<CallChart
|
||||
width={w}
|
||||
height={h}
|
||||
data={this.props.data}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</ParentSize>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ResponsiveCallChart
|
||||
27
admin/src/client/System/ResponsiveErrorChart.js
Normal file
27
admin/src/client/System/ResponsiveErrorChart.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React, { Component } from "react"
|
||||
import ErrorChart from "./ErrorChart"
|
||||
import { ParentSize } from "@vx/responsive";
|
||||
|
||||
class ResponsiveErrorChart extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return(
|
||||
<ParentSize className="graph-container">
|
||||
{({ width: w, height: h }) => {
|
||||
return (
|
||||
<ErrorChart
|
||||
width={w}
|
||||
height={h}
|
||||
data={this.props.data}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</ParentSize>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ResponsiveErrorChart
|
||||
684
admin/src/client/System/System.js
Normal file
684
admin/src/client/System/System.js
Normal file
@@ -0,0 +1,684 @@
|
||||
import React, {Component} from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import ListTalkgroups from "../Talkgroups/ListTalkgroups.js";
|
||||
import MessageBox from "../components/pure/Message.js";
|
||||
import ListGroups from "../Group/ListGroups.js";
|
||||
import GroupModal from "../Group/GroupModalContainer.js";
|
||||
import UpdatePermissionModal from "../Permission/UpdatePermissionModalContainer.js";
|
||||
import AddPermissionModal from "../Permission/AddPermissionModalContainer.js";
|
||||
import ErrorChart from "./ResponsiveErrorChart"
|
||||
import CallChart from "./ResponsiveCallChart"
|
||||
import {
|
||||
Button,
|
||||
Confirm,
|
||||
Icon,
|
||||
Form,
|
||||
TextArea,
|
||||
Header,
|
||||
Container,
|
||||
Message,
|
||||
List,
|
||||
Segment,
|
||||
Tab,
|
||||
Divider,
|
||||
Label
|
||||
} from "semantic-ui-react";
|
||||
|
||||
function planName(planType) {
|
||||
switch (planType) {
|
||||
case freePlanValue:
|
||||
return "Free";
|
||||
case proPlanValue:
|
||||
return "Pro";
|
||||
}
|
||||
}
|
||||
|
||||
var backend_server =
|
||||
process.env["BACKEND_SERVER"] != null
|
||||
? process.env["BACKEND_SERVER"]
|
||||
: "https://api.openmhz.com";
|
||||
|
||||
class System extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleUpload = this.handleUpload.bind(this);
|
||||
this.reorderGroup = this.reorderGroup.bind(this);
|
||||
this.deleteGroup = this.deleteGroup.bind(this);
|
||||
this.editGroup = this.editGroup.bind(this);
|
||||
this.saveGroupOrder = this.saveGroupOrder.bind(this);
|
||||
this.showSystemDeleteConfirm = this.showSystemDeleteConfirm.bind(this);
|
||||
this.showPermissionDeleteConfirm = this.showPermissionDeleteConfirm.bind(
|
||||
this
|
||||
);
|
||||
this.updatePermission = this.updatePermission.bind(this);
|
||||
this.handlePermissionDeleteConfirm = this.handlePermissionDeleteConfirm.bind(
|
||||
this
|
||||
);
|
||||
this.handlePermissionDeleteCancel = this.handlePermissionDeleteCancel.bind(
|
||||
this
|
||||
);
|
||||
this.handleSystemDeleteConfirm = this.handleSystemDeleteConfirm.bind(this);
|
||||
this.handleSystemDeleteCancel = this.handleSystemDeleteCancel.bind(this);
|
||||
this.handleUpgradePlan = this.handleUpgradePlan.bind(this);
|
||||
this.handleUpdatePermissionClose = this.handleUpdatePermissionClose.bind(
|
||||
this
|
||||
);
|
||||
this.handleGroupToggle = this.handleGroupToggle.bind(this);
|
||||
this.handleGroupClose = this.handleGroupClose.bind(this);
|
||||
this.handleAddPermissionToggle = this.handleAddPermissionToggle.bind(this);
|
||||
this.handleAddPermissionClose = this.handleAddPermissionClose.bind(this);
|
||||
this.processErrors = this.processErrors.bind(this);
|
||||
this.processStatistics = this.processStatistics.bind(this);
|
||||
this.state = {
|
||||
openSystemDeleteConfirm: false,
|
||||
openPermissionDeleteConfirm: false,
|
||||
openMessage: false,
|
||||
requestMessage: "",
|
||||
groupVisible: false,
|
||||
editGroupId: false,
|
||||
selectedPermission: false,
|
||||
addPermissionVisible: false,
|
||||
updatePermissionVisible: false,
|
||||
updatePermissionId: false
|
||||
};
|
||||
}
|
||||
|
||||
processErrors(errors) {
|
||||
var allData = [];
|
||||
|
||||
var num = 0;
|
||||
const now = new Date();
|
||||
var maxDate = now;
|
||||
var minDate = now;
|
||||
var minValue = 0;
|
||||
var maxValue = 0;
|
||||
var legend = [];
|
||||
for (var j = 0; j < errors.length; j++) {
|
||||
var data = [];
|
||||
for (var i = 0; i < errors[j].values.length; i++) {
|
||||
var time = new Date(errors[j].values[i].time);
|
||||
if (errors[j].values[i].errors > maxValue)
|
||||
maxValue = errors[j].values[i].errors;
|
||||
if (time < minDate) minDate = time;
|
||||
var value = {
|
||||
id: num++,
|
||||
y: errors[j].values[i].errors,
|
||||
x: time
|
||||
};
|
||||
data.push(value);
|
||||
}
|
||||
const id = Math.floor(errors[j]._id / 1000) / 1000 + "MHz";
|
||||
legend.push(id);
|
||||
var obj = {
|
||||
id: id,
|
||||
data: data
|
||||
};
|
||||
allData.push(obj);
|
||||
}
|
||||
const errorData = {
|
||||
data: allData,
|
||||
minDate: minDate,
|
||||
maxDate: maxDate,
|
||||
minValue: minValue,
|
||||
maxValue: maxValue,
|
||||
legend: legend
|
||||
};
|
||||
|
||||
this.setState({ errorData: errorData });
|
||||
}
|
||||
processStatistics(statistic) {
|
||||
var callTotals = [];
|
||||
var errorTotals = [];
|
||||
const now = new Date();
|
||||
var maxDate = now;
|
||||
var minDate = now;
|
||||
var minValue = 0;
|
||||
var maxValue = 0;
|
||||
var MS_PER_MINUTE = 60000;
|
||||
for (var j = 0; j < statistic.callTotals.length; j++) {
|
||||
var spotsBack = statistic.callTotals.length - j;
|
||||
var time = new Date(now - spotsBack * 15 * MS_PER_MINUTE);
|
||||
if (time < minDate) minDate = time;
|
||||
if (statistic.callTotals[j] > maxValue)
|
||||
maxValue = statistic.callTotals[j];
|
||||
callTotals.push({ y: statistic.callTotals[j], x: time });
|
||||
}
|
||||
for (var j = 0; j < statistic.errorTotals.length; j++) {
|
||||
var spotsBack = statistic.errorTotals.length - j;
|
||||
var time = new Date(now - spotsBack * 15 * MS_PER_MINUTE);
|
||||
if (time < minDate) minDate = time;
|
||||
if (statistic.errorTotals[j] > maxValue)
|
||||
maxValue = statistic.errorTotals[j];
|
||||
errorTotals.push({ x: time, y: statistic.errorTotals[j] });
|
||||
}
|
||||
var callData = {
|
||||
minDate: minDate,
|
||||
maxDate: maxDate,
|
||||
minValue: minValue,
|
||||
maxValue: maxValue,
|
||||
callTotals: callTotals,
|
||||
errorTotals: errorTotals
|
||||
};
|
||||
|
||||
this.setState({ callData: callData });
|
||||
}
|
||||
componentDidMount() {
|
||||
this.props.systemActions.fetchSystems();
|
||||
this.props.talkgroupActions.fetchTalkgroups(this.props.shortName);
|
||||
this.props.groupActions.fetchGroups(this.props.shortName);
|
||||
this.props.errorActions.fetch(this.props.shortName);
|
||||
this.props.permissionActions.fetch(this.props.shortName);
|
||||
if (this.props.statistic) {
|
||||
this.processStatistics(this.props.statistic);
|
||||
}
|
||||
if (this.props.errors) {
|
||||
this.processErrors(this.props.errors);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProp) {
|
||||
if (!this.props.errors && nextProp.errors) {
|
||||
this.processErrors(nextProp.errors);
|
||||
}
|
||||
if (!this.props.statistic && nextProp.statistic) {
|
||||
this.processStatistics(nextProp.statistic);
|
||||
}
|
||||
}
|
||||
|
||||
updatePermission(permissionId) {
|
||||
this.setState({
|
||||
updatePermissionId: permissionId,
|
||||
updatePermissionVisible: !this.state.updatePermissionVisible
|
||||
});
|
||||
}
|
||||
|
||||
editGroup(groupId) {
|
||||
this.setState({
|
||||
editGroupId: groupId,
|
||||
groupVisible: !this.state.groupVisible
|
||||
});
|
||||
}
|
||||
|
||||
deleteGroup(groupId) {
|
||||
this.props.groupActions.deleteGroup(this.props.shortName, groupId);
|
||||
}
|
||||
|
||||
reorderGroup(oldIndex, newIndex) {
|
||||
this.props.groupActions.reorderGroup(
|
||||
this.props.shortName,
|
||||
oldIndex,
|
||||
newIndex
|
||||
);
|
||||
}
|
||||
|
||||
handleUpdatePermissionClose(didUpdate) {
|
||||
this.setState({
|
||||
updatePermissionVisible: !this.state.updatePermissionVisible,
|
||||
updatePermissionId: false
|
||||
});
|
||||
}
|
||||
|
||||
handleAddPermissionClose(didUpdate) {
|
||||
this.setState({
|
||||
addPermissionVisible: !this.state.addPermissionVisible
|
||||
});
|
||||
}
|
||||
|
||||
handleAddPermissionToggle = () =>
|
||||
this.setState({
|
||||
addPermissionVisible: !this.state.addPermissionVisible
|
||||
});
|
||||
|
||||
handleGroupClose(didUpdate) {
|
||||
this.setState({
|
||||
groupVisible: !this.state.groupVisible,
|
||||
editGroupId: false
|
||||
});
|
||||
}
|
||||
handleGroupToggle = () => this.setState({
|
||||
editGroupId: false,
|
||||
groupVisible: !this.state.groupVisible
|
||||
})
|
||||
handleExport() {
|
||||
this.props.talkgroupActions
|
||||
.exportTalkgroups(this.props.system.shortName)
|
||||
.then(requestMessage => {
|
||||
if (requestMessage) {
|
||||
// report to the user is there was a problem during registration
|
||||
this.setState({ requestMessage });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
saveGroupOrder() {
|
||||
const order = this.props.groups.map(x => x._id);
|
||||
const data = {
|
||||
groupOrder: JSON.stringify(order)
|
||||
};
|
||||
this.props.groupActions
|
||||
.saveGroupOrder(this.props.system.shortName, data)
|
||||
.then(requestMessage => {
|
||||
if (requestMessage) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleUpgradePlan() {
|
||||
window.location = account_server + "/update-plans";
|
||||
}
|
||||
|
||||
handleUpload(file) {
|
||||
this.props.talkgroupActions
|
||||
.importTalkgroups(this.props.system.shortName, file)
|
||||
.then(requestMessage => {
|
||||
if (requestMessage) {
|
||||
// report to the user is there was a problem during registration
|
||||
this.setState({ requestMessage });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// https://github.com/Semantic-Org/Semantic-UI-React/issues/2103
|
||||
|
||||
showPermissionDeleteConfirm = permission =>
|
||||
this.setState({
|
||||
openPermissionDeleteConfirm: true,
|
||||
selectedPermission: permission
|
||||
});
|
||||
showSystemDeleteConfirm = () =>
|
||||
this.setState({ openSystemDeleteConfirm: true });
|
||||
|
||||
handlePermissionDeleteConfirm = permissionId => {
|
||||
this.setState({ openPermissionDeleteConfirm: false });
|
||||
this.props.permissionActions
|
||||
.deletePermission({
|
||||
shortName: this.props.system.shortName,
|
||||
permissionId: this.state.selectedPermission._id
|
||||
})
|
||||
.then(requestMessage => {
|
||||
if (requestMessage) {
|
||||
// report to the user is there was a problem during login
|
||||
this.setState({ requestMessage: requestMessage, openMessage: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleSystemDeleteConfirm = () => {
|
||||
this.setState({ openSystemDeleteConfirm: false });
|
||||
this.props.systemActions
|
||||
.deleteSystem(this.props.system.shortName)
|
||||
.then(requestMessage => {
|
||||
if (requestMessage) {
|
||||
// report to the user is there was a problem during login
|
||||
this.setState({ requestMessage: requestMessage, openMessage: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
handleSystemDeleteCancel = () =>
|
||||
this.setState({ openSystemDeleteConfirm: false });
|
||||
handlePermissionDeleteCancel = () =>
|
||||
this.setState({ openPermissionDeleteConfirm: false });
|
||||
render() {
|
||||
const system = this.props.system;
|
||||
let fileInput = null;
|
||||
var location = "";
|
||||
var example = "";
|
||||
var plan = "";
|
||||
var groupRender = () => {};
|
||||
|
||||
if (system) {
|
||||
if (this.props.talkgroups && this.props.talkgroups.length) {
|
||||
groupRender = () => (
|
||||
<div>
|
||||
<Button onClick={this.handleGroupToggle}>Create Group</Button>
|
||||
<Button onClick={this.saveGroupOrder} disabled={!this.props.orderChange}> Save Order</Button>
|
||||
<ListGroups
|
||||
groups={this.props.groups}
|
||||
reorderGroup={this.reorderGroup}
|
||||
deleteGroup={this.deleteGroup}
|
||||
editGroup={this.editGroup}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
groupRender = () => (
|
||||
<Container text>
|
||||
<Message
|
||||
icon="upload"
|
||||
header="Import Talkgroups"
|
||||
content="Before you can create groups of Talkgroups, you need to import some talkgroups."
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
switch (system.planType) {
|
||||
case freePlanValue:
|
||||
const tempPlan = planName(system.planType) + " plan";
|
||||
plan = (
|
||||
<div>
|
||||
{tempPlan}
|
||||
<Label as="a" onClick={this.handleUpgradePlan}>
|
||||
{" "}
|
||||
<Icon name="plus" />
|
||||
Upgrade Plan
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
|
||||
case proPlanValue:
|
||||
plan = planName(system.planType) + " plan";
|
||||
break;
|
||||
}
|
||||
|
||||
example =
|
||||
'{\n\t"systems": [\n\t\t{\n\t\t\t"shortName": "' +
|
||||
system.shortName +
|
||||
'",\n\t\t\t"apiKey": "' +
|
||||
system.key +
|
||||
'"\n\t\t}\n\t],\n\t"uploadServer": "' +
|
||||
backend_server +
|
||||
'"\n}';
|
||||
|
||||
switch (system.systemType) {
|
||||
case "state":
|
||||
location = system.state;
|
||||
break;
|
||||
case "city":
|
||||
location = system.city + ", " + system.state;
|
||||
break;
|
||||
case "county":
|
||||
location = system.county + ", " + system.state;
|
||||
break;
|
||||
case "international":
|
||||
location = system.country;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const panes = [
|
||||
{
|
||||
menuItem: "Overview",
|
||||
render: () => (
|
||||
<div>
|
||||
<Container text>
|
||||
<Segment.Group>
|
||||
<Segment>
|
||||
<Header size="small">Location</Header>
|
||||
{location}
|
||||
</Segment>
|
||||
<Segment>
|
||||
<Header size="small">Short Name</Header>
|
||||
{this.props.system.shortName}
|
||||
</Segment>
|
||||
<Segment>
|
||||
<Header size="small">Description</Header>
|
||||
{this.props.system.description}
|
||||
</Segment>
|
||||
|
||||
<Segment clearing={true}>
|
||||
<Button
|
||||
color="red"
|
||||
onClick={this.showSystemDeleteConfirm}
|
||||
disabled={this.props.systemisWaiting}
|
||||
content="Delete"
|
||||
floated="right"
|
||||
/>
|
||||
<Button
|
||||
as={Link}
|
||||
to={"/update-system/" + this.props.system.shortName}
|
||||
content="Update"
|
||||
disabled={this.props.systemisWaiting}
|
||||
floated="left"
|
||||
/>
|
||||
</Segment>
|
||||
</Segment.Group>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
menuItem: "Config",
|
||||
render: () => (
|
||||
<div>
|
||||
<Container text>
|
||||
<Header as="h2">Configure Trunk Recorder</Header>
|
||||
<p>
|
||||
Below are the parameters you will need to configure Trunk
|
||||
Recorder. The example below shows how they should be added to
|
||||
the config.json file.
|
||||
</p>
|
||||
|
||||
<Segment.Group>
|
||||
<Segment>
|
||||
<Header size="small">API Key</Header>
|
||||
{this.props.system.key}
|
||||
</Segment>
|
||||
<Segment>
|
||||
<Header size="small">Short Name</Header>
|
||||
{this.props.system.shortName}
|
||||
</Segment>
|
||||
<Segment>
|
||||
<Header size="small">Upload Server</Header>
|
||||
{backend_server}
|
||||
</Segment>
|
||||
<Segment>
|
||||
<Header size="small">Example</Header>
|
||||
<Form>
|
||||
<TextArea rows="10" readOnly="readOnly" value={example} />
|
||||
</Form>
|
||||
</Segment>
|
||||
</Segment.Group>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
menuItem: "Talkgroups",
|
||||
render: () => (
|
||||
<div>
|
||||
<Container text>
|
||||
<p>
|
||||
<em>
|
||||
This is the same .csv talkgroup file from Trunk Recorder...
|
||||
except the Hex column has been deleted
|
||||
</em>
|
||||
</p>
|
||||
<p>The columns for the file are:</p>
|
||||
<List ordered celled>
|
||||
<List.Item>
|
||||
<List.Content>
|
||||
<List.Header>Number</List.Header>
|
||||
<List.Description>The talkgroup number</List.Description>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Content>
|
||||
<List.Header>Mode</List.Header>
|
||||
<List.Description>
|
||||
The mode for the talkgroup, it can be: 'A' = analog, 'D' =
|
||||
P25 Digital, 'E' = Encrypted
|
||||
</List.Description>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Content>
|
||||
<List.Header>Alpha</List.Header>
|
||||
<List.Description>
|
||||
Up to 12 charecter description of the talkgroup
|
||||
</List.Description>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Content>
|
||||
<List.Header>Description</List.Header>
|
||||
<List.Description>
|
||||
The full title for the talkgroup
|
||||
</List.Description>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Content>
|
||||
<List.Header>Tag</List.Header>
|
||||
<List.Description>
|
||||
The type of talkgroup, eg: Dispatch, Fireground
|
||||
</List.Description>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Content>
|
||||
<List.Header>Group</List.Header>
|
||||
<List.Description>
|
||||
The department that uses this talkgroup
|
||||
</List.Description>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Content>
|
||||
<List.Header>Priority</List.Header>
|
||||
<List.Description>
|
||||
The priority for recording this talkgroup, 100 or over
|
||||
will be skipped
|
||||
</List.Description>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
</List>
|
||||
</Container>
|
||||
<Divider />
|
||||
<span>
|
||||
<label htmlFor="upload-button" className="ui icon button">
|
||||
<i className="upload icon" />
|
||||
Import Talkgroups
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
id="upload-button"
|
||||
style={{
|
||||
display: "none"
|
||||
}}
|
||||
onChange={() => {
|
||||
this.handleUpload(fileInput.files[0]);
|
||||
}}
|
||||
ref={input => {
|
||||
fileInput = input;
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<a href={"/talkgroups/" + this.props.system.shortName + "/export"}>
|
||||
<Button>
|
||||
<i className="upload icon" />
|
||||
Export Talkgroups
|
||||
</Button>
|
||||
</a>
|
||||
<ListTalkgroups talkgroups={this.props.talkgroups} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
menuItem: "Groups",
|
||||
render: groupRender
|
||||
},
|
||||
/*{
|
||||
menuItem: "Users",
|
||||
render: () => (<Tab.Pane>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<Button onClick={this.handleAddPermissionToggle} >
|
||||
Add User
|
||||
</Button>
|
||||
<ListPermissions permissions={this.props.permissions} updatePermission={this.updatePermission} deletePermission={this.showPermissionDeleteConfirm}/>
|
||||
</Tab.Pane>)
|
||||
},*/ {
|
||||
menuItem: "Stats",
|
||||
render: () => (
|
||||
<div>
|
||||
<Divider horizontal>
|
||||
<Icon name="microphone" />
|
||||
24-Hour Call History
|
||||
</Divider>
|
||||
<CallChart data={this.state.callData} />
|
||||
<Divider horizontal>
|
||||
<Icon name="microphone" />
|
||||
Hourly Vocoder Error Percentage
|
||||
</Divider>
|
||||
<ErrorChart data={this.state.errorData} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AddPermissionModal
|
||||
shortName={this.props.shortName}
|
||||
open={this.state.addPermissionVisible}
|
||||
onClose={this.handleAddPermissionClose}
|
||||
/>
|
||||
|
||||
<GroupModal
|
||||
shortName={this.props.shortName}
|
||||
editGroupId={this.state.editGroupId}
|
||||
open={this.state.groupVisible}
|
||||
onClose={this.handleGroupClose}
|
||||
/>
|
||||
<UpdatePermissionModal
|
||||
shortName={this.props.shortName}
|
||||
updatePermissionId={this.state.updatePermissionId}
|
||||
open={this.state.updatePermissionVisible}
|
||||
onClose={this.handleUpdatePermissionClose}
|
||||
/>
|
||||
{system ? (
|
||||
<Container>
|
||||
<MessageBox
|
||||
open={this.state.openMessage}
|
||||
message={this.state.requestMessage}
|
||||
title="Delete Failed"
|
||||
onClose={() => this.setState({ openMessage: false })}
|
||||
/>
|
||||
<Confirm
|
||||
open={this.state.openSystemDeleteConfirm}
|
||||
header="Are you sure you want to?"
|
||||
content={"Delete system - " + this.props.system.shortName}
|
||||
onCancel={this.handleSystemDeleteCancel}
|
||||
onConfirm={this.handleSystemDeleteConfirm}
|
||||
/>
|
||||
<Confirm
|
||||
open={this.state.openPermissionDeleteConfirm}
|
||||
header="Are you sure you want to?"
|
||||
content={
|
||||
"Remove access for: " + this.state.selectedPermission.email
|
||||
}
|
||||
onCancel={this.handlePermissionDeleteCancel}
|
||||
onConfirm={this.handlePermissionDeleteConfirm}
|
||||
/>
|
||||
|
||||
<Header as="h1">
|
||||
<Header.Content>
|
||||
{this.props.system.name}
|
||||
{/* <Header.Subheader>{plan}</Header.Subheader> */}
|
||||
</Header.Content>
|
||||
</Header>
|
||||
|
||||
<Tab
|
||||
menu={{
|
||||
secondary: true,
|
||||
pointing: true
|
||||
}}
|
||||
panes={panes}
|
||||
defaultActiveIndex={this.props.activeTab}
|
||||
/>
|
||||
</Container>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default System;
|
||||
74
admin/src/client/System/SystemCard.js
Normal file
74
admin/src/client/System/SystemCard.js
Normal file
@@ -0,0 +1,74 @@
|
||||
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
Card,
|
||||
Icon,
|
||||
Label,
|
||||
Header
|
||||
} from "semantic-ui-react";
|
||||
|
||||
|
||||
function planName(planType) {
|
||||
switch(planType) {
|
||||
case freePlanValue:
|
||||
return "Free";
|
||||
case proPlanValue:
|
||||
return "Pro";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SystemCard extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
var location = "";
|
||||
const system = this.props.system;
|
||||
var plan=null;
|
||||
|
||||
if (system) {
|
||||
|
||||
if (system.planType) {
|
||||
plan = planName(system.planType);
|
||||
}
|
||||
switch (system.systemType) {
|
||||
case "state":
|
||||
location = system.state;
|
||||
break;
|
||||
case "city":
|
||||
location = system.city + ", " + system.state;
|
||||
break;
|
||||
case "county":
|
||||
location = system.county + ", " + system.state;
|
||||
break;
|
||||
case "international":
|
||||
location = system.country;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Card link onClick={this.props.onClick}>
|
||||
|
||||
<Card.Content>
|
||||
<Header size='medium'>
|
||||
<Header.Content>
|
||||
{system.name}
|
||||
<Header.Subheader>{system.screenName}</Header.Subheader>
|
||||
</Header.Content>
|
||||
</Header>{plan&&<Label color='orange' ribbon='right'>{plan}</Label>}
|
||||
</Card.Content><Card.Content>
|
||||
|
||||
<Card.Description>{system.description}</Card.Description>
|
||||
</Card.Content>
|
||||
<Card.Content extra>
|
||||
<Icon name='marker' />
|
||||
{location}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SystemCard;
|
||||
40
admin/src/client/System/SystemContainer.js
Normal file
40
admin/src/client/System/SystemContainer.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as systemActionCreators from "./system-actions"
|
||||
import * as talkgroupActionCreators from "../Talkgroups/talkgroup-actions"
|
||||
import * as groupActionCreators from "../Group/group-actions"
|
||||
import * as permissionActionCreators from "../Permission/permission-actions"
|
||||
import * as errorActionCreators from "./error-actions"
|
||||
import { bindActionCreators } from 'redux'
|
||||
import System from "./System"
|
||||
|
||||
function mapStateToProps(state,props) {
|
||||
return {
|
||||
system: state.system.items.find((item) => item.shortName === props.match.params.shortName),
|
||||
permissions: state.permission.items,
|
||||
statistic: state.statistic[props.match.params.shortName],
|
||||
systemIsWaiting: state.system.isWaiting,
|
||||
talkgroupsIsWaiting: state.talkgroup.isWaiting,
|
||||
groupsIsWaiting: state.group.isWaiting,
|
||||
talkgroups: state.talkgroup.items[props.match.params.shortName],
|
||||
orderChange: state.group.orderChange,
|
||||
groups: state.group.items[props.match.params.shortName],
|
||||
errors: state.error.items[props.match.params.shortName],
|
||||
shortName: props.match.params.shortName,
|
||||
activeTab: (props.match.params.tab ? props.match.params.tab : 0)
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
groupActions: bindActionCreators(groupActionCreators, dispatch),
|
||||
systemActions: bindActionCreators(systemActionCreators, dispatch),
|
||||
talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch),
|
||||
errorActions: bindActionCreators(errorActionCreators, dispatch),
|
||||
permissionActions: bindActionCreators(permissionActionCreators, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(System)
|
||||
428
admin/src/client/System/SystemForm.js
Normal file
428
admin/src/client/System/SystemForm.js
Normal file
@@ -0,0 +1,428 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Dropdown,
|
||||
Divider,
|
||||
TextArea,
|
||||
Form,
|
||||
Grid,
|
||||
Segment,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Checkbox,
|
||||
Icon
|
||||
} from "semantic-ui-react";
|
||||
|
||||
// ----------------------------------------------------
|
||||
const checkInputMessagestyle = {
|
||||
color: "red"
|
||||
};
|
||||
|
||||
// ----------------------------------------------------
|
||||
class SystemForm extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
this.handleCheckboxChange = this.handleCheckboxChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.checkInputs = this.checkInputs.bind(this);
|
||||
this.render = this.render.bind(this);
|
||||
|
||||
|
||||
this.state = {
|
||||
checkInputMessages: [],
|
||||
name: "",
|
||||
shortName: "",
|
||||
description: "",
|
||||
systemType: "state",
|
||||
state: "",
|
||||
city: "",
|
||||
county: "",
|
||||
country: "",
|
||||
showScreenName: false,
|
||||
nameError: false,
|
||||
shortNameError: false,
|
||||
descriptionError: false,
|
||||
emailError: false,
|
||||
systemTypeError: false,
|
||||
stateError: false,
|
||||
cityErorr: false,
|
||||
countyError: false,
|
||||
countryError: false,
|
||||
isEditing: this.props.isEditing
|
||||
};
|
||||
if (this.props.isEditing && this.props.system)
|
||||
{
|
||||
this.state.name = this.props.system.name;
|
||||
this.state.shortName = this.props.system.shortName;
|
||||
this.state.description = this.props.system.description;
|
||||
this.state.systemType = this.props.system.systemType;
|
||||
this.state.state = this.props.system.state;
|
||||
this.state.county = this.props.system.county;
|
||||
this.state.country = this.props.system.country;
|
||||
this.state.showScreenName = this.props.system.showScreenName;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.isEditing && nextProps.system) {
|
||||
this.setState({
|
||||
name: nextProps.system.name,
|
||||
shortName: nextProps.system.shortName,
|
||||
description: nextProps.system.description,
|
||||
systemType: nextProps.system.systemType,
|
||||
state: nextProps.system.state,
|
||||
county: nextProps.system.county,
|
||||
country: nextProps.system.country,
|
||||
showScreenName: nextProps.system.showScreenName,
|
||||
});
|
||||
}
|
||||
}
|
||||
checkInputs() {
|
||||
let error = false;
|
||||
var checkInputMessages = [];
|
||||
|
||||
if (this.state.name === "") {
|
||||
this.setState({ nameError: true });
|
||||
checkInputMessages.push("System Name is required");
|
||||
error = true;
|
||||
} else {
|
||||
this.setState({ nameError: false });
|
||||
}
|
||||
|
||||
if (this.state.shortName.length == 0 || this.state.shortName.length > 10) {
|
||||
this.setState({ shortNameError: true });
|
||||
checkInputMessages.push(
|
||||
"Short Name for the system has to be 4-10 charecters"
|
||||
);
|
||||
error = true;
|
||||
} else {
|
||||
this.setState({ shortNameError: false });
|
||||
}
|
||||
|
||||
if (this.state.description === "") {
|
||||
this.setState({ descriptionError: true });
|
||||
checkInputMessages.push("System description is required");
|
||||
error = true;
|
||||
} else {
|
||||
this.setState({ descriptionError: false });
|
||||
}
|
||||
|
||||
if (
|
||||
this.state.systemType === "state" ||
|
||||
this.state.systemType === "city" ||
|
||||
this.state.systemType === "county"
|
||||
) {
|
||||
if (this.state.state === "") {
|
||||
this.setState({ stateError: true });
|
||||
checkInputMessages.push("Select a state");
|
||||
error = true;
|
||||
} else {
|
||||
this.setState({ stateError: false });
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.systemType === "city") {
|
||||
if (this.state.city === "") {
|
||||
this.setState({ cityError: true });
|
||||
checkInputMessages.push("Enter a city");
|
||||
error = true;
|
||||
} else {
|
||||
this.setState({ cityError: false });
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.systemType === "county") {
|
||||
if (this.state.county === "") {
|
||||
this.setState({ countyError: true });
|
||||
checkInputMessages.push("Enter a city");
|
||||
error = true;
|
||||
} else {
|
||||
this.setState({ countyError: false });
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.systemType === "international") {
|
||||
if (this.state.country === "") {
|
||||
this.setState({ countryError: true });
|
||||
checkInputMessages.push("Enter a city");
|
||||
error = true;
|
||||
} else {
|
||||
this.setState({ countryError: false });
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ checkInputMessages: checkInputMessages });
|
||||
return error;
|
||||
}
|
||||
|
||||
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
|
||||
handleCheckboxChange() {
|
||||
this.setState({showScreenName: !this.state.showScreenName});
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
let inputError = this.checkInputs();
|
||||
|
||||
if (!inputError) {
|
||||
const system = (({
|
||||
name,
|
||||
shortName,
|
||||
description,
|
||||
systemType,
|
||||
city,
|
||||
state,
|
||||
county,
|
||||
country,
|
||||
showScreenName
|
||||
}) => ({
|
||||
name,
|
||||
shortName,
|
||||
description,
|
||||
systemType,
|
||||
city,
|
||||
state,
|
||||
county,
|
||||
country,
|
||||
showScreenName
|
||||
}))(this.state);
|
||||
this.props.onSubmit(system);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
var requestMessage = "";
|
||||
var floatStyle = {
|
||||
overflow: "auto"
|
||||
};
|
||||
|
||||
const { isEditing } = this.props;
|
||||
const stateOptions = [
|
||||
{ value: "AL", text: "Alabama" },
|
||||
{ value: "AK", text: "Alaska" },
|
||||
{ value: "AZ", text: "Arizona" },
|
||||
{ value: "AR", text: "Arkansas" },
|
||||
{ value: "CA", text: "California" },
|
||||
{ value: "CO", text: "Colorado" },
|
||||
{ value: "CT", text: "Connecticut" },
|
||||
{ value: "DE", text: "Delaware" },
|
||||
{ value: "DC", text: "District Of Columbia" },
|
||||
{ value: "FL", text: "Florida" },
|
||||
{ value: "GA", text: "Georgia" },
|
||||
{ value: "HI", text: "Hawaii" },
|
||||
{ value: "ID", text: "Idaho" },
|
||||
{ value: "IL", text: "Illinois" },
|
||||
{ value: "IN", text: "Indiana" },
|
||||
{ value: "IA", text: "Iowa" },
|
||||
{ value: "KS", text: "Kansas" },
|
||||
{ value: "KY", text: "Kentucky" },
|
||||
{ value: "LA", text: "Louisiana" },
|
||||
{ value: "ME", text: "Maine" },
|
||||
{ value: "MD", text: "Maryland" },
|
||||
{ value: "MA", text: "Massachusetts" },
|
||||
{ value: "MI", text: "Michigan" },
|
||||
{ value: "MN", text: "Minnesota" },
|
||||
{ value: "MS", text: "Mississippi" },
|
||||
{ value: "MO", text: "Missouri" },
|
||||
{ value: "MT", text: "Montana" },
|
||||
{ value: "NE", text: "Nebraska" },
|
||||
{ value: "NV", text: "Nevada" },
|
||||
{ value: "NH", text: "New Hampshire" },
|
||||
{ value: "NJ", text: "New Jersey" },
|
||||
{ value: "NM", text: "New Mexico" },
|
||||
{ value: "NY", text: "New York" },
|
||||
{ value: "NC", text: "North Carolina" },
|
||||
{ value: "ND", text: "North Dakota" },
|
||||
{ value: "OH", text: "Ohio" },
|
||||
{ value: "OK", text: "Oklahoma" },
|
||||
{ value: "OR", text: "Oregon" },
|
||||
{ value: "PA", text: "Pennsylvania" },
|
||||
{ value: "RI", text: "Rhode Island" },
|
||||
{ value: "SC", text: "South Carolina" },
|
||||
{ value: "SD", text: "South Dakota" },
|
||||
{ value: "TN", text: "Tennessee" },
|
||||
{ value: "TX", text: "Texas" },
|
||||
{ value: "UT", text: "Utah" },
|
||||
{ value: "VT", text: "Vermont" },
|
||||
{ value: "VA", text: "Virginia" },
|
||||
{ value: "WA", text: "Washington" },
|
||||
{ value: "WV", text: "West Virginia" },
|
||||
{ value: "WI", text: "Wisconsin" },
|
||||
{ value: "WY", text: "Wyoming" }
|
||||
];
|
||||
if (
|
||||
this.state.checkInputMessages.length ||
|
||||
this.props.requestMessage.length
|
||||
) {
|
||||
var counter = 0;
|
||||
requestMessage = (
|
||||
<Message compact warning icon>
|
||||
<Icon name="lightning" />
|
||||
<Message.Content>
|
||||
<Message.Header>Problems...</Message.Header>
|
||||
<ul>
|
||||
{this.state.checkInputMessages.map(function(listValue) {
|
||||
return <li key={`ListItem_${counter++}`}>{listValue}</li>;
|
||||
})}
|
||||
{this.props.requestMessage.length > 0 && (
|
||||
<li key="req-mess">{this.props.requestMessage}</li>
|
||||
)}
|
||||
</ul>
|
||||
</Message.Content>
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Form className="raised padding segment" onSubmit={this.handleSubmit}>
|
||||
<Form.Group widths="equal">
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="name"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.nameError}
|
||||
value={this.state.name}
|
||||
label="System name"
|
||||
placeholder="System Name..."
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="shortName"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.shortNameError}
|
||||
value={this.state.shortName}
|
||||
disabled={isEditing}
|
||||
label="System Short Name (4-10 characters)"
|
||||
placeholder="Short Name..."
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form.Group>
|
||||
<Divider horizontal />
|
||||
<Form.Field>
|
||||
|
||||
<p>Your Screen Name: {this.props.screenName}</p>
|
||||
<Checkbox
|
||||
onChange={this.handleCheckboxChange}
|
||||
checked={this.state.showScreenName}
|
||||
label="Show in System Listing" />
|
||||
|
||||
</Form.Field>
|
||||
<Divider horizontal />
|
||||
<Form.TextArea
|
||||
label="Description"
|
||||
name="description"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.descriptionError}
|
||||
value={this.state.description}
|
||||
rows={2}
|
||||
placeholder="Description of system..."
|
||||
/>
|
||||
|
||||
<Divider horizontal><Icon name="marker" />Location</Divider>
|
||||
<Form.Group inline>
|
||||
<label>System Type</label>
|
||||
<Form.Radio
|
||||
label="State"
|
||||
name="systemType"
|
||||
value="state"
|
||||
checked={this.state.systemType === "state"}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
<Form.Radio
|
||||
label="County"
|
||||
name="systemType"
|
||||
value="county"
|
||||
checked={this.state.systemType === "county"}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
<Form.Radio
|
||||
label="City"
|
||||
name="systemType"
|
||||
value="city"
|
||||
checked={this.state.systemType === "city"}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
<Form.Radio
|
||||
label="International"
|
||||
name="systemType"
|
||||
value="international"
|
||||
checked={this.state.systemType === "international"}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths="equal">
|
||||
|
||||
{this.state.systemType === "city" && (
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="city"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.cityError}
|
||||
value={this.state.city}
|
||||
label="City"
|
||||
placeholder="City..."
|
||||
/>
|
||||
</Form.Field>
|
||||
)}
|
||||
{this.state.systemType === "county" && (
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="county"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.countyError}
|
||||
value={this.state.county}
|
||||
label="County"
|
||||
placeholder="County..."
|
||||
/>
|
||||
</Form.Field>
|
||||
)}
|
||||
{this.state.systemType === "international" ? (
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="country"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.countryError}
|
||||
value={this.state.country}
|
||||
label="Country"
|
||||
placeholder="Country..."
|
||||
/>
|
||||
</Form.Field>
|
||||
) : (
|
||||
<Form.Field>
|
||||
<Form.Dropdown
|
||||
name="state"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.stateError}
|
||||
value={this.state.state}
|
||||
options={stateOptions}
|
||||
label="State"
|
||||
placeholder="Select State..."
|
||||
/>
|
||||
</Form.Field>
|
||||
)}
|
||||
|
||||
</Form.Group>
|
||||
|
||||
<div style={floatStyle}>
|
||||
<Button type="submit" size="large" floated="right" value="Login">
|
||||
{isEditing != true ? "Create" : "Update"}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
{requestMessage}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SystemForm;
|
||||
63
admin/src/client/System/error-actions.js
Normal file
63
admin/src/client/System/error-actions.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import axios from "axios";
|
||||
//import { browserHistory } from "react-router"
|
||||
import * as types from "./error-constants";
|
||||
import { push } from 'connected-react-router';
|
||||
|
||||
|
||||
function beginFetch() {
|
||||
return { type: types.FETCH };
|
||||
}
|
||||
|
||||
function fetchSuccess(data) {
|
||||
return { type: types.FETCH_SUCCESS,
|
||||
data };
|
||||
}
|
||||
|
||||
function fetchError(data) {
|
||||
return { type: types.FETCH_ERROR,
|
||||
data };
|
||||
}
|
||||
|
||||
function makeUserRequest(method, data, api) {
|
||||
// returns a Promise
|
||||
return axios({
|
||||
method: method,
|
||||
url: api,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
export function changeUrl(url) {
|
||||
return dispatch => {
|
||||
dispatch(push(url));
|
||||
};
|
||||
}
|
||||
|
||||
export function fetch(shortName) {
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch(beginFetch());
|
||||
|
||||
return axios
|
||||
.get(backend_server + "/" + shortName + "/errors")
|
||||
.then(response => {
|
||||
if (response.data) {
|
||||
var data ={
|
||||
shortName: shortName,
|
||||
errors: response.data
|
||||
}
|
||||
dispatch(fetchSuccess(data));
|
||||
} else {
|
||||
dispatch(fetchError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
3
admin/src/client/System/error-constants.js
Normal file
3
admin/src/client/System/error-constants.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const FETCH = "FETCH"
|
||||
export const FETCH_SUCCESS = "FETCH_SUCCESS"
|
||||
export const FETCH_ERROR = "FETCH_ERROR"
|
||||
42
admin/src/client/System/error-reducer.js
Normal file
42
admin/src/client/System/error-reducer.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
FETCH,
|
||||
FETCH_SUCCESS,
|
||||
FETCH_ERROR
|
||||
} from "./error-constants";
|
||||
|
||||
|
||||
|
||||
const error = (
|
||||
state = {
|
||||
isWaiting: false,
|
||||
items: {}
|
||||
},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
|
||||
case FETCH:
|
||||
|
||||
return Object.assign({}, state, { isWaiting: true});
|
||||
|
||||
|
||||
case FETCH_SUCCESS:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
items: {
|
||||
...state.items,
|
||||
[action.data.shortName]: action.data.errors
|
||||
}
|
||||
});
|
||||
|
||||
case FETCH_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default error;
|
||||
20
admin/src/client/System/statistic-reducer.js
Normal file
20
admin/src/client/System/statistic-reducer.js
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
|
||||
const FETCH_SUCCESS_STATISTIC = "FETCH_SUCCESS_STATISTIC"
|
||||
|
||||
const statistic = (
|
||||
state = {},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
|
||||
|
||||
case FETCH_SUCCESS_STATISTIC:
|
||||
return Object.assign({}, state, {...action.data});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default statistic;
|
||||
233
admin/src/client/System/system-actions.js
Normal file
233
admin/src/client/System/system-actions.js
Normal file
@@ -0,0 +1,233 @@
|
||||
import axios from "axios";
|
||||
//import { browserHistory } from "react-router"
|
||||
import * as types from "./system-constants";
|
||||
import { push } from 'connected-react-router';
|
||||
|
||||
|
||||
// "Log In" action creators
|
||||
function beginCreateSystem() {
|
||||
return { type: types.CREATE_SYSTEM };
|
||||
}
|
||||
|
||||
function createSystemSuccess(data) {
|
||||
return {
|
||||
type: types.CREATE_SUCCESS_SYSTEM,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
function createSystemError(data) {
|
||||
return {
|
||||
type: types.CREATE_ERROR_SYSTEM,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
function beginUpdateSystem() {
|
||||
return { type: types.UPDATE_SYSTEM };
|
||||
}
|
||||
|
||||
function updateSystemSuccess(data) {
|
||||
return { type: types.UPDATE_SUCCESS_SYSTEM, data };
|
||||
}
|
||||
|
||||
function updateSystemError(data) {
|
||||
return { type: types.UPDATE_ERROR_SYSTEM, data };
|
||||
}
|
||||
|
||||
function beginDeleteSystem() {
|
||||
return { type: types.DELETE_SYSTEM };
|
||||
}
|
||||
|
||||
function deleteSystemSuccess(data) {
|
||||
return { type: types.DELETE_SUCCESS_SYSTEM,
|
||||
data };
|
||||
}
|
||||
|
||||
function deleteSystemError() {
|
||||
return { type: types.DELETE_ERROR_SYSTEM };
|
||||
}
|
||||
|
||||
function beginFetchSystem() {
|
||||
return { type: types.FETCH_SYSTEM };
|
||||
}
|
||||
|
||||
function fetchSystemSuccess(data) {
|
||||
return { type: types.FETCH_SUCCESS_SYSTEM,
|
||||
data };
|
||||
}
|
||||
|
||||
function fetchStatisticSuccess(data) {
|
||||
return { type: types.FETCH_SUCCESS_STATISTIC,
|
||||
data };
|
||||
}
|
||||
|
||||
function fetchSystemError(data) {
|
||||
return { type: types.FETCH_ERROR_SYSTEM,
|
||||
data };
|
||||
}
|
||||
|
||||
function beginFetchErrors() {
|
||||
return { type: types.FETCH_ERROR };
|
||||
}
|
||||
|
||||
function fetchErrorsSuccess(data) {
|
||||
return { type: types.FETCH_SUCCESS_ERROR,
|
||||
data };
|
||||
}
|
||||
|
||||
function fetchErrorsError(data) {
|
||||
return { type: types.FETCH_ERROR_ERROR,
|
||||
data };
|
||||
}
|
||||
|
||||
function makeUserRequest(method, data, api = "/login") {
|
||||
// returns a Promise
|
||||
return axios({
|
||||
method: method,
|
||||
url: api,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
export function changeUrl(url) {
|
||||
return dispatch => {
|
||||
dispatch(push(url));
|
||||
};
|
||||
}
|
||||
|
||||
// Example of an Async Action Creator
|
||||
// http://redux.js.org/docs/advanced/AsyncActions.html
|
||||
|
||||
export function updateSystem(data) {
|
||||
return dispatch => {
|
||||
dispatch(beginUpdateSystem());
|
||||
|
||||
return makeUserRequest("post", data, "/systems/" + data.shortName)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
const user = response.data.user
|
||||
|
||||
dispatch(updateSystemSuccess(response.data.system));
|
||||
} else {
|
||||
dispatch(updateSystemError());
|
||||
return response.data.message;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function shouldFetchSystems(state, subreddit) {
|
||||
if ((!state.system.items) || (state.system.items.length <1)) {
|
||||
return true
|
||||
} else if (state.system.isWaiting) {
|
||||
return false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchErrors(shortName) {
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch(beginFetchError());
|
||||
|
||||
return axios
|
||||
.get(backend_server + "/" + shortName + "/systems")
|
||||
.then(response => {
|
||||
if (response.data) {
|
||||
dispatch(fetchErrorsSuccess(shortName, response.data));
|
||||
} else {
|
||||
dispatch(fetchErrorsError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSystems() {
|
||||
return (dispatch, getState) => {
|
||||
if (shouldFetchSystems(getState())) {
|
||||
dispatch(beginFetchSystem());
|
||||
|
||||
return axios
|
||||
.get("/systems")
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(fetchSystemSuccess(response.data.systems));
|
||||
dispatch(fetchStatisticSuccess(response.data.stats));
|
||||
} else {
|
||||
dispatch(fetchSystemError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteSystem(shortName) {
|
||||
return dispatch => {
|
||||
dispatch(beginDeleteSystem());
|
||||
|
||||
return makeUserRequest("delete", null, "/systems/"+shortName)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(deleteSystemSuccess({shortName: shortName}));
|
||||
dispatch(push("/list-systems"));
|
||||
} else {
|
||||
dispatch(deleteSystemError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function createSystem(data) {
|
||||
return dispatch => {
|
||||
dispatch(beginCreateSystem());
|
||||
|
||||
return makeUserRequest("post", data, "/systems")
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(createSystemSuccess(response.data.system));
|
||||
} else {
|
||||
dispatch(createSystemError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
16
admin/src/client/System/system-constants.js
Normal file
16
admin/src/client/System/system-constants.js
Normal file
@@ -0,0 +1,16 @@
|
||||
export const CREATE_SYSTEM = "CREATE_SYSTEM"
|
||||
export const CREATE_SUCCESS_SYSTEM = "CREATE_SUCCESS_SYSTEM"
|
||||
export const CREATE_ERROR_SYSTEM = "CREATE_ERROR_SYSTEM"
|
||||
export const UPDATE_SYSTEM = "UPDATE_SYSTEM"
|
||||
export const UPDATE_SUCCESS_SYSTEM = "UPDATE_SUCCESS_SYSTEM"
|
||||
export const UPDATE_ERROR_SYSTEM = "UPDATE_ERROR_SYSTEM"
|
||||
export const DELETE_SYSTEM = "DELETE_SYSTEM"
|
||||
export const DELETE_SUCCESS_SYSTEM = "DELETE_SUCCESS_SYSTEM"
|
||||
export const DELETE_ERROR_SYSTEM = "DELETE_ERROR_SYSTEM"
|
||||
export const FETCH_SYSTEM = "FETCH_SYSTEM"
|
||||
export const FETCH_SUCCESS_SYSTEM = "FETCH_SUCCESS_SYSTEM"
|
||||
export const FETCH_ERROR_SYSTEM = "FETCH_ERROR_SYSTEM"
|
||||
export const FETCH_SUCCESS_STATISTIC = "FETCH_SUCCESS_STATISTIC"
|
||||
export const FETCH_ERROR = "FETCH_ERROR"
|
||||
export const FETCH_SUCCESS_ERROR = "FETCH_SUCCESS_ERROR"
|
||||
export const FETCH_ERROR_ERROR = "FETCH_ERROR_ERROR"
|
||||
95
admin/src/client/System/system-reducer.js
Normal file
95
admin/src/client/System/system-reducer.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
CREATE_SYSTEM,
|
||||
CREATE_SUCCESS_SYSTEM,
|
||||
CREATE_ERROR_SYSTEM,
|
||||
UPDATE_SYSTEM,
|
||||
UPDATE_SUCCESS_SYSTEM,
|
||||
UPDATE_ERROR_SYSTEM,
|
||||
DELETE_SYSTEM,
|
||||
DELETE_SUCCESS_SYSTEM,
|
||||
DELETE_ERROR_SYSTEM,
|
||||
FETCH_SYSTEM,
|
||||
FETCH_SUCCESS_SYSTEM,
|
||||
FETCH_ERROR_SYSTEM
|
||||
} from "./system-constants";
|
||||
|
||||
function updateObject(oldObject, newValues) {
|
||||
// Encapsulate the idea of passing a new object as the first parameter
|
||||
// to Object.assign to ensure we correctly copy data instead of mutating
|
||||
return Object.assign({}, oldObject, newValues);
|
||||
}
|
||||
|
||||
function updateObjectInArray(array, shortName, updatedItem) {
|
||||
return array.map( item => {
|
||||
if(item.shortName !== shortName) {
|
||||
// This isn't the item we care about - keep it as-is
|
||||
return item;
|
||||
}
|
||||
|
||||
// Otherwise, this is the one we want - return an updated value
|
||||
return {
|
||||
...item,
|
||||
...updatedItem
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const system = (
|
||||
state = {
|
||||
isWaiting: false,
|
||||
items: []},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
|
||||
case UPDATE_SYSTEM:
|
||||
case FETCH_SYSTEM:
|
||||
case CREATE_SYSTEM:
|
||||
|
||||
return Object.assign({}, state, { isWaiting: true});
|
||||
|
||||
case DELETE_SUCCESS_SYSTEM:
|
||||
return Object.assign({}, state, {
|
||||
items: state.items.filter(item => item.shortName !== action.data.shortName)
|
||||
});
|
||||
|
||||
case UPDATE_SUCCESS_SYSTEM:
|
||||
return Object.assign({}, state, {
|
||||
items: updateObjectInArray(state.items, action.data.shortName, action.data)
|
||||
});
|
||||
case FETCH_SUCCESS_SYSTEM:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
items: action.data
|
||||
});
|
||||
case CREATE_SUCCESS_SYSTEM:
|
||||
const newItems = state.items.concat({
|
||||
name: action.data.name,
|
||||
shortName: action.data.shortName,
|
||||
description: action.data.description,
|
||||
systemType: action.data.systemType,
|
||||
city: action.data.city,
|
||||
state: action.data.state,
|
||||
county: action.data.county,
|
||||
country: action.data.country,
|
||||
userId: action.data.userId,
|
||||
id: action.data.id,
|
||||
showScreenName: action.data.showScreenName
|
||||
});
|
||||
return Object.assign({}, state, { isWaiting: false,
|
||||
items: newItems});
|
||||
|
||||
case UPDATE_ERROR_SYSTEM:
|
||||
case FETCH_ERROR_SYSTEM:
|
||||
case CREATE_ERROR_SYSTEM:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default system;
|
||||
66
admin/src/client/Talkgroups/ListTalkgroups.js
Normal file
66
admin/src/client/Talkgroups/ListTalkgroups.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Form,
|
||||
Grid,
|
||||
Segment,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Icon,
|
||||
Table
|
||||
} from "semantic-ui-react";
|
||||
|
||||
// ----------------------------------------------------
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------------------
|
||||
class ListTalkgroups extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
}
|
||||
|
||||
|
||||
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
|
||||
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
|
||||
render() {
|
||||
const talkgroups = this.props.talkgroups;
|
||||
if (talkgroups) {
|
||||
return (
|
||||
|
||||
<Table celled>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Mode</Table.HeaderCell>
|
||||
<Table.HeaderCell>Number</Table.HeaderCell>
|
||||
<Table.HeaderCell>Alpha</Table.HeaderCell>
|
||||
<Table.HeaderCell>Description</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{talkgroups.map((talkgroup, i) => (
|
||||
<Table.Row key={talkgroup.num + "-" + i}>
|
||||
<Table.Cell>{talkgroup.mode}</Table.Cell>
|
||||
<Table.Cell>{talkgroup.num}</Table.Cell>
|
||||
<Table.Cell>{talkgroup.alpha}</Table.Cell>
|
||||
<Table.Cell>{talkgroup.description}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ListTalkgroups;
|
||||
181
admin/src/client/Talkgroups/talkgroup-actions.js
Normal file
181
admin/src/client/Talkgroups/talkgroup-actions.js
Normal file
@@ -0,0 +1,181 @@
|
||||
import axios, { post } from 'axios';
|
||||
//import { browserHistory } from "react-router"
|
||||
import * as types from "./talkgroup-constants";
|
||||
import { push } from 'connected-react-router';
|
||||
|
||||
// "Log In" action creators
|
||||
function beginImportTalkgroup() {
|
||||
return { type: types.IMPORT_TALKGROUP };
|
||||
}
|
||||
|
||||
function importTalkgroupSuccess(data) {
|
||||
return {
|
||||
type: types.IMPORT_SUCCESS_TALKGROUP,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
function importTalkgroupError(data) {
|
||||
return {
|
||||
type: types.IMPORT_ERROR_TALKGROUP,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
function beginExportTalkgroup() {
|
||||
return { type: types.EXPORT_TALKGROUP };
|
||||
}
|
||||
|
||||
function exportTalkgroupSuccess() {
|
||||
return { type: types.EXPORT_SUCCESS_TALKGROUP };
|
||||
}
|
||||
|
||||
function exportTalkgroupError() {
|
||||
return { type: types.EXPORT_ERROR_TALKGROUP };
|
||||
}
|
||||
|
||||
|
||||
function beginFetchTalkgroup() {
|
||||
return { type: types.FETCH_TALKGROUP };
|
||||
}
|
||||
|
||||
function fetchTalkgroupSuccess(data) {
|
||||
return { type: types.FETCH_SUCCESS_TALKGROUP,
|
||||
data };
|
||||
}
|
||||
|
||||
function fetchTalkgroupError(data) {
|
||||
return { type: types.FETCH_ERROR_TALKGROUP,
|
||||
data };
|
||||
}
|
||||
|
||||
|
||||
function makeUserRequest(method, data, api) {
|
||||
// returns a Promise
|
||||
return axios({
|
||||
method: method,
|
||||
url: api,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
export function changeUrl(url) {
|
||||
return dispatch => {
|
||||
dispatch(push(url));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// https://stackoverflow.com/questions/41878838/how-do-i-set-multipart-in-axios-with-react
|
||||
function fileUpload(shortName, file){
|
||||
const url = "/talkgroups/" + shortName + "/import";
|
||||
const formData = new FormData();
|
||||
formData.append('file',file)
|
||||
const config = {
|
||||
headers: {
|
||||
'content-type': 'multipart/form-data'
|
||||
}
|
||||
}
|
||||
return post(url, formData,config)
|
||||
}
|
||||
|
||||
// Example of an Async Action Creator
|
||||
// http://redux.js.org/docs/advanced/AsyncActions.html
|
||||
export function importTalkgroups(
|
||||
shortName, file,
|
||||
successPath // path to redirect to upon successful log in
|
||||
) {
|
||||
return dispatch => {
|
||||
dispatch(beginImportTalkgroup());
|
||||
|
||||
return fileUpload(shortName, file)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
var data = {};
|
||||
data.shortName = shortName;
|
||||
data.talkgroups = response.data.talkgroups
|
||||
dispatch(importTalkgroupSuccess(data));
|
||||
// use browserHistory singleton to control navigation. Will generate a
|
||||
// state change for time-traveling as we are using the react-router-redux package
|
||||
//browserHistory.push(successPath)
|
||||
|
||||
} else {
|
||||
dispatch(importTalkgroupError(response.data.message));
|
||||
return response.data.message;
|
||||
}
|
||||
})
|
||||
.catch(function(response) {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
return response.message;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function exportTalkgroups(
|
||||
shortName
|
||||
) {
|
||||
return dispatch => {
|
||||
dispatch(beginExportTalkgroup());
|
||||
|
||||
return axios
|
||||
.get("/talkgroups/" + shortName + "/export")
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
var data = {shortName: shortName, talkgroups: response.data.talkgroups}
|
||||
dispatch(fetchTalkgroupSuccess( data));
|
||||
} else {
|
||||
dispatch(fetchTalkgroupError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
function shouldFetchTalkgroups(state, shortName) {
|
||||
if (!state.talkgroup.items.hasOwnProperty(shortName)) {
|
||||
return true
|
||||
} else if (state.talkgroup.isWaiting) {
|
||||
return false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchTalkgroups(shortName) {
|
||||
return (dispatch, getState) => {
|
||||
if (shouldFetchTalkgroups(getState(), shortName)) {
|
||||
dispatch(beginFetchTalkgroup());
|
||||
|
||||
return axios
|
||||
.get("/talkgroups/"+shortName)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
var data = {shortName: shortName, talkgroups: response.data.talkgroups}
|
||||
dispatch(fetchTalkgroupSuccess( data));
|
||||
} else {
|
||||
dispatch(fetchTalkgroupError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
9
admin/src/client/Talkgroups/talkgroup-constants.js
Normal file
9
admin/src/client/Talkgroups/talkgroup-constants.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export const IMPORT_TALKGROUP = "IMPORT_TALKGROUP"
|
||||
export const IMPORT_SUCCESS_TALKGROUP = "IMPORT_SUCCESS_TALKGROUP"
|
||||
export const IMPORT_ERROR_TALKGROUP = "IMPORT_ERROR_TALKGROUP"
|
||||
export const EXPORT_TALKGROUP = "EXPORT_TALKGROUP"
|
||||
export const EXPORT_SUCCESS_TALKGROUP = "EXPORT_SUCCESS_TALKGROUP"
|
||||
export const EXPORT_ERROR_TALKGROUP = "EXPORT_ERROR_TALKGROUP"
|
||||
export const FETCH_TALKGROUP = "FETCH_TALKGROUP"
|
||||
export const FETCH_SUCCESS_TALKGROUP = "FETCH_SUCCESS_TALKGROUP"
|
||||
export const FETCH_ERROR_TALKGROUP = "FETCH_ERROR_TALKGROUP"
|
||||
72
admin/src/client/Talkgroups/talkgroup-reducer.js
Normal file
72
admin/src/client/Talkgroups/talkgroup-reducer.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
IMPORT_TALKGROUP,
|
||||
IMPORT_SUCCESS_TALKGROUP,
|
||||
IMPORT_ERROR_TALKGROUP,
|
||||
EXPORT_TALKGROUP,
|
||||
EXPORT_SUCCESS_TALKGROUP,
|
||||
EXPORT_ERROR_TALKGROUP,
|
||||
FETCH_TALKGROUP,
|
||||
FETCH_SUCCESS_TALKGROUP,
|
||||
FETCH_ERROR_TALKGROUP
|
||||
} from "./talkgroup-constants";
|
||||
|
||||
function updateObject(oldObject, newValues) {
|
||||
// Encapsulate the idea of passing a new object as the first parameter
|
||||
// to Object.assign to ensure we correctly copy data instead of mutating
|
||||
return Object.assign({}, oldObject, newValues);
|
||||
}
|
||||
|
||||
function updateItemInArray(array, itemId, updateItemCallback) {
|
||||
const updatedItems = array.map(item => {
|
||||
if(item.id !== itemId) {
|
||||
// Since we only want to update one item, preserve all others as they are now
|
||||
return item;
|
||||
}
|
||||
|
||||
// Use the provided callback to create an updated item
|
||||
const updatedItem = updateItemCallback(item);
|
||||
return updatedItem;
|
||||
});
|
||||
|
||||
return updatedItems;
|
||||
}
|
||||
|
||||
const talkgroup = (
|
||||
state = {
|
||||
isWaiting: false,
|
||||
items: {}},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
|
||||
case FETCH_TALKGROUP:
|
||||
case IMPORT_TALKGROUP:
|
||||
case EXPORT_TALKGROUP:
|
||||
return Object.assign({}, state, { isWaiting: true});
|
||||
|
||||
case IMPORT_SUCCESS_TALKGROUP:
|
||||
case FETCH_SUCCESS_TALKGROUP:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
items: {
|
||||
...state.items,
|
||||
[action.data.shortName]: action.data.talkgroups
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
case FETCH_ERROR_TALKGROUP:
|
||||
case IMPORT_ERROR_TALKGROUP:
|
||||
case EXPORT_ERROR_TALKGROUP:
|
||||
case EXPORT_SUCCESS_TALKGROUP:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default talkgroup;
|
||||
46
admin/src/client/User/user-actions.js
Normal file
46
admin/src/client/User/user-actions.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import axios from "axios";
|
||||
//import { browserHistory } from "react-router"
|
||||
import * as types from "./user-constants";
|
||||
import { push } from 'connected-react-router';
|
||||
|
||||
|
||||
// "Log Out" action creators
|
||||
function beginLogout() {
|
||||
return { type: types.LOGOUT_USER };
|
||||
}
|
||||
|
||||
function logoutSuccess() {
|
||||
return { type: types.LOGOUT_SUCCESS_USER };
|
||||
}
|
||||
|
||||
function logoutError() {
|
||||
return { type: types.LOGOUT_ERROR_USER };
|
||||
}
|
||||
|
||||
// Example of an Async Action Creator
|
||||
// http://redux.js.org/docs/advanced/AsyncActions.html
|
||||
export function manualLogout() {
|
||||
return dispatch => {
|
||||
dispatch(beginLogout());
|
||||
|
||||
return axios
|
||||
.get(account_server + "/logout", {withCredentials: true})
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(logoutSuccess());
|
||||
// use browserHistory singleton to control navigation. Will generate a
|
||||
// state change for time-traveling as we are using the react-router-redux package
|
||||
//browserHistory.push("/") // logout to home page
|
||||
dispatch(push("/"));
|
||||
} else {
|
||||
dispatch(logoutError());
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened during logout that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
6
admin/src/client/User/user-constants.js
Normal file
6
admin/src/client/User/user-constants.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export const LOGIN_SUCCESS_USER = "LOGIN_SUCCESS_USER"
|
||||
export const LOGIN_EMAIL_ERROR_USER = "LOGIN_EMAIL_ERROR_USER"
|
||||
export const LOGIN_ERROR_USER = "LOGIN_ERROR_USER"
|
||||
export const LOGOUT_USER = "LOGOUT_USER"
|
||||
export const LOGOUT_SUCCESS_USER = "LOGOUT_SUCCESS_USER"
|
||||
export const LOGOUT_ERROR_USER = "LOGOUT_ERROR_USER"
|
||||
55
admin/src/client/User/user-reducer.js
Normal file
55
admin/src/client/User/user-reducer.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
LOGOUT_USER,
|
||||
LOGOUT_SUCCESS_USER,
|
||||
LOGOUT_ERROR_USER,
|
||||
LOGIN_SUCCESS_USER,
|
||||
LOGIN_ERROR_USER,
|
||||
LOGIN_EMAIL_ERROR_USER
|
||||
} from "./user-constants";
|
||||
|
||||
const user = (
|
||||
state = {
|
||||
isWaiting: false,
|
||||
authenticated: false,
|
||||
userId: ""
|
||||
},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case LOGIN_SUCCESS_USER:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
authenticated: true,
|
||||
...action.data
|
||||
});
|
||||
case LOGIN_EMAIL_ERROR_USER:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
authenticated: false,
|
||||
userId: action.data.userId
|
||||
});
|
||||
case LOGIN_ERROR_USER:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
authenticated: false
|
||||
});
|
||||
case LOGOUT_USER:
|
||||
return Object.assign({}, state, { isWaiting: true });
|
||||
case LOGOUT_SUCCESS_USER:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
authenticated: false,
|
||||
email: "",
|
||||
userId: ""
|
||||
});
|
||||
case LOGOUT_ERROR_USER:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
authenticated: true
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default user;
|
||||
80
admin/src/client/client.js
Normal file
80
admin/src/client/client.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from "react"
|
||||
import { render } from "react-dom"
|
||||
import { Provider } from "react-redux"
|
||||
import { Switch, Route} from 'react-router-dom'
|
||||
import { applyMiddleware, createStore } from 'redux'
|
||||
import { routerMiddleware } from 'connected-react-router'
|
||||
import { ConnectedRouter } from 'connected-react-router'
|
||||
import thunk from "redux-thunk"
|
||||
import {createLogger} from "redux-logger"
|
||||
import isNode from "detect-node"
|
||||
import createRootReducer from './reducers'
|
||||
import { createBrowserHistory } from 'history'
|
||||
|
||||
import About from "./components/pure/About"
|
||||
import App from "./components/container/App"
|
||||
import NavigationContainer from "./components/container/NavigationContainer"
|
||||
import Restricted from "./components/container/Restricted"
|
||||
|
||||
// System
|
||||
import SystemContainer from "./System/SystemContainer"
|
||||
import EditSystemContainer from "./System/EditSystemContainer"
|
||||
import CreateSystemContainer from "./System/CreateSystemContainer"
|
||||
import ListSystemsContainer from "./System/ListSystemsContainer"
|
||||
|
||||
|
||||
// Create a history of your choosing (we're using a browser history in this case)
|
||||
export const history = createBrowserHistory()
|
||||
|
||||
// Build the middleware for intercepting and dispatching navigation actions
|
||||
const historyMiddleware = routerMiddleware(history)
|
||||
|
||||
let middleware = [
|
||||
thunk,
|
||||
historyMiddleware
|
||||
]
|
||||
|
||||
// only add Redux-Logger on the client-side because it causes problems server-side.
|
||||
if (!isNode) {
|
||||
middleware.push(createLogger())
|
||||
}
|
||||
|
||||
|
||||
// Add the reducer to your store on the `router` key
|
||||
// Also apply our middleware for navigatin
|
||||
const store = createStore(
|
||||
createRootReducer(history), // root reducer with router state
|
||||
applyMiddleware(thunk,
|
||||
historyMiddleware, createLogger())
|
||||
)
|
||||
|
||||
/*
|
||||
<Route path="/system/:shortName" component={Restricted(SystemContainer, store)} />
|
||||
<Route path="/update-system/:shortName" component={Restricted(EditSystemContainer, store)} />
|
||||
<Route path="/create-system" component={Restricted(CreateSystemContainer, store)} />
|
||||
<Route path="/list-systems" component={Restricted(ListSystemsContainer, store)} />
|
||||
|
||||
<Route path="/system/:shortName" component={Restricted(SystemContainer, store)} />
|
||||
<Route path="/update-system/:shortName" component={EditSystemContainer} />
|
||||
<Route path="/create-system" component={CreateSystemContainer} />
|
||||
<Route path="/list-systems" component={ListSystemsContainer} />
|
||||
*/
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<div>
|
||||
<App>
|
||||
<Switch>
|
||||
<Route path="/about" component={About} />
|
||||
<Route exact path="/" component={Restricted(ListSystemsContainer, store)} />
|
||||
{/* -- System -- */}
|
||||
<Route path="/system/:shortName" component={Restricted(SystemContainer, store)} />
|
||||
<Route path="/update-system/:shortName" component={Restricted(EditSystemContainer, store)} />
|
||||
<Route path="/create-system" component={Restricted(CreateSystemContainer, store)} />
|
||||
<Route path="/list-systems" component={Restricted(ListSystemsContainer, store)} />
|
||||
</Switch>
|
||||
</App>
|
||||
</div>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
, document.getElementById("app"))
|
||||
19
admin/src/client/components/container/App.js
Normal file
19
admin/src/client/components/container/App.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { Component } from "react"
|
||||
import NavigationContainer from "./NavigationContainer"
|
||||
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return(
|
||||
<div>
|
||||
<NavigationContainer />
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default App
|
||||
21
admin/src/client/components/container/NavigationContainer.js
Normal file
21
admin/src/client/components/container/NavigationContainer.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as userActions from "../../User/user-actions"
|
||||
import Navigation from "../pure/Navigation"
|
||||
|
||||
|
||||
// Function passed in to `connect` to subscribe to Redux store updates.
|
||||
// Any time it updates, mapStateToProps is called.
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
user: state.user
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Connects React component to the redux store
|
||||
// It does not modify the component class passed to it
|
||||
// Instead, it returns a new, connected component class, for you to use.
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
userActions
|
||||
)(Navigation)
|
||||
56
admin/src/client/components/container/Restricted.js
Normal file
56
admin/src/client/components/container/Restricted.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// in src/restricted.js
|
||||
import React, { Component } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import * as userActions from "../../User/user-actions"
|
||||
import * as types from "../../User/user-constants";
|
||||
import axios from "axios";
|
||||
/**
|
||||
* Higher-order component (HOC) to wrap restricted pages
|
||||
*/
|
||||
export default (BaseComponent, store) => {
|
||||
class Restricted extends Component {
|
||||
componentWillMount() {
|
||||
this.checkAuthentication(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.location !== this.props.location) {
|
||||
this.checkAuthentication(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
checkAuthentication(params) {
|
||||
const { history } = params;
|
||||
const authenticated = store.getState().user.authenticated;
|
||||
if (!authenticated) {
|
||||
axios
|
||||
.get(account_server + "/authenticated", {withCredentials: true})
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
const user = response.data.user
|
||||
store.dispatch({
|
||||
type: types.LOGIN_SUCCESS_USER,
|
||||
data: user
|
||||
});
|
||||
} else {
|
||||
window.location = account_server + "/login?nextLocation=admin";
|
||||
/*history.replace({ pathname: '/login',
|
||||
state: { nextPathname: params.location.pathname } });*/
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
window.location = account_server + "/login?nextLocation=admin";
|
||||
// Something happened during logout that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
render() {
|
||||
return <BaseComponent {...this.props} />;
|
||||
}
|
||||
}
|
||||
return withRouter(Restricted);
|
||||
}
|
||||
42
admin/src/client/components/pure/About.js
Normal file
42
admin/src/client/components/pure/About.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { Component } from "react";
|
||||
import { Container, Header } from "semantic-ui-react";
|
||||
|
||||
var site_name = process.env['SITE_NAME'] != null ? process.env['SITE_NAME'] : "OpenMHz";
|
||||
var admin_email = process.env['ADMIN_EMAIL'] != null ? process.env['ADMIN_EMAIL'] : "luke@openmhz.com";
|
||||
|
||||
class About extends Component {
|
||||
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Container text>
|
||||
|
||||
|
||||
<Header size='large'>Share the Air</Header>
|
||||
<p>{site_name} makes it easy to share and archive recordings from <a href="https://github.com/robotastic/trunk-recorder">Trunk Recorder</a>.
|
||||
Sharing access to these radio systems allows for other members of your community to follow local events.
|
||||
Raising awareness of what our local fire, police and EMS have to go through everyday should
|
||||
lead to greater appreciation for all the work they do that goes largely unseen.</p>
|
||||
<p>Each of the radio systems comes from different contributers around the country. The different
|
||||
systems may come and go as new ones are added and taken down. {site_name} only maintains the archive that
|
||||
is available online.</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<p> - Luke <a href="mailto:{admin_email}?Subject={site_name}" target="_top">{admin_email}</a> </p>
|
||||
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default About;
|
||||
31
admin/src/client/components/pure/Message.js
Normal file
31
admin/src/client/components/pure/Message.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import React, { Component } from "react";
|
||||
import { Modal, Icon, Button, Header } from "semantic-ui-react";
|
||||
|
||||
class Message extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
_logout = event => {
|
||||
event.preventDefault();
|
||||
this.props.manualLogout();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal open={this.props.open} onClose={this.props.onClose} basic size="small">
|
||||
<Header icon="info" content={this.props.title} />
|
||||
<Modal.Content>
|
||||
<p>{this.props.message}</p>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button color='green' onClick={this.props.onClose} inverted>
|
||||
<Icon name='checkmark' /> Got it
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Message;
|
||||
62
admin/src/client/components/pure/Navigation.js
Normal file
62
admin/src/client/components/pure/Navigation.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Menu, Dropdown, Icon, Header } from "semantic-ui-react";
|
||||
|
||||
const navStyle = {
|
||||
marginBottom: "30px"
|
||||
};
|
||||
const blankStyle = {
|
||||
marginBottom: "50px"
|
||||
};
|
||||
var account_server = process.env['ACCOUNT_SERVER'] != null ? process.env['ACCOUNT_SERVER'] : 'https://account.openmhz.com';
|
||||
var site_name = process.env['SITE_NAME'] != null ? process.env['SITE_NAME'] : "OpenMHz";
|
||||
|
||||
class Navigation extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._logout = this._logout.bind(this);
|
||||
}
|
||||
|
||||
_logout = event => {
|
||||
event.preventDefault();
|
||||
this.props.manualLogout();
|
||||
};
|
||||
|
||||
render() {
|
||||
var profileLink = account_server + "/profile";
|
||||
var billingLink = account_server + "/billing";
|
||||
return (
|
||||
<div>
|
||||
{this.props.user.authenticated ? (
|
||||
<Menu style={navStyle}>
|
||||
<Menu.Item name="systems" header>
|
||||
{site_name}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Menu position="right">
|
||||
|
||||
<Link to="/list-systems">
|
||||
<Menu.Item name="systems"> Systems</Menu.Item>
|
||||
</Link>
|
||||
<Link to="/about">
|
||||
<Menu.Item name="about">About</Menu.Item>
|
||||
</Link>
|
||||
<Dropdown item text={this.props.user.email}>
|
||||
<Dropdown.Menu>
|
||||
|
||||
<Dropdown.Item href={profileLink}>Profile</Dropdown.Item>
|
||||
{/* <Dropdown.Item href={billingLink}>Billing</Dropdown.Item> */}
|
||||
<Dropdown.Item onClick={this._logout}>Logout</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</Menu.Menu>
|
||||
</Menu>
|
||||
) : (
|
||||
<div style={blankStyle}/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Navigation;
|
||||
5
admin/src/client/components/style/style.css
Normal file
5
admin/src/client/components/style/style.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.field {
|
||||
padding-top: 10px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
21
admin/src/client/reducers/index.js
Normal file
21
admin/src/client/reducers/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { combineReducers } from "redux"
|
||||
import { connectRouter } from 'connected-react-router'
|
||||
import system from "../System/system-reducer"
|
||||
import statistic from "../System/statistic-reducer"
|
||||
import talkgroup from "../Talkgroups/talkgroup-reducer"
|
||||
import group from "../Group/group-reducer"
|
||||
import error from "../System/error-reducer"
|
||||
import permission from "../Permission/permission-reducer"
|
||||
import user from "../User/user-reducer"
|
||||
|
||||
|
||||
export default (history) => combineReducers({
|
||||
router: connectRouter(history),
|
||||
system,
|
||||
talkgroup,
|
||||
statistic,
|
||||
error,
|
||||
group,
|
||||
permission,
|
||||
user
|
||||
})
|
||||
86
admin/src/server/config/express.js
Normal file
86
admin/src/server/config/express.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import path from "path"
|
||||
import express from "express"
|
||||
import session from "express-session"
|
||||
import bodyParser from "body-parser"
|
||||
import connectMongo from "connect-mongo"
|
||||
import secrets from "./secrets"
|
||||
|
||||
const MongoStore = connectMongo(session)
|
||||
var cookie_domain = process.env['COOKIE_DOMAIN'] != null ? process.env['COOKIE_DOMAIN'] : '.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
var backend_server = process.env['BACKEND_SERVER'] != null ? process.env['BACKEND_SERVER'] : 'https://api.openmhz.com';
|
||||
var frontend_server = process.env['FRONTEND_SERVER'] != null ? process.env['FRONTEND_SERVER'] : 'https://openmhz.com';
|
||||
var socket_server = process.env['SOCKET_SERVER'] != null ? process.env['SOCKET_SERVER'] : 'wss://socket.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
var account_server = process.env['ACCOUNT_SERVER'] != null ? process.env['ACCOUNT_SERVER'] : 'https://account.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
|
||||
export default function(app, passport) {
|
||||
app.set("port", 3008)
|
||||
|
||||
// X-Powered-By header has no functional value.
|
||||
// Keeping it makes it easier for an attacker to build the site's profile
|
||||
// It can be removed safely
|
||||
app.disable("x-powered-by")
|
||||
|
||||
app.use(bodyParser.json())
|
||||
app.use(bodyParser.urlencoded({ extended: true }))
|
||||
app.use(express.static(path.join(process.cwd(), 'public')));
|
||||
|
||||
const sess = {
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
secret: secrets.sessionSecret,
|
||||
proxy: true,
|
||||
name: "sessionId",
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
domain: cookie_domain
|
||||
},
|
||||
store: new MongoStore({
|
||||
url: secrets.db,
|
||||
autoReconnect: true
|
||||
})
|
||||
}
|
||||
|
||||
var node_env = process.env.NODE_ENV;
|
||||
console.log('--------------------------');
|
||||
console.log('===> 😊 Starting Server . . .');
|
||||
console.log('===> Environment: ' + node_env);
|
||||
/*
|
||||
if(node_env === 'production') {
|
||||
console.log('===> 🚦 Note: In order for authentication to work in production');
|
||||
console.log('===> you will need a secure HTTPS connection');
|
||||
sess.cookie.secure = true; // Serve secure cookies
|
||||
}*/
|
||||
|
||||
app.use(session(sess))
|
||||
|
||||
app.use(passport.initialize())
|
||||
app.use(passport.session())
|
||||
|
||||
app.use('/*', function(req, res, next) {
|
||||
var allowedOrigins = [ account_server];
|
||||
allowedOrigins.push(frontend_server);
|
||||
allowedOrigins.push(backend_server);
|
||||
allowedOrigins.push(socket_server);
|
||||
|
||||
var origin = req.headers.origin;
|
||||
|
||||
|
||||
if (allowedOrigins.indexOf(origin) > -1) {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
} else if (req.headers["user-agent"] == 'TrunkRecorder1.0') {
|
||||
res.setHeader('Access-Control-Allow-Origin', "*");
|
||||
} else {
|
||||
res.setHeader('Access-Control-Allow-Origin', "*");
|
||||
if (origin) {
|
||||
console.warn("forcing CORS for: " + origin + " referer: " + req.headers.referer);
|
||||
}
|
||||
}
|
||||
res.header("Access-Control-Allow-Headers", "X-Requested-With");
|
||||
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS,WEBSOCKET');
|
||||
res.header('Access-Control-Allow-Credentials', 'true');
|
||||
res.header("Access-Control-Allow-Headers", "Access-Control-Allow-Headers, Access-Control-Allow-Credentials, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Max-Age");
|
||||
res.header('Access-Control-Max-Age', '600');
|
||||
next();
|
||||
});
|
||||
}
|
||||
36
admin/src/server/config/passport-strategies/local.js
Normal file
36
admin/src/server/config/passport-strategies/local.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
Configuring local strategy to authenticate strategies
|
||||
Code modified from : https://github.com/madhums/node-express-mongoose-demo/blob/master/config/passport/local.js
|
||||
*/
|
||||
|
||||
import mongoose from "mongoose"
|
||||
import { Strategy as LocalStrategy } from "passport-local"
|
||||
import User from "../../models/user"
|
||||
|
||||
/*
|
||||
By default, LocalStrategy expects to find credentials in parameters named username and password.
|
||||
If your site prefers to name these fields differently, options are available to change the defaults.
|
||||
*/
|
||||
export default new LocalStrategy({
|
||||
usernameField: "email"
|
||||
}, (email, password, done) => {
|
||||
User.findOne({
|
||||
$or: [{
|
||||
email
|
||||
}, {
|
||||
local: {
|
||||
email
|
||||
}
|
||||
}]
|
||||
}, (err, user) => {
|
||||
if(!user) return done(null, false, { message: `Email ${email} not found` })
|
||||
|
||||
user.comparePassword(password, (err, isMatch) => {
|
||||
if (isMatch) {
|
||||
return done(null, user)
|
||||
} else {
|
||||
return done(null, false, { message: "Invalid email or password" })
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
26
admin/src/server/config/passport.js
Normal file
26
admin/src/server/config/passport.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/* Initializing PassportJS */
|
||||
import User from "../models/user"
|
||||
import local from "./passport-strategies/local"
|
||||
|
||||
export default function (app, passport) {
|
||||
// Configure Passport authenticated session persistence.
|
||||
//
|
||||
// In order to restore authentication state across HTTP requests, Passport needs
|
||||
// to serialize users into and deserialize users out of the session. The
|
||||
// typical implementation of this is as simple as supplying the user ID when
|
||||
// serializing, and querying the user record by ID from the database when
|
||||
// deserializing.
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, user.id)
|
||||
})
|
||||
|
||||
passport.deserializeUser((id, done) => {
|
||||
User.findById(id, (err, user) => {
|
||||
done(err, user)
|
||||
})
|
||||
})
|
||||
|
||||
// use the following strategies
|
||||
passport.use(local)
|
||||
}
|
||||
|
||||
15
admin/src/server/config/secrets.js
Normal file
15
admin/src/server/config/secrets.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/** Important **/
|
||||
/** You should not be committing this file to GitHub **/
|
||||
/** Repeat: DO! NOT! COMMIT! THIS! FILE! TO! YOUR! REPO! **/
|
||||
var host = process.env['MONGO_NODE_DRIVER_HOST'] != null ? process.env['MONGO_NODE_DRIVER_HOST'] : 'localhost';
|
||||
var port = process.env['MONGO_NODE_DRIVER_PORT'] != null ? process.env['MONGO_NODE_DRIVER_PORT'] : 27017;
|
||||
var mongoUrl = 'mongodb://' + host + ':' + port + '/scanner';
|
||||
|
||||
|
||||
|
||||
const secrets = {
|
||||
db: mongoUrl,
|
||||
sessionSecret: "letthisbeyoursecret"
|
||||
}
|
||||
|
||||
export default secrets
|
||||
297
admin/src/server/controllers/groups.js
Normal file
297
admin/src/server/controllers/groups.js
Normal file
@@ -0,0 +1,297 @@
|
||||
import Group from "../models/group";
|
||||
import System from "../models/system";
|
||||
import mongoose from "mongoose";
|
||||
import passport from "passport";
|
||||
|
||||
//router.get('/system/:shortName/group/:groupId?', isLoggedIn, function(req, res) {
|
||||
|
||||
exports.getGroups = function(req, res, next) {
|
||||
process.nextTick(function() {
|
||||
System.findOne(
|
||||
{
|
||||
shortName: req.params.shortName.toLowerCase()
|
||||
},
|
||||
function(err, system) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
res.json({ success: false, message: err });
|
||||
return;
|
||||
}
|
||||
if (!system) {
|
||||
console.error(err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: "That Short Name does not exist."
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!system.userId.equals(req.user._id)) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: "You are not the user associated with this system."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.params.groupId) {
|
||||
var groupId = req.params.groupId.toLowerCase();
|
||||
|
||||
Group.findOne(
|
||||
{
|
||||
_id: new mongoose.Types.ObjectId(groupId),
|
||||
shortName: req.params.shortName.toLowerCase()
|
||||
},
|
||||
{ sort: { position: -1 } },
|
||||
function(err, group) {
|
||||
if (err) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
res.json({
|
||||
success: true,
|
||||
group: group
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
Group.find(
|
||||
{
|
||||
shortName: req.params.shortName.toLowerCase()
|
||||
}).sort("position").exec(
|
||||
function(err, groups) {
|
||||
if (err) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
res.json({
|
||||
success: true,
|
||||
groups: groups
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
exports.deleteGroup = function(req, res, next) {
|
||||
//router.get('/system/:shortName/remove_group/:groupId', isLoggedIn, function(req, res) {
|
||||
System.findOne({
|
||||
'shortName': req.params.shortName.toLowerCase()
|
||||
}, function(err, system) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
res.json({ success: false, message: err });
|
||||
return;
|
||||
}
|
||||
if (!system) {
|
||||
console.error(err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: "That Short Name does not exist."
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!system.userId.equals(req.user._id)) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: "You are not the user associated with this system."
|
||||
});
|
||||
return;
|
||||
}
|
||||
Group.findOneAndRemove({
|
||||
shortName: req.params.shortName.toLowerCase(),
|
||||
'_id': new mongoose.Types.ObjectId(req.params.groupId)
|
||||
}, function(err) {
|
||||
|
||||
if (err) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
res.json({
|
||||
success: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
// removed!
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
exports.reorderGroups = function(req, res, next) {
|
||||
//router.post('/system/:shortName/group_order', isLoggedIn, function(req, res) {
|
||||
process.nextTick(function() {
|
||||
System.findOne(
|
||||
{
|
||||
shortName: req.params.shortName.toLowerCase()
|
||||
},
|
||||
function(err, system) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
res.json({ success: false, message: err });
|
||||
return;
|
||||
}
|
||||
if (!system) {
|
||||
console.error(err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: "That Short Name does not exist."
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!system.userId.equals(req.user._id)) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: "You are not the user associated with this system."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var groupOrder = JSON.parse(req.body.groupOrder);
|
||||
for (var i = 0; i < groupOrder.length; i++) {
|
||||
|
||||
console.log("in group: " + groupOrder[i] + " position: " + i);
|
||||
Group.findOneAndUpdate(
|
||||
{
|
||||
_id: new mongoose.Types.ObjectId(groupOrder[i]),
|
||||
shortName: req.params.shortName.toLowerCase()
|
||||
},
|
||||
{
|
||||
position: i
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
upsert: false
|
||||
},
|
||||
function(err, group) {
|
||||
console.log("Save group: " + group._id + " position: " + group.position);
|
||||
if (err) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
|
||||
console.log(
|
||||
"Error: Group Order - " +
|
||||
err +
|
||||
" Shortname: " +
|
||||
req.params.shortName.toLowerCase()
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
res.json({
|
||||
success: true
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
exports.upsertGroup = function(req, res, next) {
|
||||
//router.post('/system/:shortName/group/:groupId?', isLoggedIn, function(req, res) {
|
||||
process.nextTick(function() {
|
||||
System.findOne(
|
||||
{
|
||||
shortName: req.params.shortName.toLowerCase()
|
||||
},
|
||||
function(err, system) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
res.json({ success: false, message: err });
|
||||
return;
|
||||
}
|
||||
if (!system) {
|
||||
console.error(err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: "That Short Name does not exist."
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!system.userId.equals(req.user._id)) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: "You are not the user associated with this system."
|
||||
});
|
||||
return;
|
||||
}
|
||||
var message = "";
|
||||
var groupName = req.body.groupName.replace(/[^\w\s]/g, "");
|
||||
var talkgroups = JSON.parse(req.body.talkgroups);
|
||||
console.log(
|
||||
"Talkgroups: " +
|
||||
talkgroups +
|
||||
" for group: " +
|
||||
groupName +
|
||||
" array: " +
|
||||
Array.isArray(talkgroups)
|
||||
);
|
||||
if (!talkgroups.length || !Array.isArray(talkgroups)) {
|
||||
message = "Talkgroups need to be selected, group can not be empty ";
|
||||
}
|
||||
if (!groupName.length) {
|
||||
message = message + "Group Name is required";
|
||||
}
|
||||
|
||||
if (message.length) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: message
|
||||
});
|
||||
return;
|
||||
}
|
||||
// if we got here, that means that everything is good for updating/inserting
|
||||
|
||||
var groupId = req.params.groupId;
|
||||
|
||||
Group.findOneAndUpdate(
|
||||
{
|
||||
_id: new mongoose.Types.ObjectId(groupId),
|
||||
shortName: req.params.shortName.toLowerCase()
|
||||
},
|
||||
{
|
||||
groupName: groupName,
|
||||
talkgroups: talkgroups
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
upsert: true
|
||||
},
|
||||
function(err, group) {
|
||||
if (err) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
res.json({
|
||||
success: true,
|
||||
group: group
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
243
admin/src/server/controllers/permissions.js
Normal file
243
admin/src/server/controllers/permissions.js
Normal file
@@ -0,0 +1,243 @@
|
||||
import mongoose from "mongoose";
|
||||
import passport from "passport";
|
||||
import Permission from "../models/permission";
|
||||
import System from "../models/system";
|
||||
import User from "../models/user";
|
||||
|
||||
|
||||
exports.fetchPermissions = function(req, res, next) {
|
||||
Permission.find(
|
||||
{
|
||||
shortName: req.params.shortName.toLowerCase()
|
||||
},
|
||||
function(err, permissions) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
res.json({ success: false, message: err });
|
||||
return;
|
||||
}
|
||||
var userIds = permissions.map(permission => {return permission.userId});
|
||||
User.find(
|
||||
{
|
||||
_id: {
|
||||
$in: userIds
|
||||
}
|
||||
},
|
||||
function(err, users) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
res.json({ success: false, message: err });
|
||||
return;
|
||||
}
|
||||
var returnUsers = users.map(user =>{
|
||||
var permission = permissions.find(obj => {
|
||||
|
||||
if (obj.userId.toString() == user._id.toString()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!permission) {
|
||||
console.error("Error - Permission not found for user: " + user._id);
|
||||
res.json({ success: false, message: "Permission not found" });
|
||||
return;
|
||||
}
|
||||
return {_id: permission._id, userId: user._id, firstName: user.firstName, lastName: user.lastName, email: user.email, role: permission.role };
|
||||
});
|
||||
|
||||
res.json({ success: true, permissions: returnUsers });
|
||||
});
|
||||
return;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
exports.addPermission = async (req, res, next) => {
|
||||
//router.post('/system/:shortName/group/:groupId?', isLoggedIn, function(req, res) {
|
||||
|
||||
|
||||
let role = req.body.role;
|
||||
var system = false;
|
||||
var user = false;
|
||||
var existing = false;
|
||||
var saved = false;
|
||||
if (!role || (role >= 20) || (role <1)) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: "Invalid Role"
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.log("role looks good");
|
||||
try {
|
||||
system = await System.findOne(
|
||||
{
|
||||
shortName: req.params.shortName.toLowerCase()
|
||||
});
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.json({ success: false, message: err });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!system) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: "That System does not exist."
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.log("system looks good")
|
||||
try {
|
||||
user = await User.findOne(
|
||||
{
|
||||
email: req.body.email
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.json({ success: false, message: err });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
res.json({ success: false, message: "User not found" });
|
||||
return;
|
||||
}
|
||||
console.log("user looks good")
|
||||
try {
|
||||
existing = await Permission.findOne({userId: user._id, systemId: system._id});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.json({ success: false, message: err });
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
res.json({ success: false, message: "User already has acccess" });
|
||||
return;
|
||||
}
|
||||
console.log("no existing")
|
||||
var permission = new Permission({
|
||||
userId: user._id,
|
||||
systemId: system._id,
|
||||
shortName: system.shortName,
|
||||
role: role
|
||||
});
|
||||
|
||||
|
||||
try {
|
||||
saved = await permission.save()
|
||||
} catch(err) {
|
||||
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.log("saved it")
|
||||
var newPermission = saved.toObject();
|
||||
newPermission.firstName = user.firstName;
|
||||
newPermission.lastName = user.lastName;
|
||||
newPermission.email = user.email;
|
||||
|
||||
|
||||
console.log("Add Permission to: " + system.shortName + " for: " + newPermission.email + " by: " + req.user.email);
|
||||
res.json({
|
||||
success: true,
|
||||
permission: newPermission
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
exports.updatePermission = function(req, res, next) {
|
||||
process.nextTick(function() {
|
||||
Permission.findOne(
|
||||
{
|
||||
_id: new mongoose.Types.ObjectId(req.params.permissionId),
|
||||
shortName: req.params.shortName.toLowerCase(req.params.shortName)
|
||||
},
|
||||
|
||||
function(err, permission) {
|
||||
if (err) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (permission.role >= 20) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: "Cannot change permissions of system owner"
|
||||
});
|
||||
return;
|
||||
}
|
||||
var newRole = req.body.role;
|
||||
if (!newRole || (newRole >= 20) || (newRole <1)) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: "Invalid Role"
|
||||
});
|
||||
return;
|
||||
}
|
||||
permission.role = newRole;
|
||||
permission.save(function(err, saved) {
|
||||
if (err) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
permission: saved.toJSON()
|
||||
});
|
||||
return;
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
exports.deletePermission = function(req, res, next) {
|
||||
//router.get('/system/:shortName/remove_group/:groupId', isLoggedIn, function(req, res) {
|
||||
|
||||
Permission.findOneAndRemove({
|
||||
shortName: req.params.shortName.toLowerCase(),
|
||||
'_id': new mongoose.Types.ObjectId(req.params.permissionId),
|
||||
role: { $lt : 20}
|
||||
}, function(err, permission) {
|
||||
|
||||
if (err) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!permission) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: "Permission not found"
|
||||
});
|
||||
return;
|
||||
|
||||
} else {
|
||||
|
||||
res.json({
|
||||
success: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
// removed!
|
||||
});
|
||||
|
||||
|
||||
};
|
||||
509
admin/src/server/controllers/systems.js
Normal file
509
admin/src/server/controllers/systems.js
Normal file
@@ -0,0 +1,509 @@
|
||||
import mongoose from "mongoose";
|
||||
import passport from "passport";
|
||||
import System from "../models/system";
|
||||
import SystemStat from "../models/system_stat";
|
||||
import Talkgroup from "../models/talkgroup";
|
||||
import Group from "../models/group";
|
||||
import secrets from "../config/secrets";
|
||||
import uuid from 'node-uuid';
|
||||
const crypto = require("crypto");
|
||||
|
||||
exports.isLoggedIn = function (req, res, next) {
|
||||
if (req.isAuthenticated()) return next();
|
||||
res.redirect(account_server + "/login");
|
||||
};
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
exports.listSystems = function (req, res, next) {
|
||||
console.log("Listing Systems for: " + req.user._id);
|
||||
System.find({
|
||||
userId: mongoose.Types.ObjectId(req.user._id)
|
||||
},
|
||||
function (err, systems) {
|
||||
if (err) {
|
||||
console.error("Error - ListSystems: " + err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var shortNames = new Array();
|
||||
for (var i = 0; i < systems.length; i++) {
|
||||
shortNames.push(systems[i].shortName);
|
||||
}
|
||||
|
||||
SystemStat.find({
|
||||
shortName: {
|
||||
$in: shortNames
|
||||
}
|
||||
},
|
||||
function (err, stats) {
|
||||
var returnSys = systems.map(obj => {
|
||||
var rObj = (({
|
||||
name,
|
||||
shortName,
|
||||
description,
|
||||
systemType,
|
||||
city,
|
||||
state,
|
||||
county,
|
||||
country,
|
||||
userId,
|
||||
key,
|
||||
planType,
|
||||
showScreenName
|
||||
}) => ({
|
||||
name,
|
||||
shortName,
|
||||
description,
|
||||
systemType,
|
||||
city,
|
||||
state,
|
||||
county,
|
||||
country,
|
||||
userId,
|
||||
key,
|
||||
planType,
|
||||
showScreenName
|
||||
}))(obj);
|
||||
if (obj.showScreenName) {
|
||||
rObj.screenName = req.user.screenName;
|
||||
} else {
|
||||
rObj.screenName = null;
|
||||
}
|
||||
rObj.id = obj._id;
|
||||
return rObj;
|
||||
});
|
||||
var sys_stats = {}
|
||||
var MS_PER_MINUTE = 60000;
|
||||
var now = new Date();
|
||||
for (var i = 0; i < stats.length; i++) {
|
||||
sys_stats[stats[i].shortName] = {};
|
||||
|
||||
sys_stats[stats[i].shortName].callTotals = stats[i].callTotals;
|
||||
sys_stats[stats[i].shortName].errorTotals = stats[i].errorTotals;
|
||||
sys_stats[stats[i].shortName].errorStats = stats[i].errorStats;
|
||||
sys_stats[stats[i].shortName].usageBytes = stats[i].usageBytes;
|
||||
sys_stats[stats[i].shortName].usageMb = stats[i].usageMb;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
systems: returnSys,
|
||||
stats: sys_stats
|
||||
});
|
||||
return;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function remove_system(shortName) {
|
||||
/*
|
||||
var sysDir = config.mediaDirectory + "/" + shortName + "/"
|
||||
console.log("Removing DIR: " + sysDir);
|
||||
fs.remove(sysDir, function(err) {
|
||||
if (err) return console.error(err)
|
||||
});
|
||||
|
||||
Transmission.remove({
|
||||
shortName: shortName
|
||||
}, function(err) {
|
||||
if (err) return console.error(err);
|
||||
// removed!
|
||||
});
|
||||
*/
|
||||
System.remove({
|
||||
shortName: shortName
|
||||
},
|
||||
function (err) {
|
||||
if (err) return console.error(err);
|
||||
// removed!
|
||||
}
|
||||
);
|
||||
|
||||
Talkgroup.remove({
|
||||
shortName: shortName
|
||||
},
|
||||
function (err) {
|
||||
if (err) return console.error(err);
|
||||
// removed!
|
||||
}
|
||||
);
|
||||
|
||||
Group.remove({
|
||||
shortName: shortName
|
||||
},
|
||||
function (err) {
|
||||
if (err) return console.error(err);
|
||||
// removed!
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
exports.deleteSystem = function (req, res, next) {
|
||||
//router.get('/delete_system/:shortName', isLoggedIn, function(req, res) {
|
||||
System.findOne({
|
||||
shortName: req.params.shortName.toLowerCase()
|
||||
},
|
||||
function (err, system) {
|
||||
if (err) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!system) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: "That Short Name does not exist."
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!system.userId.equals(req.user._id)) {
|
||||
console.log("Error deleting syste - Wrong User - sys: " + system.userId + " profile: " + mongoose.Types.ObjectId(req.user._id));
|
||||
res.json({
|
||||
success: false,
|
||||
message: "You are not the user associated with this system."
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.log("Removing: " + system.name);
|
||||
remove_system(system.shortName);
|
||||
res.json({
|
||||
success: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// -------------------------------------------
|
||||
exports.ownSystem = async function (req, res, next) {
|
||||
try {
|
||||
var system = await System.findOne({
|
||||
shortName: req.body.shortName.toLowerCase()
|
||||
});
|
||||
|
||||
if (!system) {
|
||||
console.error("ERROR: OwnSystem - system does not exist: " + req.body.shortName.toLowerCase());
|
||||
res.json({
|
||||
success: false,
|
||||
message: "That System does not exist."
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!system.userId.equals(req.user._id)) {
|
||||
console.error("ERROR: OwnSystem - not users system " + req.body.shortName.toLowerCase());
|
||||
|
||||
res.json({
|
||||
success: false,
|
||||
message: "You are not the user associated with this system."
|
||||
});
|
||||
console.error("System: " + system.userId + " User: " + req.user._id);
|
||||
return;
|
||||
}
|
||||
|
||||
res.locals.test = "Yoho!"
|
||||
res.locals.system = system;
|
||||
console.log(system);
|
||||
console.log(res.locals);
|
||||
next();
|
||||
|
||||
} catch (err) {
|
||||
console.error("ERROR: Unique ShortName - caught: " + err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
exports.uniqueShortName = async function (req, res, next) {
|
||||
try {
|
||||
var system = await System.findOne({
|
||||
shortName: res.locals.shortName
|
||||
});
|
||||
|
||||
if (system) {
|
||||
console.error("ERROR: Unique Shortname already in use: " + req.body.shortName.toLowerCase());
|
||||
res.json({
|
||||
success: false,
|
||||
message: "Short Name already in use"
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
} catch (err) {
|
||||
console.error("ERROR: Unique ShortName - caught: " + err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
exports.updateSystem = async function (req, res, next) {
|
||||
|
||||
console.log(res.locals)
|
||||
res.locals.system.name = res.locals.name;
|
||||
res.locals.system.city = res.locals.city;
|
||||
res.locals.system.state = res.locals.state;
|
||||
res.locals.system.county = res.locals.county;
|
||||
res.locals.system.country = res.locals.country;
|
||||
res.locals.system.description = res.locals.description;
|
||||
res.locals.system.systemType = res.locals.systemType;
|
||||
res.locals.system.showScreenName = res.locals.showScreenName;
|
||||
|
||||
res.locals.system.save(function (err) {
|
||||
if (err) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
var returnSys = (({
|
||||
name,
|
||||
shortName,
|
||||
description,
|
||||
systemType,
|
||||
city,
|
||||
state,
|
||||
county,
|
||||
country,
|
||||
userId,
|
||||
key,
|
||||
showScreenName
|
||||
}) => ({
|
||||
name,
|
||||
shortName,
|
||||
description,
|
||||
systemType,
|
||||
city,
|
||||
state,
|
||||
county,
|
||||
country,
|
||||
userId,
|
||||
key,
|
||||
showScreenName
|
||||
}))(res.locals.system);
|
||||
returnSys.id = res.locals.system._id;
|
||||
res.json({
|
||||
success: true,
|
||||
system: returnSys
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
exports.validateSystem = async function (req, res, next) {
|
||||
try {
|
||||
res.locals.showScreenName = req.body.showScreenName;
|
||||
|
||||
if (!req.body.name || (req.body.name.length < 2)) {
|
||||
console.error("ERROR: Validate System - req.body.name");
|
||||
res.json({
|
||||
success: false,
|
||||
message: "System Name is Required"
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.locals.name = req.body.name.replace(/[^\w\s\.\,\-\_]/gi, '');
|
||||
|
||||
if (!req.body.description || (req.body.description.length < 2)) {
|
||||
console.error("ERROR: Validate System - req.body.description");
|
||||
res.json({
|
||||
success: false,
|
||||
message: "System Description is Required"
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.locals.description = req.body.description.replace(/[^\w\s\.\,\-\_]/gi, '');
|
||||
|
||||
if (!req.body.shortName || (req.body.shortName.length < 2)) {
|
||||
console.error("ERROR: Validate System - req.body.shortName");
|
||||
res.json({
|
||||
success: false,
|
||||
message: "System ShortName is Required"
|
||||
});
|
||||
return;
|
||||
}
|
||||
var tempShortName = req.body.shortName.replace(/[^\w]/gi, '');
|
||||
res.locals.shortName = tempShortName.toLowerCase();
|
||||
|
||||
// -------------------- Handle all the different SystemTypes of Locations
|
||||
|
||||
|
||||
// make sure it is one of the expected types
|
||||
if ((req.body.systemType != "state") && (req.body.systemType != "city") && (req.body.systemType != "county") && (req.body.systemType != "international")) {
|
||||
console.error("ERROR: Validate System - req.body.systemType: " + req.body.systemType);
|
||||
res.json({
|
||||
success: false,
|
||||
message: "Select system location"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// copy it over
|
||||
res.locals.systemType = req.body.systemType;
|
||||
|
||||
// now check to make sure the correct things for location is filled out
|
||||
if (req.body.systemType === "state" ||
|
||||
req.body.systemType === "city" ||
|
||||
req.body.systemType === "county") {
|
||||
if (!req.body.state || (req.body.state === "") || (req.body.state.length < 2)) {
|
||||
console.error("ERROR: Validate System - req.body.state: " + req.body.state);
|
||||
res.json({
|
||||
success: false,
|
||||
message: "Select the State for the System"
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.locals.state = req.body.state;
|
||||
}
|
||||
|
||||
if (req.body.systemType === "city") {
|
||||
if (!req.body.city || (req.body.city === "") || (req.body.city.length < 2)) {
|
||||
console.error("ERROR: Validate System - req.body.systemType: " + req.body.systemType);
|
||||
res.json({
|
||||
success: false,
|
||||
message: "Enter the city for the System"
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.locals.city = req.body.city.replace(/[^\w\s\.\,\-\_]/gi, '');
|
||||
}
|
||||
|
||||
if (req.body.systemType === "county") {
|
||||
if (!req.body.county || (req.body.county === "") || (req.body.county.length < 2)) {
|
||||
console.error("ERROR: Validate System - req.body.county: " + req.body.county);
|
||||
res.json({
|
||||
success: false,
|
||||
message: "Enter the county for the System"
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.locals.county = req.body.county.replace(/[^\w\s\.\,\-\_]/gi, '');
|
||||
}
|
||||
|
||||
if (req.body.systemType === "international") {
|
||||
if (!req.body.country || (req.body.country === "") || (req.body.country.length < 2)) {
|
||||
console.error("ERROR: Validate System - req.body.country: " + req.body.country);
|
||||
res.json({
|
||||
success: false,
|
||||
message: "Enter the country for the System"
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.locals.country = req.body.country.replace(/[^\w\s\.\,\-\_]/gi, '');
|
||||
}
|
||||
|
||||
|
||||
} catch (err) {
|
||||
console.error("ERROR: Validate System - caught: " + err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
exports.createSystem = async function (req, res, next) {
|
||||
|
||||
var key = uuid();
|
||||
key = crypto
|
||||
.createHash("sha256")
|
||||
.update(key)
|
||||
.update("salt")
|
||||
.digest("hex");
|
||||
const system = (({
|
||||
name,
|
||||
shortName,
|
||||
description,
|
||||
systemType,
|
||||
city,
|
||||
state,
|
||||
county,
|
||||
country,
|
||||
showScreenName
|
||||
}) => ({
|
||||
name,
|
||||
shortName,
|
||||
description,
|
||||
systemType,
|
||||
city,
|
||||
state,
|
||||
county,
|
||||
country,
|
||||
showScreenName
|
||||
}))(res.locals);
|
||||
system.key = key;
|
||||
system.userId = mongoose.Types.ObjectId(req.user._id);
|
||||
System.create(system, function (err, newSys) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
var returnSys = (({
|
||||
name,
|
||||
shortName,
|
||||
description,
|
||||
systemType,
|
||||
city,
|
||||
state,
|
||||
county,
|
||||
country,
|
||||
userId,
|
||||
showScreenName,
|
||||
key
|
||||
}) => ({
|
||||
name,
|
||||
shortName,
|
||||
description,
|
||||
systemType,
|
||||
city,
|
||||
state,
|
||||
county,
|
||||
country,
|
||||
userId,
|
||||
showScreenName,
|
||||
key
|
||||
}))(newSys);
|
||||
returnSys.id = newSys._id;
|
||||
res.json({
|
||||
success: true,
|
||||
system: returnSys
|
||||
});
|
||||
return;
|
||||
});
|
||||
}
|
||||
253
admin/src/server/controllers/talkgroups.js
Normal file
253
admin/src/server/controllers/talkgroups.js
Normal file
@@ -0,0 +1,253 @@
|
||||
import mongoose from "mongoose";
|
||||
import passport from "passport";
|
||||
import Talkgroup from "../models/talkgroup";
|
||||
import System from "../models/system";
|
||||
import secrets from "../config/secrets";
|
||||
import fs from 'fs-extra';
|
||||
|
||||
const crypto = require("crypto");
|
||||
var parse = require('csv-parse');
|
||||
|
||||
exports.isLoggedIn = function(req, res, next) {
|
||||
if (req.isAuthenticated()) return next();
|
||||
res.redirect(account_server + "/login");
|
||||
};
|
||||
|
||||
//router.get("/tg_export/:shortName", isLoggedIn, function(req, res) {
|
||||
exports.exportTalkgroups = function(req, res, next) {
|
||||
process.nextTick(function() {
|
||||
System.findOne(
|
||||
{
|
||||
shortName: req.params.shortName.toLowerCase()
|
||||
},
|
||||
function(err, system) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
res.json({ success: false, message: err });
|
||||
return;
|
||||
}
|
||||
if (!system) {
|
||||
console.error(err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: "That Short Name does not exist."
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!system.userId.equals(req.user._id)) {
|
||||
res.json({
|
||||
success: false,
|
||||
message: "You are not the user associated with this system."
|
||||
});
|
||||
return;
|
||||
}
|
||||
Talkgroup.find(
|
||||
{
|
||||
shortName: req.params.shortName.toLowerCase()
|
||||
},
|
||||
function(err, talkgroups) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
res.json({ success: false, message: err });
|
||||
return;
|
||||
}
|
||||
if (!talkgroups || !Array.isArray(talkgroups)) {
|
||||
console.error(err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: "Talkgroups were not found."
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", "text/csv");
|
||||
talkgroups.forEach(function(item) {
|
||||
/* res.write('"' + item.num + '", "'
|
||||
+ item.mode + '", "'
|
||||
+ item.alpha + '", "'
|
||||
+ item.description + '", "'
|
||||
+ item.tag + '", "'
|
||||
+ item.group + '", "'
|
||||
+ item.priority+ '"\r\n');*/
|
||||
if (item) {
|
||||
var sending = "";
|
||||
sending = item.num + ", ";
|
||||
if (item.mode) {
|
||||
sending = sending + item.mode.toString().replace(/\,/g, "") + ", ";
|
||||
} else {
|
||||
sending = sending + " ,";
|
||||
}
|
||||
if (item.alpa) {
|
||||
sending = sending + item.alpha.toString().replace(/\,/g, "") + ", ";
|
||||
} else {
|
||||
sending = sending + " ,";
|
||||
}
|
||||
if (item.description) {
|
||||
sending = sending + item.description.toString().replace(/\,/g, "") + ", ";
|
||||
} else {
|
||||
sending = sending + " ,";
|
||||
}
|
||||
if (item.tag) {
|
||||
sending = sending + item.tag.toString().replace(/\,/g, "") + ", ";
|
||||
} else {
|
||||
sending = sending + " ,";
|
||||
}
|
||||
if (item.group) {
|
||||
sending = sending + item.group.toString().replace(/\,/g, "") + ", ";
|
||||
} else {
|
||||
sending = sending + " ,";
|
||||
}
|
||||
if (item.priority) {
|
||||
sending = sending + item.priority.toString().replace(/\,/g, "") + "\r\n";
|
||||
} else {
|
||||
sending = sending + "\r\n";
|
||||
}
|
||||
res.write(sending);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
res.end();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
function csv_import(shortName, filename, callback) {
|
||||
var talkgroups = [];
|
||||
Talkgroup.remove(
|
||||
{
|
||||
shortName: shortName
|
||||
},
|
||||
function(err) {
|
||||
if (err) {
|
||||
console.error("Error: csv import: " + err);
|
||||
callback(err, null);
|
||||
} else {
|
||||
var parser = parse(
|
||||
{
|
||||
columns: [
|
||||
"num",
|
||||
"mode",
|
||||
"alpha",
|
||||
"description",
|
||||
"tag",
|
||||
"group",
|
||||
"priority"
|
||||
]
|
||||
},
|
||||
async function(err, data) {
|
||||
if (err) {
|
||||
console.error("Error: csv import 2: " + err);
|
||||
callback(err, null);
|
||||
} else {
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var row = data[i];
|
||||
var newTg = new Talkgroup();
|
||||
newTg.num = parseInt(row.num);
|
||||
newTg.alpha = row.alpha;
|
||||
newTg.mode = row.mode;
|
||||
newTg.description = row.description;
|
||||
newTg.tag = row.tag.toLowerCase();
|
||||
newTg.group = row.group.toLowerCase();
|
||||
newTg.priority = row.priority;
|
||||
newTg.shortName = shortName;
|
||||
|
||||
await newTg.save(function(err) {
|
||||
if (err) {
|
||||
console.log("Error: CSV import: " + err);
|
||||
callback(err, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
callback(null, data);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
fs.createReadStream(filename).pipe(parser);
|
||||
// removed!
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
//router.post("/tg_import/:shortName", isLoggedIn, upload.single('file'), function(req, res, next) {
|
||||
exports.importTalkgroups = function(req, res, next) {
|
||||
process.nextTick(function() {
|
||||
System.findOne(
|
||||
{
|
||||
shortName: req.params.shortName.toLowerCase()
|
||||
},
|
||||
function(err, system) {
|
||||
if (err) {
|
||||
console.error("Error - importTalkgroups: " + err);
|
||||
res.json({ success: false, message: err });
|
||||
return;
|
||||
}
|
||||
if (!system) {
|
||||
console.error("Error - importTalkgroups: System not found - " + req.params.shortName.toLowerCase());
|
||||
res.json({
|
||||
success: false,
|
||||
message: "That Short Name does not exist."
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!system.userId.equals(req.user._id)) {
|
||||
console.error("Error - importTalkgroups: you are not the user for hte system: " + req.user._id);
|
||||
res.json({
|
||||
success: false,
|
||||
message: "You are not the user associated with this system."
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!req.file || req.file.size === 0) {
|
||||
res.json({ success: false, message: "Please select a file." });
|
||||
return;
|
||||
}
|
||||
|
||||
fs.exists(req.file.path, function(exists) {
|
||||
if (exists) {
|
||||
csv_import(system.shortName, req.file.path, function(err, data) {
|
||||
if (err) {
|
||||
console.error("Erorr - CSV import, callback: " + err);
|
||||
res.json({ success: false, message: err });
|
||||
res.end();
|
||||
return;
|
||||
} else {
|
||||
res.json({ success: true, talkgroups: data });
|
||||
return;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error("Error - importTalkgroups: no file");
|
||||
res.end(
|
||||
"Well, there is no magic for those who don’t believe in it!"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
exports.fetchTalkgroups = function(req, res, next) {
|
||||
Talkgroup.find(
|
||||
{
|
||||
shortName: req.params.shortName.toLowerCase()
|
||||
},
|
||||
function(err, talkgroups) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
res.json({ success: false, message: err });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, talkgroups: talkgroups });
|
||||
return;
|
||||
}
|
||||
);
|
||||
};
|
||||
218
admin/src/server/index.js
Normal file
218
admin/src/server/index.js
Normal file
@@ -0,0 +1,218 @@
|
||||
import fs from "fs"
|
||||
import mongoose from "mongoose"
|
||||
import express from "express"
|
||||
import passport from "passport"
|
||||
import webpack from "webpack"
|
||||
import path from "path"
|
||||
import config from "../../webpack/webpack.config.dev.js"
|
||||
import secrets from "./config/secrets"
|
||||
import configurePassport from "./config/passport"
|
||||
import configureExpress from "./config/express"
|
||||
import systems from "./controllers/systems"
|
||||
import groups from "./controllers/groups"
|
||||
import permissions from "./controllers/permissions"
|
||||
import talkgroups from "./controllers/talkgroups"
|
||||
|
||||
import Permission from "./models/permission";
|
||||
import "./models/user"
|
||||
import multer from 'multer'
|
||||
var upload = multer({
|
||||
dest: 'uploads/'
|
||||
});
|
||||
// -------------------------------------------
|
||||
|
||||
const app = express()
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
const connect = () => {
|
||||
mongoose.connect(secrets.db, (err, res) => {
|
||||
if (err) {
|
||||
console.log(`Error connecting to ${secrets.db}. ${err}`)
|
||||
} else {
|
||||
console.log(`Successfully connected to ${secrets.db}.`)
|
||||
}
|
||||
})
|
||||
}
|
||||
connect()
|
||||
|
||||
mongoose.connection.on("error", console.error)
|
||||
mongoose.connection.on("disconnected", connect)
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development"
|
||||
|
||||
// if in development mode set up the middleware required for hot reloading and rebundling
|
||||
if(isDev) {
|
||||
|
||||
const compiler = webpack(config)
|
||||
|
||||
app.use(require("webpack-dev-middleware")(compiler, {
|
||||
noInfo: true,
|
||||
publicPath: config.output.publicPath
|
||||
}))
|
||||
|
||||
app.use(require("webpack-hot-middleware")(compiler))
|
||||
}
|
||||
/*
|
||||
async function getRole(req) {
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
if (req.params.shortName && req.user) {
|
||||
var short_name = req.params.shortName.toLowerCase();
|
||||
var userId = req.user._id;
|
||||
Permission.findOne({'userId': userId, 'shortName': short_name}, function(err, permission) {
|
||||
if (err) {
|
||||
console.error('Error - getRole userId: ' + userId + ' shortName: ' + short_name + ' error: ' + err);
|
||||
reject(-1);
|
||||
}
|
||||
console.log('Found - getRole userId: ' + userId + ' shortName: ' + short_name + ' role: ' + permission.role);
|
||||
|
||||
resolve(permission.role);
|
||||
});
|
||||
} else {
|
||||
reject(-1);
|
||||
}});
|
||||
return promise;
|
||||
}*/
|
||||
|
||||
|
||||
|
||||
function isOwner(req, res, next) {
|
||||
if (req.params.shortName && req.user) {
|
||||
var short_name = req.params.shortName.toLowerCase();
|
||||
var userId = req.user._id;
|
||||
Permission.findOne({'userId': userId, 'shortName': short_name}, function(err, permission) {
|
||||
if (err) {
|
||||
console.error('Error - getRole userId: ' + userId + ' shortName: ' + short_name + ' error: ' + err);
|
||||
}
|
||||
console.log('Found - getRole userId: ' + userId + ' shortName: ' + short_name + ' role: ' + permission.role);
|
||||
if ( permission.role >= 15) {
|
||||
return next();
|
||||
} else {
|
||||
res.status(401).send({
|
||||
success: false,
|
||||
message: "Insufficent Permission, role: " + permission.role
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function isAdmin(req, res, next) {
|
||||
if (req.params.shortName && req.user) {
|
||||
var short_name = req.params.shortName.toLowerCase();
|
||||
var userId = req.user._id;
|
||||
Permission.findOne({'userId': userId, 'shortName': short_name}, function(err, permission) {
|
||||
if (err) {
|
||||
console.error('Error - getRole userId: ' + userId + ' shortName: ' + short_name + ' error: ' + err);
|
||||
}
|
||||
console.log('Found - getRole userId: ' + userId + ' shortName: ' + short_name + ' role: ' + permission.role);
|
||||
if (permission.role >= 10) {
|
||||
return next();
|
||||
} else {
|
||||
res.status(401).send({
|
||||
success: false,
|
||||
message: "Insufficent Permission, role: " + permission.role
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isLoggedIn(req, res, next) {
|
||||
if (req.isAuthenticated()) return next();
|
||||
res.status(401).send({
|
||||
success: false,
|
||||
message: "Not Authenticated."
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
var admin_server = process.env['ADMIN_SERVER'] != null ? process.env['ADMIN_SERVER'] : 'https://admin.openmhz.com';
|
||||
var beta_frontend_server = process.env['BETA_FRONTEND_SERVER'] != null ? process.env['BETA_FRONTEND_SERVER'] : 'https://beta.openmhz.com';
|
||||
var backend_server = process.env['BACKEND_SERVER'] != null ? process.env['BACKEND_SERVER'] : 'https://api.openmhz.com';
|
||||
var frontend_server = process.env['FRONTEND_SERVER'] != null ? process.env['FRONTEND_SERVER'] : 'https://openmhz.com';
|
||||
var media_server = process.env['MEDIA_SERVER'] != null ? process.env['MEDIA_SERVER'] : 'https://media.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
var socket_server = process.env['SOCKET_SERVER'] != null ? process.env['SOCKET_SERVER'] : 'wss://api.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
var account_server = process.env['ACCOUNT_SERVER'] != null ? process.env['ACCOUNT_SERVER'] : 'https://account.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
var site_name = process.env['SITE_NAME'] != null ? process.env['SITE_NAME'] : "OpenMHz";
|
||||
const proPlanValue = process.env['PRO_PLAN'] != null ? process.env['PRO_PLAN'] : 10;
|
||||
const freePlanValue = process.env['FREE_PLAN'] != null ? process.env['FREE_PLAN'] : 0;
|
||||
// -------------------------------------------
|
||||
|
||||
configurePassport(app, passport)
|
||||
configureExpress(app, passport)
|
||||
|
||||
// -------------------------------------------
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
app.get("")
|
||||
|
||||
//app.get("/permissions/:shortname", isOwner, permissions.fetchPermissions)
|
||||
/*app.get("/permissions/:shortName", permissions.fetchPermissions);
|
||||
app.delete("/permissions/:shortName/:permissionId", isAdmin, permissions.deletePermission);
|
||||
app.post("/permissions/:shortName/:permissionId", isAdmin, permissions.updatePermission);
|
||||
app.post("/permissions/:shortName", isAdmin, permissions.addPermission);*/
|
||||
app.get("/talkgroups/:shortName", isLoggedIn, talkgroups.fetchTalkgroups)
|
||||
app.post("/talkgroups/:shortName/import", isLoggedIn, upload.single('file'), talkgroups.importTalkgroups)
|
||||
app.get("/talkgroups/:shortName/export", isLoggedIn, talkgroups.exportTalkgroups)
|
||||
app.post("/groups/:shortName/reorder", isLoggedIn, groups.reorderGroups);
|
||||
app.post("/groups/:shortName/:groupId?", isLoggedIn, groups.upsertGroup);
|
||||
app.get("/groups/:shortName/:groupId?", isLoggedIn, groups.getGroups);
|
||||
app.delete("/groups/:shortName/:groupId", isLoggedIn, groups.deleteGroup);
|
||||
app.get("/systems", isLoggedIn, systems.listSystems)
|
||||
app.delete("/systems/:shortName", isLoggedIn, systems.deleteSystem)
|
||||
app.post("/systems/:shortName", [isLoggedIn, systems.ownSystem, systems.validateSystem, systems.updateSystem])
|
||||
app.post("/systems", [isLoggedIn, systems.uniqueShortName, systems.validateSystem, systems.createSystem])
|
||||
|
||||
app.get("*", (req, res, next) => {
|
||||
|
||||
// if we are in production mode then an extension will be provided, usually ".min"
|
||||
const minified = process.env.MIN_EXT || ""
|
||||
|
||||
// this is the HTML we will send to the client when they request any page. React and React Router
|
||||
// will take over once the scripts are loaded client-side
|
||||
const appHTML =
|
||||
`<!doctype html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||||
<title>${site_name} Admin</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="/style/style.css"></link>
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/semantic-ui@2.4.1/dist/semantic.min.css" />
|
||||
<script>
|
||||
const admin_server = "${admin_server}";
|
||||
const backend_server = "${backend_server}";
|
||||
const media_server = "${media_server}";
|
||||
const socket_server = "${socket_server}";
|
||||
const account_server = "${account_server}";
|
||||
const proPlanValue = ${proPlanValue};
|
||||
const freePlanValue = ${freePlanValue};
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="/assets/app${minified}.js"></script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
res.status(200).end(appHTML)
|
||||
|
||||
})
|
||||
|
||||
// start listening to incoming requests
|
||||
app.listen(app.get("port"), app.get("host"), (err) => {
|
||||
if (err) {
|
||||
console.err(err.stack)
|
||||
} else {
|
||||
console.log(`App listening on port ${app.get("port")} [${process.env.NODE_ENV} mode]`)
|
||||
}
|
||||
})
|
||||
14
admin/src/server/models/group.js
Normal file
14
admin/src/server/models/group.js
Normal file
@@ -0,0 +1,14 @@
|
||||
var mongoose = require('mongoose');
|
||||
|
||||
|
||||
|
||||
var groupSchema = mongoose.Schema({
|
||||
shortName: String,
|
||||
groupName: String,
|
||||
groupId: String,
|
||||
position: Number,
|
||||
talkgroups: [Number]
|
||||
});
|
||||
|
||||
|
||||
module.exports = mongoose.model('Group', groupSchema);
|
||||
24
admin/src/server/models/permission.js
Normal file
24
admin/src/server/models/permission.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// Defining a User Model in mongoose
|
||||
// Code modified from https://github.com/sahat/hackathon-starter
|
||||
|
||||
var mongoose = require("mongoose");
|
||||
|
||||
|
||||
|
||||
const PermissionSchema = new mongoose.Schema({
|
||||
userId: mongoose.Schema.Types.ObjectId,
|
||||
systemId: mongoose.Schema.Types.ObjectId,
|
||||
shortName: String,
|
||||
role: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
ver: {
|
||||
type: Number,
|
||||
default: 1.2
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
module.exports=mongoose.model("Permission", PermissionSchema)
|
||||
27
admin/src/server/models/system.js
Normal file
27
admin/src/server/models/system.js
Normal file
@@ -0,0 +1,27 @@
|
||||
var mongoose = require('mongoose');
|
||||
var bcrypt = require('bcrypt-nodejs');
|
||||
|
||||
var systemSchema = mongoose.Schema({
|
||||
name: String,
|
||||
shortName: String,
|
||||
systemType: String,
|
||||
county: String,
|
||||
country: String,
|
||||
city: String,
|
||||
state: String,
|
||||
description: String,
|
||||
showScreenName: Boolean,
|
||||
planType: {type: Number, default: 0},
|
||||
userId: mongoose.Schema.Types.ObjectId,
|
||||
key: String
|
||||
});
|
||||
|
||||
systemSchema.methods.generateHash = function(password) {
|
||||
return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null);
|
||||
};
|
||||
|
||||
systemSchema.methods.validPassword = function(password) {
|
||||
return bcrypt.compareSync(password, this.local.password);
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('System', systemSchema);
|
||||
12
admin/src/server/models/system_stat.js
Normal file
12
admin/src/server/models/system_stat.js
Normal file
@@ -0,0 +1,12 @@
|
||||
var mongoose = require('mongoose');
|
||||
|
||||
var systemStatSchema = mongoose.Schema({
|
||||
callTotals: [Number],
|
||||
errorTotals: [Number],
|
||||
usageBytes: Number,
|
||||
usageMb: Number,
|
||||
shortName: String
|
||||
}, { collection: 'system_stats' });
|
||||
|
||||
|
||||
module.exports = mongoose.model('SystemStat', systemStatSchema);
|
||||
17
admin/src/server/models/talkgroup.js
Normal file
17
admin/src/server/models/talkgroup.js
Normal file
@@ -0,0 +1,17 @@
|
||||
var mongoose = require('mongoose');
|
||||
var bcrypt = require('bcrypt-nodejs');
|
||||
|
||||
var talkgroupSchema = mongoose.Schema({
|
||||
shortName: String,
|
||||
userId: String,
|
||||
num: Number,
|
||||
mode: String,
|
||||
alpha: String,
|
||||
description: String,
|
||||
tag: String,
|
||||
group: String,
|
||||
priority: Number
|
||||
});
|
||||
|
||||
|
||||
module.exports = mongoose.model('Talkgroup', talkgroupSchema);
|
||||
92
admin/src/server/models/user.js
Normal file
92
admin/src/server/models/user.js
Normal file
@@ -0,0 +1,92 @@
|
||||
// Defining a User Model in mongoose
|
||||
// Code modified from https://github.com/sahat/hackathon-starter
|
||||
import bcrypt from "bcrypt-nodejs"
|
||||
import mongoose from "mongoose"
|
||||
import crypto from "crypto"
|
||||
|
||||
|
||||
|
||||
const UserSchema = new mongoose.Schema({
|
||||
local: {
|
||||
name: String,
|
||||
email: String,
|
||||
password: String
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
unique: true,
|
||||
lowercase: true
|
||||
},
|
||||
password: String,
|
||||
screenName: {
|
||||
type: String,
|
||||
unique: true,
|
||||
lowercase: true
|
||||
},
|
||||
firstName: String,
|
||||
lastName: String,
|
||||
location: String,
|
||||
email: String,
|
||||
resetPasswordToken: String,
|
||||
resetPasswordTTL: Date,
|
||||
confirmEmail: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
customerId: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
confirmEmailToken: String,
|
||||
confirmEmailTTL: Date,
|
||||
admin: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
terms: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
ver: {
|
||||
type: Number,
|
||||
default: 1.1
|
||||
},
|
||||
sysCount: Number,
|
||||
lastLogin: { type : Date, default: Date.now }
|
||||
})
|
||||
|
||||
/**
|
||||
* Password hash middleware.
|
||||
*/
|
||||
UserSchema.pre("save", function(next) {
|
||||
var user = this
|
||||
if (!user.isModified("password")) return next()
|
||||
bcrypt.genSalt(8, (err, salt) => {
|
||||
if (err) return next(err)
|
||||
bcrypt.hash(user.password, salt, null, (err, hash) => {
|
||||
if (err) return next(err)
|
||||
user.password = hash;
|
||||
user.local.password = hash;
|
||||
next()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
Defining our own custom document instance method
|
||||
*/
|
||||
UserSchema.methods = {
|
||||
comparePassword: function(candidatePassword, cb) {
|
||||
bcrypt.compare(candidatePassword, this.local.password, (err, isMatch) => {
|
||||
if (err) return cb(err)
|
||||
cb(null, isMatch)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Statics
|
||||
*/
|
||||
UserSchema.statics = {}
|
||||
|
||||
export default mongoose.model("User", UserSchema)
|
||||
5
admin/src/server/public/style/style.css
Normal file
5
admin/src/server/public/style/style.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.field {
|
||||
padding-top: 10px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
Reference in New Issue
Block a user