Merge pull request #144 from openmhz/features/clean-up

Features/clean up
This commit is contained in:
Luke Berndt
2023-06-07 22:11:06 -04:00
committed by GitHub
30 changed files with 42485 additions and 2691 deletions

20130
admin/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,66 @@ exports.isLoggedIn = function (req, res, next) {
// -------------------------------------------
exports.listAllSystems = async function (req, res, next) {
if (!req.user.admin) {
res.status(401);
res.json({ success: false, message: "Not Authorized" });
return;
}
const systems = await System.find().exec();
if (systems == null) {
res.status(400);
res.json({ success: false, message: err });
return;
}
let returnSys = systems.map(obj => {
var rObj = (({
name,
shortName,
description,
systemType,
city,
state,
county,
country,
userId,
key,
showScreenName,
ignoreUnknownTalkgroup,
lastActive,
active
}) => ({
name,
shortName,
description,
systemType,
city,
state,
county,
country,
userId,
key,
showScreenName,
ignoreUnknownTalkgroup,
lastActive,
active
}))(obj);
rObj.id = obj._id;
return rObj;
});
res.json({
success: true,
systems: returnSys
});
return;
};
exports.listSystems = async function (req, res, next) {
const userId = new mongoose.Types.ObjectId(req.user._id);
const systems = await System.find({ userId: userId }).exec();

View File

@@ -79,7 +79,7 @@ function isOwner(req, res, next) {
}
}
/*
function isAdmin(req, res, next) {
if (req.params.shortName && req.user) {
var short_name = req.params.shortName.toLowerCase();
@@ -99,7 +99,7 @@ function isAdmin(req, res, next) {
}
});
}
}
}*/
function isLoggedIn(req, res, next) {
if (req.isAuthenticated()) return next();
@@ -109,6 +109,16 @@ function isLoggedIn(req, res, next) {
});
};
function isAdmin(req, res, next) {
if (req.isAuthenticated() && req.user.admin) return next();
res.status(401).send({
success: false,
message: "Not Authenticated."
});
};
function uploadFile(req, res, next) {
const upload = multer({dest: 'uploads/'}).single('file');
@@ -142,6 +152,7 @@ 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.get("/admin/systems", isAdmin, systems.listAllSystems)
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])

View File

