mirror of
https://github.com/openmhz/trunk-server.git
synced 2026-11-12 08:47:40 -05:00
Merge pull request #144 from openmhz/features/clean-up
Features/clean up
This commit is contained in:
20130
admin/package-lock.json
generated
Normal file
20130
admin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
58
admin/src/AllSystems/AllSystems.js
Normal file
58
admin/src/AllSystems/AllSystems.js
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
1300
admin/yarn.lock
1300
admin/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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]
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
@@ -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]));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
})
|
||||
@@ -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
18541
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
444
frontend/src/Call/BetterActivityChart.js
Normal file
444
frontend/src/Call/BetterActivityChart.js
Normal 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>
|
||||
);
|
||||
}
|
||||
);
|
||||
*/
|
||||
@@ -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>
|
||||
|
||||
212
frontend/src/Call/Example.js
Normal file
212
frontend/src/Call/Example.js
Normal 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>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
3317
frontend/yarn.lock
3317
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user