mirror of
https://github.com/openmhz/trunk-server.git
synced 2026-11-09 15:27:30 -05:00
539 lines
16 KiB
JavaScript
539 lines
16 KiB
JavaScript
import React, { useEffect, useLayoutEffect, useState } from "react";
|
|
import { Link, useParams, useNavigate } from 'react-router-dom';
|
|
import ListTalkgroups from "../Talkgroups/ListTalkgroups.js";
|
|
import MessageBox from "../Components/Message.js";
|
|
import ListGroups from "../Group/ListGroups.js";
|
|
import GroupModal from "../Group/GroupModal.js";
|
|
import ErrorChart from "./ResponsiveErrorChart"
|
|
import CallChart from "./ResponsiveCallChart"
|
|
import { useGetSystemsQuery, useGetTalkgroupsQuery, useGetGroupsQuery, useGetErrorsQuery, useDeleteGroupMutation, useDeleteSystemMutation, useImportTalkgroupsMutation, useSaveGroupOrderMutation } from '../features/api/apiSlice'
|
|
import {
|
|
Button,
|
|
Confirm,
|
|
Icon,
|
|
Form,
|
|
TextArea,
|
|
Message,
|
|
Header,
|
|
Container,
|
|
List,
|
|
Segment,
|
|
Tab,
|
|
Divider
|
|
} from "semantic-ui-react";
|
|
|
|
const System = () => {
|
|
const { shortName } = useParams();
|
|
const navigate = useNavigate();
|
|
const { data: systemsData, isSuccess: isSystemsSuccess } = useGetSystemsQuery();
|
|
const { data: talkgroupsData, isSuccess: isTalkgroupsSuccess } = useGetTalkgroupsQuery(shortName);
|
|
const { data: groupsData } = useGetGroupsQuery(shortName);
|
|
//const { data: systemErrorData, isSuccess: isErrorsSuccess } = useGetErrorsQuery(shortName);
|
|
const [deleteGroupAPI] = useDeleteGroupMutation();
|
|
const [reorderGroupsAPI] = useSaveGroupOrderMutation();
|
|
const [importTalkgroupsAPI, { error: importError }] = useImportTalkgroupsMutation();
|
|
const [deleteSystemAPI ] = useDeleteSystemMutation();
|
|
const [openSystemDeleteConfirm, setOpenSystemDeleteConfirm] = useState(false);
|
|
const [openMessage, setOpenMessage] = useState(false);
|
|
const [requestMessage, setRequestMessage] = useState("");
|
|
const [groupVisible, setGroupVisible] = useState(false);
|
|
const [editGroupId, setEditGroupId] = useState(false);
|
|
const [errorData, setErrorData] = useState({});
|
|
const [callStatistics, setCallStatistics] = useState({});
|
|
const [groupOrder, setGroupOrder] = useState([]);
|
|
|
|
|
|
const processErrors = (statistics) => {
|
|
const errors = statistics.decodeErrorsFreq;
|
|
var allData = [];
|
|
|
|
var num = 0;
|
|
const now = new Date();
|
|
var maxDate = now;
|
|
var minDate = now;
|
|
var minValue = 0;
|
|
var maxValue = 0;
|
|
var legend = [];
|
|
var MS_PER_MINUTE = 60000;
|
|
for (const freq in errors) {
|
|
const freqErrors = errors[freq];
|
|
var data = [];
|
|
for (var i = 0; i < freqErrors.errorHistory.length; i++) {
|
|
let spotsBack = i; //freqErrors.errorHistory.length - i;
|
|
let time = new Date(now - spotsBack * 15 * MS_PER_MINUTE);
|
|
if (time < minDate) minDate = time;
|
|
|
|
if (freqErrors.errorHistory[i] > maxValue)
|
|
maxValue = freqErrors.errorHistory[i];
|
|
var value = {
|
|
id: num++,
|
|
y: freqErrors.errorHistory[i],
|
|
x: time
|
|
};
|
|
data.push(value);
|
|
}
|
|
const id = Math.floor(freq / 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
|
|
};
|
|
|
|
setErrorData(errorData);
|
|
};
|
|
|
|
const processStatistics = (statistic) => {
|
|
var callTotals = [];
|
|
var uploadErrors= [];
|
|
let talkgroupStats = [];
|
|
let decodeErrorsFreq = [];
|
|
|
|
const now = new Date();
|
|
var maxDate = now;
|
|
var minDate = now;
|
|
var minValue = 0;
|
|
var maxValue = 0;
|
|
var MS_PER_MINUTE = 60000;
|
|
if (statistic) {
|
|
for (let j = 0; j < statistic.callTotals.length; j++) {
|
|
let spotsBack = j;//statistic.callTotals.length - j;
|
|
let 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 (let j = 0; j < statistic.uploadErrors.length; j++) {
|
|
let spotsBack = statistic.uploadErrors.length - j;
|
|
let time = new Date(now - spotsBack * 15 * MS_PER_MINUTE);
|
|
if (time < minDate) minDate = time;
|
|
if (statistic.uploadErrors[j] > maxValue)
|
|
maxValue = statistic.uploadErrors[j];
|
|
uploadErrors.push({ x: time, y: statistic.uploadErrors[j] });
|
|
}
|
|
const data = {
|
|
minDate: minDate,
|
|
maxDate: maxDate,
|
|
minValue: minValue,
|
|
maxValue: maxValue,
|
|
callTotals: callTotals,
|
|
uploadErrors: uploadErrors
|
|
};
|
|
|
|
setCallStatistics(data);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
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);
|
|
if (this.props.statistic) {
|
|
this.processStatistics(this.props.statistic);
|
|
}
|
|
if (this.props.errors) {
|
|
this.processErrors(this.props.errors);
|
|
}
|
|
}
|
|
|
|
componentDidUpdate(prevProp) {
|
|
|
|
// If we just received the error statistics for a system, for the first time, proccess them.
|
|
if (this.props.errors && !prevProp.errors) {
|
|
this.processErrors(this.props.errors);
|
|
}
|
|
// If we just received the call statistics for a system, for the first time, proccess them.
|
|
if (this.props.statistic && !prevProp.statistic) {
|
|
this.processStatistics(this.props.statistic);
|
|
}
|
|
}
|
|
*/
|
|
|
|
const editGroup = (groupId) => {
|
|
setEditGroupId(groupId)
|
|
setGroupVisible((state) => !state)
|
|
}
|
|
|
|
const deleteGroup = async (groupId) => {
|
|
for (const num in groupsData) {
|
|
if (groupsData[num]._id === groupId) {
|
|
await deleteGroupAPI(groupsData[num])
|
|
}
|
|
}
|
|
}
|
|
|
|
const reorderGroup = (oldIndex, newIndex) => {
|
|
if ((newIndex < 0) || (newIndex > (groupOrder.length - 1))) {
|
|
return
|
|
}
|
|
const moveOrder = groupOrder.slice();
|
|
moveOrder.splice(newIndex, 0, moveOrder.splice(oldIndex, 1)[0]);
|
|
setGroupOrder(moveOrder);
|
|
}
|
|
|
|
const handleGroupClose = () => {
|
|
setEditGroupId(false)
|
|
setGroupVisible(false)
|
|
}
|
|
|
|
const handleGroupToggle = () => {
|
|
setEditGroupId(false)
|
|
setGroupVisible((state) => !state)
|
|
}
|
|
|
|
const saveGroupOrder = async () => {
|
|
await reorderGroupsAPI({ shortName: shortName, order: { groupOrder: JSON.stringify(groupOrder) } });
|
|
}
|
|
|
|
const handleUpload = async (file) => {
|
|
const formData = new FormData();
|
|
formData.append('file', file)
|
|
await importTalkgroupsAPI({ shortName: shortName, file: formData });
|
|
}
|
|
|
|
|
|
|
|
const handleSystemDeleteConfirm = async () => {
|
|
setOpenSystemDeleteConfirm(false);
|
|
try {
|
|
await deleteSystemAPI(shortName).unwrap();
|
|
navigate("/list-systems")
|
|
} catch (error) {
|
|
const message = error.data.message;
|
|
console.log(message);
|
|
setRequestMessage(message);
|
|
}
|
|
|
|
};
|
|
const handleSystemDeleteCancel = () => setOpenSystemDeleteConfirm(false);
|
|
|
|
|
|
let system = false;
|
|
if (isSystemsSuccess) {
|
|
system = systemsData.systems.find(sys => sys.shortName === shortName);
|
|
}
|
|
|
|
useLayoutEffect(() => {
|
|
if (groupsData) {
|
|
let newOrder = [];
|
|
groupsData.forEach(group => {
|
|
|
|
newOrder.push(group._id);
|
|
});
|
|
setGroupOrder(newOrder);
|
|
}
|
|
}, [groupsData])
|
|
useEffect(() => {
|
|
if (isSystemsSuccess && systemsData.stats[shortName]) {
|
|
processStatistics(systemsData.stats[shortName])
|
|
processErrors(systemsData.stats[shortName])
|
|
}
|
|
}, [isSystemsSuccess, systemsData]);
|
|
/*
|
|
useEffect(() => {
|
|
if (isErrorsSuccess) {
|
|
processErrors(systemErrorData)
|
|
}
|
|
}, [systemErrorData]);*/
|
|
|
|
let fileInput = null;
|
|
var location = "";
|
|
var example = "";
|
|
var groupRender = () => { };
|
|
|
|
if (system) {
|
|
if (isTalkgroupsSuccess && talkgroupsData.length) {
|
|
groupRender = () => (
|
|
<div>
|
|
<Button onClick={handleGroupToggle}>Create Group</Button>
|
|
<Button onClick={saveGroupOrder} > Save Order</Button>
|
|
<ListGroups
|
|
groups={groupsData}
|
|
order={groupOrder}
|
|
reorderGroup={reorderGroup}
|
|
deleteGroup={deleteGroup}
|
|
editGroup={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>
|
|
);
|
|
}
|
|
|
|
|
|
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": "' +
|
|
process.env.REACT_APP_BACKEND_SERVER +
|
|
'"\n}';
|
|
|
|
switch (system.systemType) {
|
|
case "state":
|
|
location = system.state;
|
|
break;
|
|
default:
|
|
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>
|
|
{system.shortName}
|
|
</Segment>
|
|
<Segment>
|
|
<Header size="small">Description</Header>
|
|
{system.description}
|
|
</Segment>
|
|
{system.status &&
|
|
<Segment>
|
|
<Header size="small">Status Message</Header>
|
|
{system.status}
|
|
</Segment>
|
|
}
|
|
<Segment clearing={true}>
|
|
<Button
|
|
color="red"
|
|
onClick={() => setOpenSystemDeleteConfirm(true)}
|
|
disabled={!isSystemsSuccess}
|
|
content="Delete"
|
|
floated="right"
|
|
/>
|
|
<Button
|
|
as={Link}
|
|
to={"/update-system/" + system.shortName}
|
|
content="Update"
|
|
disabled={!isSystemsSuccess}
|
|
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>
|
|
{system.key}
|
|
</Segment>
|
|
<Segment>
|
|
<Header size="small">Short Name</Header>
|
|
{system.shortName}
|
|
</Segment>
|
|
<Segment>
|
|
<Header size="small">Upload Server</Header>
|
|
{process.env.REACT_APP_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>
|
|
The Talkgroup file is a .CSV file that has infromation about the different talkgroups on the system. Headers are required for the columns, and should be the first
|
|
line of the file. The columns can be in any order, but the headers must match exactly.<br/>
|
|
An example header line is: <br />
|
|
<code>Decimal,Alpha Tag,Description</code><br/>
|
|
<p>There can be additional columns so you can use the same Talkgroup CSV file from Trunk Recorder.</p>
|
|
</em>
|
|
</p>
|
|
<p>The columns for the file are:</p>
|
|
<List ordered celled>
|
|
<List.Item>
|
|
<List.Content>
|
|
<List.Header>Decimal</List.Header>
|
|
<List.Description>The talkgroup number in decimal</List.Description>
|
|
</List.Content>
|
|
</List.Item>
|
|
<List.Item>
|
|
<List.Content>
|
|
<List.Header>Alpha Tag</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>
|
|
</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={() => {
|
|
handleUpload(fileInput.files[0]);
|
|
}}
|
|
ref={input => {
|
|
fileInput = input;
|
|
}}
|
|
/>
|
|
</span>
|
|
<a href={"/talkgroups/" + system.shortName + "/export"}>
|
|
<Button>
|
|
<i className="upload icon" />
|
|
Export Talkgroups
|
|
</Button>
|
|
</a>
|
|
{importError ? (
|
|
<div>
|
|
<Message color='red'>{importError.data}</Message>
|
|
</div>
|
|
) : (<div></div>)}
|
|
|
|
<ListTalkgroups talkgroups={talkgroupsData} />
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
menuItem: "Groups",
|
|
render: groupRender
|
|
}, {
|
|
menuItem: "Stats",
|
|
render: () => (
|
|
<div>
|
|
<Divider horizontal>
|
|
<Icon name="microphone" />
|
|
24-Hour Call History
|
|
</Divider>
|
|
<CallChart data={callStatistics} />
|
|
<Divider horizontal>
|
|
<Icon name="microphone" />
|
|
Hourly Vocoder Error Percentage
|
|
</Divider>
|
|
<ErrorChart data={errorData} />
|
|
</div>
|
|
)
|
|
}
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
|
|
|
|
<GroupModal
|
|
shortName={shortName}
|
|
editGroupId={editGroupId}
|
|
open={groupVisible}
|
|
onClose={handleGroupClose}
|
|
/>
|
|
|
|
{system &&
|
|
<Container>
|
|
<MessageBox
|
|
open={openMessage}
|
|
message={requestMessage}
|
|
title="Delete Failed"
|
|
onClose={() => setOpenMessage(false)}
|
|
/>
|
|
<Confirm
|
|
open={openSystemDeleteConfirm}
|
|
header="Are you sure you want to?"
|
|
content={"Delete system - " + system.shortName}
|
|
onCancel={handleSystemDeleteCancel}
|
|
onConfirm={handleSystemDeleteConfirm}
|
|
/>
|
|
<Header as="h1">
|
|
<Header.Content>
|
|
{system.name}
|
|
</Header.Content>
|
|
</Header>
|
|
|
|
<Tab
|
|
menu={{
|
|
secondary: true,
|
|
pointing: true
|
|
}}
|
|
panes={panes}
|
|
//defaultActiveIndex={activeTab}
|
|
/>
|
|
</Container>
|
|
}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|
|
export default System;
|