@@ -1,5 +1,6 @@
var mongoose = require('mongoose');
var bcrypt = require('bcrypt');
var User = require('./user');
var systemSchema = mongoose.Schema({
name: String,
@@ -12,7 +13,9 @@ var systemSchema = mongoose.Schema({
description: String,
showScreenName: Boolean,
ignoreUnknownTalkgroup : Boolean,
userId: mongoose.Schema.Types.ObjectId,
active: {type: Boolean, default: false},
lastActive: Date,
userId: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
key: String
});

View File

@@ -0,0 +1,58 @@
import {
Container,
Divider,
Header,
Table
} from "semantic-ui-react";
import { useGetAllSystemsQuery, } from '../features/api/apiSlice'
// ----------------------------------------------------
const ListSystems = () => {
const { data, isSuccess } = useGetAllSystemsQuery();
let systems = false;
if (isSuccess) {
systems = data.systems;
}
//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
return (
<div>
<Container>
<Header as="h1">Radio Systems</Header>
{systems &&
systems.map((system) =>
<Table celled selectable>
<Table.Header>
<Table.Row>
<Table.HeaderCell>ShortName</Table.HeaderCell>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Last Active</Table.HeaderCell>
<Table.HeaderCell>Owner</Table.HeaderCell>
<Table.HeaderCell>Email</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>{system.shortName}</Table.Cell>
<Table.Cell>{system.name}</Table.Cell>
<Table.Cell>{system.lastActive}</Table.Cell>
<Table.Cell>{system.userId.firstName} {system.userId.lastName}</Table.Cell>
<Table.Cell>{system.userId.email}</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
)}
</Container>
</div>
);
}
export default ListSystems;

View File

@@ -1,7 +1,7 @@
import Navigation from "./Navigation"
import { Routes, Route } from 'react-router-dom';
import Restricted from "./Restricted"
import {AdminRestricted, Restricted} from "./Restricted"
import About from "../Components/About"
import System from "../System/System"
@@ -20,6 +20,7 @@ const App = (params) => {
<Route path="/update-system/:shortName" element={ <Restricted><UpdateSystem/></Restricted>} />
<Route path="/create-system" element={ <Restricted><CreateSystem/></Restricted>} />
<Route path="/list-systems" element={ <Restricted><ListSystems/></Restricted>} />
<Route path="/all-systems" element={ <AdminRestricted><ListSystems/></AdminRestricted>} />
<Route path="/" element={ <Restricted><ListSystems/></Restricted>} />
<Route render={() => (<div>Miss</div>)} />
</Routes>

View File

@@ -9,7 +9,7 @@ const navStyle = {
const Navigation = (props) => {
const dispatch = useDispatch();
const { email } = useSelector((state) => state.user);
const { email, admin } = useSelector((state) => state.user);
const logout = event => {
event.preventDefault();
@@ -24,6 +24,11 @@ const Navigation = (props) => {
{process.env.REACT_APP_SITE_NAME}
</Menu.Item>
<Menu.Menu position="right">
{admin &&
<Link to="/all-systems">
<Menu.Item name="all-systems">All Systems</Menu.Item>
</Link>
}
<Link to="/list-systems">
<Menu.Item name="systems"> Systems</Menu.Item>
</Link>

View File

@@ -7,7 +7,7 @@ import { authenticateUser } from "../features/user/userSlice";
* Higher-order component (HOC) to wrap restricted pages
*/
const Restricted = ({children }) => {
export const Restricted = ({children }) => {
const { authenticated, hasAuthenticated } = useSelector((state) => state.user);
const dispatch = useDispatch();
useEffect(() => {
@@ -26,4 +26,23 @@ const Restricted = ({children }) => {
};
export const AdminRestricted = ({children }) => {
const { authenticated, hasAuthenticated, admin } = useSelector((state) => state.user);
const dispatch = useDispatch();
useEffect(() => {
dispatch(authenticateUser({}));
},[]);
if (hasAuthenticated) {
if (authenticated && admin) {
return children;;
}
else {
window.location = process.env.REACT_APP_ACCOUNT_SERVER + "/login?nextLocation=admin";
}
}
return <div/>
};
export default Restricted

View File

@@ -23,8 +23,8 @@ const GroupModal = (props) => {
}
const [groupName, setGroupName] = useState("");
const [group, setGroup] = useState(emptyGroup)
const [talkgroups, setTalkgroups] = useState(props.talkgroups ? props.talkgroups : [])
const [group, setGroup] = useState(emptyGroup);
const [talkgroupList, setTalkgroupList] = useState([]);
const { data: talkgroupsData} = useGetTalkgroupsQuery(props.shortName);
const { data: groupsData} = useGetGroupsQuery(props.shortName);
@@ -56,7 +56,7 @@ const GroupModal = (props) => {
// we want to remove that are part of the Group from the list on the Left
const group = { name: editGroup.groupName, items: items }
setGroup(group);
setTalkgroups(talkgroups);
setTalkgroupList(talkgroups);
setGroupName(editGroup.groupName);
}
},[groupsData,talkgroupsData, props.editGroupId]);
@@ -64,20 +64,11 @@ const GroupModal = (props) => {
useEffect(() => {
if (!props.editGroupId && talkgroupsData) {
setTalkgroups([...talkgroupsData]);
setTalkgroupList([...talkgroupsData]);
}
},[talkgroupsData, props.editGroupId]);
useEffect(() => {
if (!props.editGroupId && props.talkgroups) {
setGroupName("");
setGroup(emptyGroup);
setTalkgroups( [...props.talkgroups]);
}
},[props.editGroupId, props.talkgroups]);
const removeTalkgroup = (index) => {
const removeTalkgroup = (event, index) => {
const item = {
num: group.items[index].num,
description: group.items[index].description
@@ -96,24 +87,29 @@ const GroupModal = (props) => {
.slice(0, index)
.concat(this.state.group.items.slice(index + 1))
});
setTalkgroups(talkgroups);
setTalkgroupList(talkgroups);
}
const addTalkgroup = (event, index) => {
// Build a talkgroup item for the group based on the Index of the item from the list
const item = {
num: talkgroupsData[index].num,
description: talkgroupsData[index].description
num: talkgroupList[index].num,
description: talkgroupList[index].description
};
// Add item to the Group
setGroup({ items: group.items.concat(item) })
setTalkgroups(talkgroups.slice(0, index).concat(talkgroups.slice(index + 1)));
// remove the item from the list
setTalkgroupList(talkgroupList.slice(0, index).concat(talkgroupList.slice(index + 1)));
}
const handleClose = () => {
setGroupName("");
setGroup(emptyGroup);
setTalkgroups([...talkgroupsData]);
setTalkgroupList([...talkgroupsData]);
props.onClose();
}
@@ -130,7 +126,7 @@ const GroupModal = (props) => {
setGroupName("");
setGroup(emptyGroup);
setTalkgroups([...talkgroupsData]);
setTalkgroupList([...talkgroupsData]);
await updateGroup(data);
props.onClose(true);
} else {
@@ -142,7 +138,7 @@ const GroupModal = (props) => {
setGroupName("");
setGroup(emptyGroup);
setTalkgroups([...talkgroupsData]);
setTalkgroupList([...talkgroupsData]);
await createGroup(data);
props.onClose();
@@ -175,8 +171,8 @@ const GroupModal = (props) => {
</Table.Row>
</Table.Header>
<Table.Body>
{talkgroups &&
talkgroups.map((talkgroup, i) => (
{talkgroupList &&
talkgroupList.map((talkgroup, i) => (
<Table.Row link="true" key={talkgroup.num + "-" + i}>
<Table.Cell>{talkgroup.num}</Table.Cell>

View File

@@ -136,6 +136,10 @@ export const apiSlice = createApi({
// The URL for the request is '/fakeApi/posts'
query: () => ({ url: `/systems`, credentials: "include" })
}),
getAllSystems: builder.query({
// The URL for the request is '/fakeApi/posts'
query: () => ({ url: `/admin/systems`, credentials: "include" })
}),
createSystem: builder.mutation({
query: (system) => ({
url: `/systems`,
@@ -198,4 +202,4 @@ export const apiSlice = createApi({
})
// Export the auto-generated hook for the `getPosts` query endpoint
export const { useGetGroupsQuery, useGetSystemsQuery, useGetTalkgroupsQuery, useGetErrorsQuery, useDeleteGroupMutation, useCreateGroupMutation, useCreateSystemMutation, useUpdateSystemMutation, useDeleteSystemMutation, useSaveGroupOrderMutation, useUpdateGroupMutation, useImportTalkgroupsMutation } = apiSlice
export const { useGetGroupsQuery, useGetSystemsQuery, useGetAllSystemsQuery, useGetTalkgroupsQuery, useGetErrorsQuery, useDeleteGroupMutation, useCreateGroupMutation, useCreateSystemMutation, useUpdateSystemMutation, useDeleteSystemMutation, useSaveGroupOrderMutation, useUpdateGroupMutation, useImportTalkgroupsMutation } = apiSlice

File diff suppressed because it is too large Load Diff

View File

@@ -292,7 +292,7 @@ exports.get_call = async function (req, res) {
);
} else {
console.warn("[" + req.params.shortName + "] Error - /:shortName/call/:id Could not find item " + err + " ID: " + objectId);
console.warn("[" + req.params.shortName + "] Error - /:shortName/call/:id Could not find item ID: " + objectId);
res.status(404);
res.send(JSON.stringify({
success: false,

View File

@@ -13,6 +13,7 @@ exports.get_stats = function(req, res) {
var talkgroupStats = sysStats.talkgroupStats(short_name);
var uploadErrors = sysStats.uploadErrors(short_name);
let decodeErrorsFreq = sysStats.decodeErrorsFreq(short_name);
if (callTotals && talkgroupStats) {
res.contentType('json');
res.send(JSON.stringify({
@@ -32,3 +33,4 @@ exports.get_stats = function(req, res) {
var statSched = schedule.scheduleJob('*/15 * * * *', function() {
sysStats.shiftStats();
});

View File

@@ -1,34 +1,49 @@
var MongoClient = require('mongodb').MongoClient;
const { CloudWatchLogs } = require("aws-sdk");
var { callModel: Call } = require("./models/call");
var Event = require("./models/event");
var Podcast = require("./models/podcast");
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 state = {
db: null,
exports.cleanOldEvents = async function() {
var date = new Date();
Event.bulkWrite([
{
deleteMany: {
filter: { expireTime: {$lt: date} }
}
}
]).then(res => {
// Prints "1 1 1"
console.log("Removed " + res.deletedCount + " Podcasts");
});
}
exports.connect = async function (done) {
if (state.db) return done()
var url = 'mongodb://' + host + ':' + port + '/scanner';
const client = new MongoClient(url);
await client.connect();
state.db = client.db();
done();
exports.cleanOldPodcasts = async function() {
var date = new Date();
Podcast.bulkWrite([
{
deleteMany: {
filter: { expireTime: {$lt: date} }
}
}
]).then(res => {
// Prints "1 1 1"
console.log("Removed " + res.deletedCount + " Events");
});
}
exports.get = function () {
return state.db
}
exports.close = function (done) {
if (state.db) {
state.db.close(function (err, result) {
state.db = null
state.mode = null
done(err)
})
}
exports.cleanOldCalls = async function() {
var date = new Date();
date.setMonth(date.getMonth() - 1);
Call.bulkWrite([
{
deleteMany: {
filter: { time: {$lt: date} }
}
}
]).then(res => {
// Prints "1 1 1"
console.log("Removed " + res.deletedCount + " Calls older than " + date);
});
}

View File

@@ -10,7 +10,8 @@ var groups = require("./controllers/groups");
var stats = require("./controllers/stats");
var events = require("./controllers/events");
var config = require('./config/config.json');
let db = require('./db')
var schedule = require('node-schedule');
var mongoose = require("mongoose");
const { ObjectId } = require('mongodb');
const Group = require("./models/group");
@@ -291,6 +292,20 @@ io.sockets.on('connection', function (client) {
});
stats.init_stats();
// This will run at 3am each day.
// IF you don't set the minute to 0, it will run every minute while it is still 3, so at 3:01, 3:02... etc
schedule.scheduleJob('0 3 * * *', function() {
db.cleanOldCalls();
});
schedule.scheduleJob('15 3 * * *', function() {
db.cleanOldEvents();
});
schedule.scheduleJob('30 3 * * *', function() {
db.cleanOldPodcasts();
});
server.listen(app.get("port"), (err) => {
if (err) {
@@ -300,16 +315,5 @@ server.listen(app.get("port"), (err) => {
}
})
/*
var calcRule = new schedule.RecurrenceRule();
calcRule.minute = 4;
var calcSched = schedule.scheduleJob(calcRule, function() {
console.log('Time to calulcate stats');
call_stats.build_call_volume();
console.log('Time to calulcate usage');
call_stats.build_usage();
});
*/
module.exports = app;

View File

@@ -17,7 +17,6 @@ var systemSchema = mongoose.Schema({
ignoreUnknownTalkgroup : Boolean,
active: {type: Boolean, default: false},
lastActive: Date,
planType: {type: Number, default: 0},
userId: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
key: String
});

View File

@@ -1,11 +1,16 @@
var mongoose = require('mongoose');
let decodeErrorSchema = mongoose.Schema({
totalLen: Number,
errors: Number,
spikes: Number,
errorHistory: [Number],
spikeHistory: [Number],
})
let talkgroupStatsSchema = mongoose.Schema({
calls: Number,
totalLen: Number,
callCountHistory: [Number],
callAvgLenHistory: [Number]
})

View File

@@ -1,463 +0,0 @@
var fs = require('fs');
var path = require('path');
var config = require('./config/config.json');
var mongo = require('mongodb');
var ObjectID = require('mongodb').ObjectID;
mongo.BSONPure = require('bson').BSONPure;
var BSON = mongo.BSONPure;
var MongoClient = require('mongodb').MongoClient,
assert = require('assert');
var db = require('./db');
var util = require("util"),
rl = require('readline'),
spawn = require('child_process').spawn;
function build_call_stats(results) {
var historic = new Array();
// go through all of the results
if (results && (results.length > 0)) {
for (var i = 0; i < results.length; i++) {
// if this is the first time we have seen this shortName in the results, create and array for it in the Historic array
if (historic[results[i]._id.shortName] == undefined) {
historic[results[i]._id.shortName] = new Array();
for (j = 0; j < 121; j++) {
historic[results[i]._id.shortName][j] = 0;
}
}
// add the number of calls for this system at this hour
historic[results[i]._id.shortName][parseInt(results[i]._id.hour)] = results[i].calls;
}
db.get().collection('systems', function(err, sysCollection) {
var cursor = sysCollection.find();
cursor.each(function(err, item) {
// go through all the systems
if (item) {
// if a system has had no calls, create a blank history array
if (historic[item.shortName] == undefined) {
historic[item.shortName] = new Array();
for (j = 0; j < 121; j++) {
historic[item.shortName][j] = 0;
}
}
} else {
// when item == null that means that we have gone through all the systems...
// time to save it.
for (var shortName in historic) {
if (historic.hasOwnProperty(shortName)) {
// do stuff
if (historic[shortName][0]>0) {
var active = true;
} else {
var active = false;
}
sysCollection.update({
shortName: shortName
}, {
$set: {
"active": active
}
}, {
upsert: true
}, function(err, objects) {
if (err) console.warn(err.message);
});
db.get().collection('system_stats', function(err, statsCollection) {
if (err) console.log(err);
//console.log();
statsCollection.update({
shortName: shortName
}, {
$set: {
"callHistory": historic[shortName]
}
}, {
upsert: true
}, function(err, objects) {
if (err) console.warn(err.message);
//console.log(util.inspect(objects));
});
});
}
}
}
});
});
//db.get().close();
}
}
function build_test_stat(results) {
var historic = new Array();
// go through all of the results
if (results && (results.length > 0)) {
for (var i = 0; i < results.length; i++) {
// if this is the first time we have seen this shortName in the results, create and array for it in the Historic array
if (historic[results[i]._id.shortName] == undefined){
historic[results[i]._id.shortName] = {};
}
if (historic[results[i]._id.shortName][results[i]._id.talkgroupNum] == undefined) {
historic[results[i]._id.shortName][results[i]._id.talkgroupNum] = new Array();
for (j = 0; j < 24; j++) {
historic[results[i]._id.shortName][results[i]._id.talkgroupNum][j] = 0;
}
}
// add the number of calls for this system at this hour
if (parseInt(results[i]._id.hour) < 24)
historic[results[i]._id.shortName][results[i]._id.talkgroupNum][parseInt(results[i]._id.hour)] = results[i].value.count;
}
}
console.log(util.inspect(results));
/*
var chan_count = 0;
stats = {};
var db_count = 0;
for (var chan_num in channels) {
var historic = new Array();
chan_count++;
for (hour = 0; hour < 24; hour++) {
historic[hour] = 0;
}
stats[chan_num] = {
name: channels[chan_num].alpha,
desc: channels[chan_num].desc,
num: chan_num,
historic: historic
};
var query = {
"_id.talkgroup": parseInt(chan_num)
};
collection.find(query).toArray(function (err, results) {
db_count++;
if (err) console.log(err);
if (results && (results.length > 0)) {
for (var i = 0; i < results.length; i++) {
stats[results[0]._id.talkgroup].historic[results[i]._id.hour] = results[i].value.count;
}
}
if (chan_count == db_count) {
for (var chan_num in stats) {
var chan = stats[chan_num];
var erase_me = true;
for (var i = 0; i < chan.historic.length; i++) {
if (chan.historic[i] != 0) {
erase_me = false;
break;
}
}
if (erase_me) {
delete stats[chan_num];
}
}
}
});
}*/
}
exports.test2 = function() {
map = function () {
var now = new Date();
var difference = now.getTime() - this.time.getTime();
var hour = Math.floor(difference / 1000 / 60 / 60);
emit({
hour: hour,
talkgroupNum: this.talkgroupNum,
shortName: this.shortName
}, {
count: 1
});
}
reduce = function (key, values) {
var count = 0;
values.forEach(function (v) {
count += v['count'];
});
return {
count: count
};
}
db.get().collection('calls', function (err, transCollection) {
var yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
transCollection.mapReduce(map, reduce, {
query: {
time: {
$gte: yesterday
}
},
out : { inline: 1 },
}, function (err, results) {
if (err) console.error(err);
build_test_stat(results);
//console.log(util.inspect(results));
});
});
}
exports.test = function() {
var collection = db.get().collection("calls");
var now = new Date();
var start = new Date();
start.setDate(start.getDate() - 50);
collection.aggregate([
{
$project: {
shortName: 1,
talkgroupNum: 1,
ago: {
$floor: {
$divide: [{
$subtract: [now, "$time"]
}, 1000 * 60]
}
},
"interval": {
"$subtract": [
{ "$minute": "$time" },
{ "$mod": [{ "$minute": "$time"}, 15] }
]
}
}
}, {
"$group": {
"_id": {
shortName: "$shortName",
talkgroupNum: "$talkgroupNum",
ago: "$ago",
"interval": "$interval"
},
count: {$sum: 1}
}
},
{$sort:{_id:-1}}
], function(err, results) {
if (err) console.error("ERror" + err);
console.log(util.inspect(results));
});
}
exports.build_tg_volume = function() {
var collection = db.get().collection("calls");
var now = new Date();
var start = new Date();
start.setDate(start.getDate() - 50);
collection.aggregate([{
$match: {
time: {
$gte: start
}
}
}, {
$project: {
shortName: 1,
hour: {
$floor: {
$divide: [{
$subtract: [now, "$time"]
}, 1000 * 60 * 60]
}
}
}
}, {
$group: {
_id: {
shortName: "$shortName",
hour: "$hour"
},
calls: {
$sum: 1
}
}
}], function(err, results) {
if (err) console.error("ERror" + err);
if (results) {
console.log("Building stats...");
console.log(util.inspect(results));
build_call_stats(results);
}
});
}
exports.build_call_volume = function() {
var collection = db.get().collection("calls");
var now = new Date();
var start = new Date();
start.setDate(start.getDate() - 5);
collection.aggregate([{
$match: {
time: {
$gte: start
}
}
}, {
$project: {
shortName: 1,
hour: {
$floor: {
$divide: [{
$subtract: [now, "$time"]
}, 1000 * 60 * 60]
}
}
}
}, {
$group: {
_id: {
shortName: "$shortName",
hour: "$hour"
},
calls: {
$sum: 1
}
}
}], function(err, results) {
if (err) console.error("ERror" + err);
if (results) {
console.log("Building stats...");
console.log(util.inspect(results));
build_call_stats(results);
}
});
}
/*
exports.build_call_volume = function() {
map = function() {
var now = new Date();
var difference = now.getTime() - this.time.getTime();
var hour = Math.floor(difference / 1000 / 60 / 60);
emit({
hour: hour,
shortName: this.shortName
}, {
count: 1
});
}
reduce = function(key, values) {
var count = 0;
values.forEach(function(v) {
count += v['count'];
});
printjson(count);
return {
count: count
};
}
db.get().collection('calls', function(err, callsCollection) {
var start = new Date();
start.setDate(start.getDate() - 5);
callsCollection.mapReduce(map, reduce, {
query: {
time: {
$gte: start
}
},
out: {
replace: "call_volume"
}
}, function(err, collection) {
if (err) console.error("ERror" + err);
if (collection) {
console.log("Building stats...");
build_call_stats(collection);
}
});
});
};
*/
exports.build_usage = function() {
var base_path = config.mediaDirectory;
var total = 0;
var sysCount = 0;
db.get().collection('systems', function(err, sysCollection) {
var cursor = sysCollection.find();
cursor.each(function(err, item) {
if (item) {
var local_path = config.mediaDirectory + "/" + item.shortName + "/";
console.log("System: " + item.shortName + " [ " + local_path + " ]");
var du = spawn('du', ['-s', local_path]);
//var ls = spawn('ls', ['-lh', '/usr']);
//linereader = rl.createInterface(du.stdout, du.stdin);
// Read line by line.
//du.stdout.on('data', function (data) {
du.stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
});
du.on('close', (code) => {
//console.log(`child process exited with code ${code}`);
});
/*
ls.stdout.on('data', (data) => {
console.log(data);
console.log(`stdout: ${data}`);
});
*/
du.stdout.on('data', function(data) {
var out = String(data);
var arr = out.split('\t');
var size = parseInt(arr[0]);
console.log(out + " Size: " + size);
db.get().collection('system_stats', function(err, statsCollection) {
statsCollection.update({
shortName: item.shortName
}, {
$set: {
"usageBytes": size,
"usageMb": (size / 1024)
}
}, {
upsert: true
}, function(err, objects) {
if (err) console.warn(err.message);
});
});
});
} else {
console.log("Processed: " + sysCount);
}
});
});
};

View File

@@ -1,164 +0,0 @@
var fs = require('fs');
var path = require('path');
var config = require('./config/config.json');
var csvWriter = require('csv-write-stream');
var mongo = require('mongodb');
var ObjectID = require('mongodb').ObjectID;
mongo.BSONPure = require('bson').BSONPure;
var BSON = mongo.BSONPure;
var MongoClient = require('mongodb').MongoClient,
assert = require('assert');
var db = require('./db');
var getSize = require('get-folder-size');
var util = require("util"),
rl = require('readline'),
spawn = require('child_process').spawn;
exports.test = function() {
map = function() {
for (var idx = 0; idx < this.freqList.length; idx++) {
var key = this.freqList[idx].freq;
if (this.freqList[idx].errors >= 0) {
if (this.freqList[idx].len > 0) {
var errRatio = Math.round((this.freqList[idx].errors / this.freqList[idx].len) * 100 * 1e2) / 1e2 ;
} else {
var errRatio = 0;
}
var value = {
"spikes": this.freqList[idx].spikes,
"errRatio": errRatio
};
emit(key, value);
}
}
}
reduce = function(key, values) {
var spikes = new Array();
var errors = new Array();
printjson(key);
printjson(values);
values.forEach(function(v) {
spikes.push(v['spikes']);
errors.push(v['errRatio']);
});
return {
"spikes": spikes,
"errors": errors
};
}
db.get().collection('calls', function(err, transCollection) {
transCollection.mapReduce(map, reduce, {
query: {
"shortName": "tester"
},
out: {
inline: 1
},
}, function(err, results) {
if (err) {
console.error(err);
} else {
for (var freq in results) {
if (results[freq].value.spikes) {
writer.pipe(fs.createWriteStream(results[freq]._id + '.csv'));
for (var idx = 0; idx < results[freq].value.spikes.length; idx++) {
writer.write({
"spikes": results[freq].value.spikes[idx],
"errors": results[freq].value.errors[idx]
});
}
}
/*results[freq].forEach(function(err){
writer.write(err)
});*/
console.log("Freq: " + freq + "\n" + util.inspect(results[freq]));
}
writer.end();
}
});
});
}
db.connect(function(err) {
if (err) {
console.log('Unable to connect to Mongo.')
process.exit(1)
} else {
//call_stats.build_usage();
//call_stats.build_call_volume();
//exports.test();
var start = new Date();
start.setHours(start.getHours() - 1);
var collection = db.get().collection("calls");
collection.aggregate([
{
$match: {
time: {
$gte: start
},
shortName: "wmata"
}
},
{ "$unwind": "$freqList"
},
{ "$project": {
"freqList":1,
"errorRatio": { $cond: [ { $eq: [ "$freqList.len", 0 ] }, 0, {"$multiply": [ {"$divide": ["$freqList.errors", "$freqList.len"]}, 100]} ] },
"time": { $dateToString: { format: "%H:%M:%S", date: "$time" } }
}},
{
"$group": {
"_id": {
freq: "$freqList.freq"
},
"time": { $push: "$time"},
"errors": { $push: "$errorRatio"},
"spikes": { $push: "$freqList.spikes"}
}
},
{$sort:{_id:-1}}
], function(err, results) {
if (err) console.error("ERror" + err);
for (var freq in results) {
if (results[freq].errors) {
var writer = csvWriter();
writer.pipe(fs.createWriteStream(results[freq]._id.freq + '.csv'));
for (var idx = 0; idx < results[freq].spikes.length; idx++) {
writer.write({
"time": results[freq].time[idx],
"spikes": results[freq].spikes[idx],
"errors": results[freq].errors[idx]
});
}
writer.end();
console.log("Freq: " + freq + "\n" + util.inspect(results[freq]));
}
}
});
}
})

View File

@@ -1,202 +0,0 @@
var fs = require('fs'),
path = require('path'),
merge = require('fmerge'),
util = require('util'),
rimraf = require('rimraf'),
now,
testRun
var totals = {fileSize: 0, deletedSize:0, files: 0, deleted:0}
function isOlder(path, ageSeconds) {
var stats = fs.statSync(path),
mtime = stats.mtime.getTime(),
expirationTime = (mtime + (ageSeconds * 1000))
return now > expirationTime
}
function hasMaxLevel(options) {
return options && options.hasOwnProperty('maxLevel')
}
function getMaxLevel(options) {
return hasMaxLevel(options) ? options.maxLevel : -1
}
function getAgeSeconds(options) {
return (options && options.age && options.age.seconds) ? options.age.seconds : null
}
function doDeleteDirectory(currentDir, options, currentLevel) {
var doDelete = false
var dir = options && options.dir
if (dir) {
var ageSeconds = getAgeSeconds(options)
var basename = path.basename(currentDir)
if (util.isArray(dir)) {
doDelete = (dir.indexOf("*") !== 1) || (dir.indexOf(basename) !== -1)
} else if (basename === dir || dir === "*") {
doDelete = true
}
if (doDelete && hasMaxLevel(options) && currentLevel > 0) {
doDelete = currentLevel <= getMaxLevel(options)
}
if (ageSeconds && doDelete) {
doDelete = isOlder(currentDir, ageSeconds)
}
}
return doDelete
}
function doDeleteFile(currentFile, options) {
// by default it deletes nothing
var doDelete = false
var extensions = (options && options.extensions) ? options.extensions : null
var files = (options && options.files) ? options.files : null
var dir = (options && options.dir) ? options.dir : null
var ignore = (options && options.ignore) ? options.ignore : null
// return the last portion of a path, the filename aka basename
var basename = path.basename(currentFile)
if (files) {
if (util.isArray(files))
doDelete = (files.indexOf("*.*") !== -1) || (files.indexOf(basename) !== -1)
else {
if (files === '*.*') {
doDelete = true
} else {
doDelete = (basename === files)
}
}
}
if (!doDelete && extensions) {
var currentExt = path.extname(currentFile)
if (util.isArray(extensions)) {
doDelete = extensions.indexOf(currentExt) !== -1
} else {
doDelete = (currentExt === extensions)
}
}
if (doDelete && ignore) {
if (util.isArray(ignore))
doDelete = !(ignore.indexOf(basename) !== -1)
else
doDelete = !(basename === ignore)
}
if (doDelete) {
var ageSeconds = getAgeSeconds(options)
if (ageSeconds)
doDelete = isOlder(currentFile, ageSeconds)
}
return doDelete
}
function isTestRun(options) {
return (options && options.hasOwnProperty('test')) ? options.test : false
}
/**
* findRemoveSync(currentDir, options) takes any start directory and searches files from there for removal.
* the selection of files for removal depends on the given options. when no options are given, or only the maxLevel
* parameter is given, then everything is removed as if there were no filters.
*
* beware: everything happens synchronously.
*
*
* @param {String} currentDir any directory to operate within. it will seek files and/or directories recursively from there.
* beware that it deletes the given currentDir when no options or only the maxLevel parameter are given.
* @param options json object with optional properties like extensions, files, ignore, maxLevel and age.seconds.
* @return {Object} json object of files and/or directories that were found and successfully removed.
* @api public
*/
var findRemoveSync = module.exports = function(currentDir, options, currentLevel) {
var removed = {}
if (fs.existsSync(currentDir)) {
var maxLevel = getMaxLevel(options),
deleteDirectory = false
var removeEmpty = (options && options.removeEmpty);
if (currentLevel === undefined)
currentLevel = 0
else
currentLevel++
if (currentLevel < 1) {
now = new Date().getTime()
totals = {fileSize: 0, deletedSize:0, files: 0, deleted:0};
testRun = isTestRun(options)
} else {
// check directories before deleting files inside.
// this to maintain the original creation time,
// because linux modifies creation date of folders when files within have been deleted.
deleteDirectory = doDeleteDirectory(currentDir, options, currentLevel)
}
if (maxLevel === -1 || currentLevel < maxLevel) {
var filesInDir = fs.readdirSync(currentDir)
filesInDir.forEach(function(file) {
var currentFile = path.join(currentDir, file)
var stat = fs.statSync(currentFile);
if (stat.isDirectory()) {
// the recursive call
var result = findRemoveSync(currentFile, options, currentLevel)
// merge results
removed = merge(removed, result)
} else {
if (doDeleteFile(currentFile, options)) {
if (!testRun)
fs.unlinkSync(currentFile)
totals.deletedSize += stat.size
totals.deleted++;
removed[currentFile] = true
} else {
totals.fileSize += stat.size
totals.files++
}
}
})
if (removeEmpty && (filesInDir<1)) {
deleteDirectory = true;
}
}
if (deleteDirectory) {
try {
if (!testRun)
rimraf.sync(currentDir)
removed[currentDir] = true
} catch (err) {
throw err
}
}
}
if (currentLevel==0) {
return {removed: removed, totals: totals}
} else {
return removed
}
}

View File

@@ -1,14 +0,0 @@
var call_stats = require('./call_stats');
var db = require('./db');
db.connect(function(err) {
if (err) {
console.log('Unable to connect to Mongo.')
process.exit(1)
} else {
//call_stats.build_usage();
//call_stats.build_call_volume();
call_stats.test2();
}
})

View File

@@ -1,5 +1,4 @@
var util = require("util");
var db = require('./db');
const SystemStat = require("./models/system_stat");
var System = require("./models/system");
var talkgroupStats = {};
@@ -34,26 +33,26 @@ async function updateActiveSystems() {
exports.initStats = async function () {
// get the System Stats collection
for await (const item of SystemStat.find()) {
const obj = item.toObject();
// Talkgroup Stats
if (item.talkgroupStats != undefined) {
talkgroupStats[item.shortName] = obj.talkgroupStats;
if (obj.talkgroupStats !== undefined) {
talkgroupStats[obj.shortName] = Object.fromEntries(obj.talkgroupStats);
}
// Decode Errors
if (item.decodeErrorsFreq != undefined) {
decodeErrorsFreq[item.shortName] = obj.decodeErrorsFreq;
if (obj.decodeErrorsFreq !== undefined) {
decodeErrorsFreq[obj.shortName] = Object.fromEntries(obj.decodeErrorsFreq);
}
// Upload Error Totals
if (item.uploadErrors != undefined) {
uploadErrors[item.shortName] = obj.uploadErrors;
if (obj.uploadErrors !== undefined) {
uploadErrors[obj.shortName] = obj.uploadErrors;
}
if (item.callTotals != undefined) {
callTotals[item.shortName] = obj.callTotals;
if (obj.callTotals !== undefined) {
callTotals[obj.shortName] = obj.callTotals;
}
};
}
@@ -62,7 +61,7 @@ exports.initStats = async function () {
exports.addError = function (call) {
// if there is no Array associated with the system.
if (uploadErrors[call.shortName] == undefined) {
if (uploadErrors[call.shortName] === undefined) {
uploadErrors[call.shortName] = new Array();
for (var j = 0; j < spots; j++) {
uploadErrors[call.shortName][j] = 0;
@@ -76,12 +75,12 @@ exports.addError = function (call) {
// Keeps track of the number of calls for each talkgroup for a system
exports.addCall = function (call) {
// if you haven't started keeping track of stats for the System yet
if (talkgroupStats[call.shortName] == undefined) {
if (talkgroupStats[call.shortName] === undefined) {
talkgroupStats[call.shortName] = {};
}
var sysTalkgroupStats = talkgroupStats[call.shortName];
// if you haven't started keeping track of Stats for this TG yet...
if (sysTalkgroupStats[call.talkgroupNum] == undefined) {
if (sysTalkgroupStats[call.talkgroupNum] === undefined) {
sysTalkgroupStats[call.talkgroupNum] = {}
sysTalkgroupStats[call.talkgroupNum].calls = 0;
sysTalkgroupStats[call.talkgroupNum].totalLen = 0;
@@ -105,7 +104,7 @@ exports.addCall = function (call) {
var sysErrors = decodeErrorsFreq[call.shortName];
// if you haven't started keeping track of Stats for this TG yet...
if (sysErrors[call.freq] == undefined) {
if (sysErrors[call.freq] === undefined) {
sysErrors[call.freq] = {}
sysErrors[call.freq].totalLen = 0;
sysErrors[call.freq].errors = 0;
@@ -237,7 +236,7 @@ exports.shiftStats = async function () {
tg.totalLen = 0;
if (tgHistoryTotal == 0) {
// there has been no recent activity on this talkgroup. remove it from the stats.
delete stat[talkgroupNum];
delete sysTalkgroupStats[talkgroupNum];
}
}
}

18541
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -11,10 +11,17 @@
"@testing-library/user-event": "^14.4.3",
"@visx/axis": "^3.1.0",
"@visx/curve": "^3.0.0",
"@visx/event": "^3.0.1",
"@visx/gradient": "^3.0.0",
"@visx/grid": "^3.0.1",
"@visx/group": "^3.0.0",
"@visx/mock-data": "^3.0.0",
"@visx/scale": "^3.0.0",
"@visx/shape": "^3.0.0",
"@visx/tooltip": "^3.1.2",
"axios": "1.2.1",
"d3-array": "^3.2.3",
"d3-time-format": "^4.1.0",
"date-fns": "2.29.3",
"ejs": "3.1.8",
"history": "^5.3.0",

View File

@@ -9,7 +9,7 @@ import { useSelector, useDispatch } from 'react-redux'
import { setFilter, setDateFilter } from "../features/callPlayer/callPlayerSlice";
import { useGetStatsQuery,useGetTalkgroupsQuery } from '../features/api/apiSlice'
import ActivityChart from "./ActivityChart";
import ActivityChart from "./BetterActivityChart";
import { selectSystem } from "../features/systems/systemsSlice";
import {
Container,
@@ -41,7 +41,7 @@ function Activity(props) {
if (talkgroupData && talkgroupData.talkgroups[tgNum] !== undefined) {
tg = talkgroupData.talkgroups[tgNum].description;
}
result.push(<ActivityChart key={tgNum} tg={tg} data={statsData.talkgroupStats[tgNum].callCountHistory} />);
result.push(<ActivityChart key={tgNum} tg={tg} tgNum={tgNum} data={statsData.talkgroupStats[tgNum].callCountHistory} navigate={props.navigate}/>);
}
return result;
} else {

View File

@@ -0,0 +1,444 @@
import React, { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSearchParams } from "react-router-dom";
import { AreaClosed, Line, Bar } from '@visx/shape';
import { curveMonotoneX } from '@visx/curve';
import { GridRows, GridColumns } from '@visx/grid';
import { scaleTime, scaleLinear } from '@visx/scale';
import { withTooltip, Tooltip, TooltipWithBounds, defaultStyles } from '@visx/tooltip';
import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
import { localPoint } from '@visx/event';
import { LinearGradient } from '@visx/gradient';
import { max, extent, bisector } from 'd3-array';
import { timeFormat } from 'd3-time-format';
import { AxisLeft } from '@visx/axis';
import {
Header
} from "semantic-ui-react";
export const background = '#9f0000'; //'#3b6978';
export const background2 = '#9f0000'; //'#204051';
export const accentColor = '#edffea';
export const accentColorDark = '#ff7543'; //#75daad';
const tooltipStyles = {
...defaultStyles,
background,
border: '1px solid white',
color: 'white',
};
// util
const formatDate = timeFormat("%b %d, %H:%m");
// accessors
const getDate = (d) => {
return d.x;
}
const getCallActivity = (d) => {
return d.y;
}
const ChartWithTooltip = (props) => {
//const props = {data};
const {
tooltipData,
tooltipLeft,
tooltipTop,
tooltipOpen,
showTooltip,
hideTooltip,
} = useTooltip();
const width = 500;
const height = 250;
const navigate = useNavigate();
let [searchParams, setSearchParams] = useSearchParams();
// If you don't want to use a Portal, simply replace `TooltipInPortal` below with
// `Tooltip` or `TooltipWithBounds` and remove `containerRef`
const { containerRef, TooltipInPortal } = useTooltipInPortal({
// use TooltipWithBounds
detectBounds: true,
// when tooltip containers are scrolled, this will correctly update the Tooltip position
scroll: true,
})
const bisectDate = bisector((d) => d.x).left;
const getMouseData = (event) => {
const coords = localPoint(event.target.ownerSVGElement, event);
const { x } = coords;
const x0 = dateScale.invert(x);
const index = bisectDate(calls, x0, 1);
const d0 = calls[index - 1];
const d1 = calls[index];
let d = d0;
if (d1 && getDate(d1)) {
d = x0.valueOf() - getDate(d0).valueOf() > getDate(d1).valueOf() - x0.valueOf() ? d1 : d0;
}
return { data: d, coords }
}
const handleMouseClick = (event) => {
const { data } = getMouseData(event);
const timestamp = data.x.getTime();
const params ={
"filter-type": "talkgroups",
"filter-code": props.tgNum,
"time": timestamp
}
setSearchParams(params);
props.navigate(props.tgNum, timestamp);
}
const handleMouseOver = (event, datum) => {
const { coords, data } = getMouseData(event);
showTooltip({
tooltipLeft: coords.x,
tooltipTop: callActivityScale(getCallActivity(data)),
tooltipData: data
});
};
// bounds
const margin = { top: 10, right: 0, bottom: 0, left: 25 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const calls = useMemo(
() => {
let callTotals = [];
const now = new Date();
var MS_PER_MINUTE = 60000;
for (let j = 0; j < props.data.length; j++) {
let spotsBack = j;
let time = new Date(now - spotsBack * 15 * MS_PER_MINUTE);
callTotals.push({ y: props.data[j], x: time });
}
callTotals.sort((a,b) => a.x-b.x);
return callTotals
}, [props.data]
);
const dateScale = useMemo(
() =>
scaleTime({
range: [margin.left, innerWidth + margin.left],
domain: extent(calls, getDate),
}),
[innerWidth, margin.left, calls],
);
const callActivityScale = useMemo(
() => {
return scaleLinear({
range: [innerHeight + margin.top, margin.top],
domain: [0, (max(calls, getCallActivity) || 0)], // + innerHeight / 3],
nice: true,
})
},
[margin.top, innerHeight, calls],
);
return (
// Set `ref={containerRef}` on the element corresponding to the coordinate system that
// `left/top` (passed to `TooltipInPortal`) are relative to.
<div style={{ float: "left", margin: "15px"}}>
<Header as='h3'>{props.tg}</Header>
<div style={{position: 'relative'}}>
<svg ref={containerRef} width={width} height={height} >
<rect
x={margin.left}
y={0}
width={width-margin.left-margin.right}
height={height}
fill="url(#area-background-gradient)"
rx={14}
/>
<LinearGradient id="area-background-gradient" from={background} to={background2} />
<LinearGradient id="area-gradient" from={accentColor} to={accentColor} toOpacity={0.1} />
<GridRows
left={margin.left}
scale={callActivityScale}
width={innerWidth}
strokeDasharray="1,3"
stroke={accentColor}
strokeOpacity={0}
pointerEvents="none"
/>
<GridColumns
top={margin.top}
scale={dateScale}
height={innerHeight}
strokeDasharray="1,3"
stroke={accentColor}
strokeOpacity={0.2}
pointerEvents="none"
/>
<AreaClosed
data={calls}
x={(d) => dateScale(getDate(d)) ?? 0}
y={(d) => callActivityScale(getCallActivity(d)) ?? 0}
yScale={callActivityScale}
strokeWidth={1}
stroke="url(#area-gradient)"
fill="url(#area-gradient)"
curve={curveMonotoneX}
/>
<Bar
x={margin.left}
y={margin.top}
width={innerWidth}
height={innerHeight}
fill="transparent"
rx={14}
onMouseMove={handleMouseOver}
onMouseUp={handleMouseClick}
onMouseOut={hideTooltip}
/>
<AxisLeft scale={callActivityScale} left={margin.left} numTicks={2} hideAxisLine={true} hideZero={true} />
{tooltipData && (
<g>
<Line
from={{ x: tooltipLeft, y: margin.top }}
to={{ x: tooltipLeft, y: innerHeight + margin.top }}
stroke={accentColorDark}
strokeWidth={2}
pointerEvents="none"
strokeDasharray="5,2"
/>
<circle
cx={tooltipLeft}
cy={tooltipTop + 1}
r={4}
fill="black"
fillOpacity={0.1}
stroke="black"
strokeOpacity={0.1}
strokeWidth={2}
pointerEvents="none"
/>
<circle
cx={tooltipLeft}
cy={tooltipTop}
r={4}
fill={accentColorDark}
stroke="white"
strokeWidth={2}
pointerEvents="none"
/>
</g>
)}
</svg>
{tooltipOpen && (
<div>
<TooltipWithBounds
key={Math.random()}
top={tooltipTop - 12}
left={tooltipLeft}
style={tooltipStyles}
>
{`${getCallActivity(tooltipData)}`}
</TooltipWithBounds>
<Tooltip
top={-28}
left={tooltipLeft}
style={{
...defaultStyles,
minWidth: 72,
textAlign: 'center',
transform: 'translateX(-50%)',
}}
>
{formatDate(getDate(tooltipData))}
</Tooltip>
</div>
)}
</div>
</div>
)
};
export default ChartWithTooltip;
/*
export default withTooltip(({
width = 250,
height = 250,
margin = { top: 0, right: 0, bottom: 0, left: 0 },
showTooltip,
hideTooltip,
data,
tooltipTop = 0,
tooltipLeft = 0,
}) => {
//if (width < 10) return null;
// bounds
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const stock = buildData();
const tooltipData = stock;
// scales
const dateScale = useMemo(
() =>
scaleTime({
range: [margin.left, innerWidth + margin.left],
domain: extent(stock, getDate),
}),
[innerWidth, margin.left],
);
const stockValueScale = useMemo(
() =>
scaleLinear({
range: [innerHeight + margin.top, margin.top],
domain: [0, (max(stock, getStockValue) || 0) + innerHeight / 3],
nice: true,
}),
[margin.top, innerHeight],
);
// tooltip handler
const handleTooltip = useCallback(
(event) => {
const { x } = localPoint(event) || { x: 0 };
const x0 = dateScale.invert(x);
const index = bisectDate(stock, x0, 1);
const d0 = stock[index - 1];
const d1 = stock[index];
let d = d0;
if (d1 && getDate(d1)) {
d = x0.valueOf() - getDate(d0).valueOf() > getDate(d1).valueOf() - x0.valueOf() ? d1 : d0;
}
showTooltip({
tooltipData: d,
tooltipLeft: x,
tooltipTop: stockValueScale(getStockValue(d)),
});
},
[showTooltip, stockValueScale, dateScale],
);
return (
<div>
<svg width={width} height={height}>
<rect
x={0}
y={0}
width={width}
height={height}
fill="url(#area-background-gradient)"
rx={14}
/>
<LinearGradient id="area-background-gradient" from={background} to={background2} />
<LinearGradient id="area-gradient" from={accentColor} to={accentColor} toOpacity={0.1} />
<GridRows
left={margin.left}
scale={stockValueScale}
width={innerWidth}
strokeDasharray="1,3"
stroke={accentColor}
strokeOpacity={0}
pointerEvents="none"
/>
<GridColumns
top={margin.top}
scale={dateScale}
height={innerHeight}
strokeDasharray="1,3"
stroke={accentColor}
strokeOpacity={0.2}
pointerEvents="none"
/>
<AreaClosed
data={stock}
x={(d) => dateScale(getDate(d)) ?? 0}
y={(d) => stockValueScale(getStockValue(d)) ?? 0}
yScale={stockValueScale}
strokeWidth={1}
stroke="url(#area-gradient)"
fill="url(#area-gradient)"
curve={curveMonotoneX}
/>
<Bar
x={margin.left}
y={margin.top}
width={innerWidth}
height={innerHeight}
fill="transparent"
rx={14}
onTouchStart={handleTooltip}
onTouchMove={handleTooltip}
onMouseMove={handleTooltip}
onMouseLeave={() => hideTooltip()}
/>
<g>
<Line
from={{ x: tooltipLeft, y: margin.top }}
to={{ x: tooltipLeft, y: innerHeight + margin.top }}
stroke={accentColorDark}
strokeWidth={2}
pointerEvents="none"
strokeDasharray="5,2"
/>
<circle
cx={tooltipLeft}
cy={tooltipTop + 1}
r={4}
fill="black"
fillOpacity={0.1}
stroke="black"
strokeOpacity={0.1}
strokeWidth={2}
pointerEvents="none"
/>
<circle
cx={tooltipLeft}
cy={tooltipTop}
r={4}
fill={accentColorDark}
stroke="white"
strokeWidth={2}
pointerEvents="none"
/>
</g>
</svg>
<div>
<TooltipWithBounds
key={Math.random()}
top={tooltipTop - 12}
left={tooltipLeft + 12}
style={tooltipStyles}
>
{getStockValue(tooltipData)}
</TooltipWithBounds>
<Tooltip
top={innerHeight + margin.top - 14}
left={tooltipLeft}
style={{
...defaultStyles,
minWidth: 72,
textAlign: 'center',
transform: 'translateX(-50%)',
}}
>
{formatDate(getDate(tooltipData))}
</Tooltip>
</div>
</div>
</div>
);
}
);
*/

View File

@@ -51,14 +51,14 @@ function Calls(props) {
const pageYOffset = useRef(); // Store the current Scroll Offset in a way that guarantees the latest value is available when Call Data is updated
const shouldPlayAddCallRef = useRef(); // we need to do this to make the current value of isPlaying available in the socket message callback
shouldPlayAddCallRef.current = (!isPlaying && autoPlay) ? true : false;
const uri = queryString.parse(useLocation().search);
const filterType = useSelector((state) => state.callPlayer.filterType);
const filterGroupId = useSelector((state) => state.callPlayer.filterGroupId);
const filterTalkgroups = useSelector((state) => state.callPlayer.filterTalkgroups);
const filterStarred = useSelector((state) => state.callPlayer.filterStarred);
const filterDate = useSelector((state) => state.callPlayer.filterDate);
//const live = useSelector((state) => state.callPlayer.live);
const uri = queryString.parse(useLocation().search);
const pathname = useLocation().pathname;
let currentCallId = false;
@@ -84,6 +84,22 @@ function Calls(props) {
const handleFilterToggle = () => setFilterVisible(!filterVisible);
const handleCalendarToggle = () => setCalendarVisible(!calendarVisible);
const handleActivityToggle = () => setActivityVisible(!activityVisible);
const handleActivityNavigate = (tgNum, timestamp) =>{
let filter = {
filterDate: timestamp,
filterType: 2, // this is "all"
filterTalkgroups: [tgNum],
filterGroupId: false,
filterStarred: false,
shortName: shortName
};
dispatch(setFilter(filter));
setLive(false);
stopSocket();
setActivityVisible(false)
}
const getFilterDescription = () => {
let filter = { type: 'all', code: "", filterStarred: false };
@@ -235,6 +251,7 @@ function Calls(props) {
const setStateFromUri = async () => {
let filter = {
filterDate: false,
filterType: 0, // this is "all"
@@ -420,7 +437,7 @@ function Calls(props) {
</Menu.Menu>
</Menu>
{activityVisible
? <Activity />
? <Activity navigate={handleActivityNavigate} />
: <CallPlayer callsData={callsData} selectCallId={selectCallId} handleNewer={handleNewer} handleOlder={handleOlder} />
}
</div>

View File

@@ -0,0 +1,212 @@
import React, { useMemo, useCallback } from 'react';
import { AreaClosed, Line, Bar } from '@visx/shape';
import appleStock, { AppleStock } from '@visx/mock-data/lib/mocks/appleStock';
import { curveMonotoneX } from '@visx/curve';
import { GridRows, GridColumns } from '@visx/grid';
import { scaleTime, scaleLinear } from '@visx/scale';
import { withTooltip, Tooltip, TooltipWithBounds, defaultStyles } from '@visx/tooltip';
import { WithTooltipProvidedProps } from '@visx/tooltip/lib/enhancers/withTooltip';
import { localPoint } from '@visx/event';
import { LinearGradient } from '@visx/gradient';
import { max, extent, bisector } from 'd3-array';
import { timeFormat } from 'd3-time-format';
type TooltipData = AppleStock;
const stock = appleStock.slice(800);
export const background = '#3b6978';
export const background2 = '#204051';
export const accentColor = '#edffea';
export const accentColorDark = '#75daad';
const tooltipStyles = {
...defaultStyles,
background,
border: '1px solid white',
color: 'white',
};
// util
const formatDate = timeFormat("%b %d, '%y");
// accessors
const getDate = (d: AppleStock) => new Date(d.date);
const getStockValue = (d: AppleStock) => d.close;
const bisectDate = bisector<AppleStock, Date>((d) => new Date(d.date)).left;
export type AreaProps = {
width: number;
height: number;
margin?: { top: number; right: number; bottom: number; left: number };
};
export default withTooltip<AreaProps, TooltipData>(
({
width,
height,
margin = { top: 0, right: 0, bottom: 0, left: 0 },
showTooltip,
hideTooltip,
tooltipData,
tooltipTop = 0,
tooltipLeft = 0,
}: AreaProps & WithTooltipProvidedProps<TooltipData>) => {
if (width < 10) return null;
// bounds
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
// scales
const dateScale = useMemo(
() =>
scaleTime({
range: [margin.left, innerWidth + margin.left],
domain: extent(stock, getDate) as [Date, Date],
}),
[innerWidth, margin.left],
);
const stockValueScale = useMemo(
() =>
scaleLinear({
range: [innerHeight + margin.top, margin.top],
domain: [0, (max(stock, getStockValue) || 0) + innerHeight / 3],
nice: true,
}),
[margin.top, innerHeight],
);
// tooltip handler
const handleTooltip = useCallback(
(event: React.TouchEvent<SVGRectElement> | React.MouseEvent<SVGRectElement>) => {
const { x } = localPoint(event) || { x: 0 };
const x0 = dateScale.invert(x);
const index = bisectDate(stock, x0, 1);
const d0 = stock[index - 1];
const d1 = stock[index];
let d = d0;
if (d1 && getDate(d1)) {
d = x0.valueOf() - getDate(d0).valueOf() > getDate(d1).valueOf() - x0.valueOf() ? d1 : d0;
}
showTooltip({
tooltipData: d,
tooltipLeft: x,
tooltipTop: stockValueScale(getStockValue(d)),
});
},
[showTooltip, stockValueScale, dateScale],
);
return (
<div>
<svg width={width} height={height}>
<rect
x={0}
y={0}
width={width}
height={height}
fill="url(#area-background-gradient)"
rx={14}
/>
<LinearGradient id="area-background-gradient" from={background} to={background2} />
<LinearGradient id="area-gradient" from={accentColor} to={accentColor} toOpacity={0.1} />
<GridRows
left={margin.left}
scale={stockValueScale}
width={innerWidth}
strokeDasharray="1,3"
stroke={accentColor}
strokeOpacity={0}
pointerEvents="none"
/>
<GridColumns
top={margin.top}
scale={dateScale}
height={innerHeight}
strokeDasharray="1,3"
stroke={accentColor}
strokeOpacity={0.2}
pointerEvents="none"
/>
<AreaClosed<AppleStock>
data={stock}
x={(d) => dateScale(getDate(d)) ?? 0}
y={(d) => stockValueScale(getStockValue(d)) ?? 0}
yScale={stockValueScale}
strokeWidth={1}
stroke="url(#area-gradient)"
fill="url(#area-gradient)"
curve={curveMonotoneX}
/>
<Bar
x={margin.left}
y={margin.top}
width={innerWidth}
height={innerHeight}
fill="transparent"
rx={14}
onTouchStart={handleTooltip}
onTouchMove={handleTooltip}
onMouseMove={handleTooltip}
onMouseLeave={() => hideTooltip()}
/>
{tooltipData && (
<g>
<Line
from={{ x: tooltipLeft, y: margin.top }}
to={{ x: tooltipLeft, y: innerHeight + margin.top }}
stroke={accentColorDark}
strokeWidth={2}
pointerEvents="none"
strokeDasharray="5,2"
/>
<circle
cx={tooltipLeft}
cy={tooltipTop + 1}
r={4}
fill="black"
fillOpacity={0.1}
stroke="black"
strokeOpacity={0.1}
strokeWidth={2}
pointerEvents="none"
/>
<circle
cx={tooltipLeft}
cy={tooltipTop}
r={4}
fill={accentColorDark}
stroke="white"
strokeWidth={2}
pointerEvents="none"
/>
</g>
)}
</svg>
{tooltipData && (
<div>
<TooltipWithBounds
key={Math.random()}
top={tooltipTop - 12}
left={tooltipLeft + 12}
style={tooltipStyles}
>
{`$${getStockValue(tooltipData)}`}
</TooltipWithBounds>
<Tooltip
top={innerHeight + margin.top - 14}
left={tooltipLeft}
style={{
...defaultStyles,
minWidth: 72,
textAlign: 'center',
transform: 'translateX(-50%)',
}}
>
{formatDate(getDate(tooltipData))}
</Tooltip>
</div>
)}
</div>
);
},
);

View File

@@ -19,6 +19,7 @@ import AboutComponent from "./About/AboutComponent"
// Event
import ListEvents from "./Event/ListEvents"
import ViewEvent from "./Event/ViewEvent"
import ActivityChart from './Call/BetterActivityChart';
const App = () => {
@@ -32,6 +33,7 @@ return (
<Route exact path="/events" element={<ListEvents/>} />
<Route exact path="/events/:id" element={<ViewEvent/>} />
<Route exact path="/about" element={<AboutComponent/>} />
<Route exact path="/test" element={<ActivityChart/>} />
</Routes>
)
}

File diff suppressed because it is too large Load Diff