mirror of
https://github.com/openmhz/trunk-server.git
synced 2026-11-14 09:48:10 -05:00
@@ -1,11 +1,19 @@
|
|||||||
/** Important **/
|
/** Important **/
|
||||||
/** You should not be committing this file to GitHub **/
|
/** You should not be committing this file to GitHub **/
|
||||||
/** Repeat: DO! NOT! COMMIT! THIS! FILE! TO! YOUR! REPO! **/
|
/** Repeat: DO! NOT! COMMIT! THIS! FILE! TO! YOUR! REPO! **/
|
||||||
var host = process.env['MONGO_NODE_DRIVER_HOST'] != null ? process.env['MONGO_NODE_DRIVER_HOST'] : 'localhost';
|
const mongo_host = typeof process.env['MONGO_HOST'] !== 'undefined' ? process.env['MONGO_HOST'] : 'mongo';
|
||||||
var port = process.env['MONGO_NODE_DRIVER_PORT'] != null ? process.env['MONGO_NODE_DRIVER_PORT'] : 27017;
|
const mongo_port = typeof process.env['MONGO_PORT'] !== 'undefined' ? process.env['MONGO_PORT'] : 27017;
|
||||||
var mongoUrl = 'mongodb://' + host + ':' + port + '/scanner';
|
const mongo_user = process.env['MONGO_USER'];
|
||||||
|
const mongo_password = process.env['MONGO_PASSWORD'];
|
||||||
|
|
||||||
|
let mongoUrl;
|
||||||
|
|
||||||
|
if ((typeof mongo_user !== 'undefined') && (typeof mongo_password !== 'undefined')) {
|
||||||
|
console.log("Using authentication for MongoDB - user: " + mongo_user);
|
||||||
|
mongoUrl = 'mongodb://' + mongo_user + ':' + mongo_password + '@' + mongo_host + ':' + mongo_port + '/scanner';
|
||||||
|
} else {
|
||||||
|
mongoUrl = 'mongodb://' + mongo_host + ':' + mongo_port + '/scanner';
|
||||||
|
}
|
||||||
|
|
||||||
const secrets = {
|
const secrets = {
|
||||||
db: mongoUrl,
|
db: mongoUrl,
|
||||||
|
|||||||
@@ -42,8 +42,9 @@ const Login = (props) => {
|
|||||||
const loginSubmit = async (event) => {
|
const loginSubmit = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const result = await dispatch(loginUser({ email, password })).unwrap();
|
const result = await dispatch(loginUser({ email, password })).unwrap();
|
||||||
|
const user = result.user;
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
if ((pathname != "/terms") && (terms != 1.1)) {
|
if ((pathname != "/terms") && (user.terms != 1.1)) {
|
||||||
navigate("/terms")
|
navigate("/terms")
|
||||||
} else if (nextLocation) {
|
} else if (nextLocation) {
|
||||||
switch (nextLocation) {
|
switch (nextLocation) {
|
||||||
@@ -130,7 +131,7 @@ const Login = (props) => {
|
|||||||
href="mailto:{process.env.REACT_APP_ADMIN_EMAIL}?Subject={process.env.REACT_APP_SITE_NAME}"
|
href="mailto:{process.env.REACT_APP_ADMIN_EMAIL}?Subject={process.env.REACT_APP_SITE_NAME}"
|
||||||
target="_top"
|
target="_top"
|
||||||
>
|
>
|
||||||
admin@email.com
|
{process.env.REACT_APP_ADMIN_EMAIL}
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
</p>
|
</p>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
/** Important **/
|
/** Important **/
|
||||||
/** You should not be committing this file to GitHub **/
|
/** You should not be committing this file to GitHub **/
|
||||||
/** Repeat: DO! NOT! COMMIT! THIS! FILE! TO! YOUR! REPO! **/
|
/** Repeat: DO! NOT! COMMIT! THIS! FILE! TO! YOUR! REPO! **/
|
||||||
var host = process.env['MONGO_NODE_DRIVER_HOST'] != null ? process.env['MONGO_NODE_DRIVER_HOST'] : 'localhost';
|
const mongo_host = typeof process.env['MONGO_HOST'] !== 'undefined' ? process.env['MONGO_HOST'] : 'mongo';
|
||||||
var port = process.env['MONGO_NODE_DRIVER_PORT'] != null ? process.env['MONGO_NODE_DRIVER_PORT'] : 27017;
|
const mongo_port = typeof process.env['MONGO_PORT'] !== 'undefined' ? process.env['MONGO_PORT'] : 27017;
|
||||||
var mongoUrl = 'mongodb://' + host + ':' + port + '/scanner';
|
const mongo_user = process.env['MONGO_USER'];
|
||||||
|
const mongo_password = process.env['MONGO_PASSWORD'];
|
||||||
|
|
||||||
|
let mongoUrl;
|
||||||
|
|
||||||
|
if ((typeof mongo_user !== 'undefined') && (typeof mongo_password !== 'undefined')) {
|
||||||
|
console.log("Using authentication for MongoDB - user: " + mongo_user);
|
||||||
|
mongoUrl = 'mongodb://' + mongo_user + ':' + mongo_password + '@' + mongo_host + ':' + mongo_port + '/scanner';
|
||||||
|
} else {
|
||||||
|
mongoUrl = 'mongodb://' + mongo_host + ':' + mongo_port + '/scanner';
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = secrets = {
|
module.exports = secrets = {
|
||||||
db: mongoUrl,
|
db: mongoUrl,
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ exports.exportTalkgroups = function (req, res, next) {
|
|||||||
|
|
||||||
async function csv_import(shortName, filename, callback) {
|
async function csv_import(shortName, filename, callback) {
|
||||||
var talkgroups = [];
|
var talkgroups = [];
|
||||||
const count = await Talkgroup.deleteMany({ shortName: shortName });
|
|
||||||
|
|
||||||
var parser = parse(
|
var parser = parse(
|
||||||
{
|
{
|
||||||
@@ -90,6 +90,7 @@ async function csv_import(shortName, filename, callback) {
|
|||||||
console.error("Error: csv import 2: " + err);
|
console.error("Error: csv import 2: " + err);
|
||||||
callback(err, null);
|
callback(err, null);
|
||||||
} else {
|
} else {
|
||||||
|
const count = await Talkgroup.deleteMany({ shortName: shortName });
|
||||||
var response = [];
|
var response = [];
|
||||||
|
|
||||||
for (var i = 0; i < data.length; i++) {
|
for (var i = 0; i < data.length; i++) {
|
||||||
@@ -97,7 +98,7 @@ async function csv_import(shortName, filename, callback) {
|
|||||||
if (!( "Decimal" in row) || !("Description" in row) || !("Alpha Tag" in row)) {
|
if (!( "Decimal" in row) || !("Description" in row) || !("Alpha Tag" in row)) {
|
||||||
console.error("Error: csv import column header error");
|
console.error("Error: csv import column header error");
|
||||||
console.error(row)
|
console.error(row)
|
||||||
callback("Column Headers are required - Required Columns: 'Decimal' 'Decscription' 'Alpha Tag' Optional Column: 'Priority' ", null);
|
callback("Column Headers are required - Required Columns: 'Decimal' 'Description' 'Alpha Tag' Optional Column: 'Priority' ", null);
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -399,6 +399,7 @@ const System = () => {
|
|||||||
<em>
|
<em>
|
||||||
The Talkgroup file is a .CSV file that has infromation about the different talkgroups on the system. Headers are required for the columns, and should be the first
|
The Talkgroup file is a .CSV file that has infromation about the different talkgroups on the system. Headers are required for the columns, and should be the first
|
||||||
line of the file. The columns can be in any order, but the headers must match exactly.<br/>
|
line of the file. The columns can be in any order, but the headers must match exactly.<br/>
|
||||||
|
<p>If you export the file from Excel, make sure to use "Comma Separated Values" and NOT "CSV UTF-8" for the File Format.</p>
|
||||||
An example header line is: <br />
|
An example header line is: <br />
|
||||||
<code>Decimal,Alpha Tag,Description</code><br/>
|
<code>Decimal,Alpha Tag,Description</code><br/>
|
||||||
<p>There can be additional columns so you can use the same Talkgroup CSV file from Trunk Recorder.</p>
|
<p>There can be additional columns so you can use the same Talkgroup CSV file from Trunk Recorder.</p>
|
||||||
|
|||||||
@@ -2,11 +2,19 @@
|
|||||||
|
|
||||||
const opentelemetry = require('@opentelemetry/sdk-node');
|
const opentelemetry = require('@opentelemetry/sdk-node');
|
||||||
const { MongoDBInstrumentation } = require('@opentelemetry/instrumentation-mongodb');
|
const { MongoDBInstrumentation } = require('@opentelemetry/instrumentation-mongodb');
|
||||||
|
|
||||||
const { TraceIdRatioBasedSampler } = require('@opentelemetry/sdk-trace-node');
|
const { TraceIdRatioBasedSampler } = require('@opentelemetry/sdk-trace-node');
|
||||||
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
|
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
|
||||||
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
|
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
|
||||||
const { diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api');
|
const { diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api');
|
||||||
|
const {
|
||||||
|
Detector,
|
||||||
|
DetectorSync,
|
||||||
|
IResource,
|
||||||
|
ResourceDetectionConfig,
|
||||||
|
envDetectorSync,
|
||||||
|
hostDetectorSync,
|
||||||
|
processDetectorSync,
|
||||||
|
} = require("@opentelemetry/resources")
|
||||||
// const {
|
// const {
|
||||||
// dockerCGroupV1Detector,
|
// dockerCGroupV1Detector,
|
||||||
// } = require('@opentelemetry/resource-detector-docker');
|
// } = require('@opentelemetry/resource-detector-docker');
|
||||||
@@ -20,6 +28,19 @@ console.log("OTEL_EXPORTER_OTLP_HEADERS: " + process.env.OTEL_EXPORTER_OTLP_HEAD
|
|||||||
console.log("OTEL_SERVICE_NAME: " + process.env.OTEL_SERVICE_NAME);
|
console.log("OTEL_SERVICE_NAME: " + process.env.OTEL_SERVICE_NAME);
|
||||||
|
|
||||||
|
|
||||||
|
// from https://github.com/open-telemetry/opentelemetry-js/issues/4638
|
||||||
|
// this is a workaround to wait for async attributes to be resolved
|
||||||
|
function awaitAttributes(detector){
|
||||||
|
return {
|
||||||
|
async detect(config) {
|
||||||
|
const resource = detector.detect(config)
|
||||||
|
await resource.waitForAsyncAttributes()
|
||||||
|
|
||||||
|
return resource
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const samplePercentage = 0.05;
|
const samplePercentage = 0.05;
|
||||||
|
|
||||||
const sdk = new opentelemetry.NodeSDK({
|
const sdk = new opentelemetry.NodeSDK({
|
||||||
@@ -37,6 +58,11 @@ const sdk = new opentelemetry.NodeSDK({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
resourceDetectors: [
|
||||||
|
awaitAttributes(envDetectorSync),
|
||||||
|
awaitAttributes(processDetectorSync),
|
||||||
|
awaitAttributes(hostDetectorSync),
|
||||||
|
],
|
||||||
//resourceDetectors: [dockerCGroupV1Detector],
|
//resourceDetectors: [dockerCGroupV1Detector],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ var fs = require('fs');
|
|||||||
|
|
||||||
const { PutObjectCommand, S3Client } = require("@aws-sdk/client-s3");
|
const { PutObjectCommand, S3Client } = require("@aws-sdk/client-s3");
|
||||||
const { fromIni } = require("@aws-sdk/credential-providers");
|
const { fromIni } = require("@aws-sdk/credential-providers");
|
||||||
|
const e = require('express');
|
||||||
|
|
||||||
var s3_endpoint = process.env['S3_ENDPOINT'] != null ? process.env['S3_ENDPOINT'] : 'https://s3.us-west-1.wasabisys.com';
|
var s3_endpoint = process.env['S3_ENDPOINT'] != null ? process.env['S3_ENDPOINT'] : 'https://s3.us-west-1.wasabisys.com';
|
||||||
var s3_region = process.env['S3_REGION'] != null ? process.env['S3_REGION'] : 'us-west-1';
|
var s3_region = process.env['S3_REGION'] != null ? process.env['S3_REGION'] : 'us-west-1';
|
||||||
@@ -177,43 +178,47 @@ const createPodcast = async (tmpEventFolder, podcastFile, event) => {
|
|||||||
inputs.forEach((value, i) => {
|
inputs.forEach((value, i) => {
|
||||||
concat = concat + "file '" + value + "'\n";
|
concat = concat + "file '" + value + "'\n";
|
||||||
});
|
});
|
||||||
const concatFile = tmpEventFolder + "/FILES.txt"
|
try {
|
||||||
fs.writeFileSync(concatFile, concat, "utf8");
|
const concatFile = tmpEventFolder + "/FILES.txt"
|
||||||
// https://video.stackexchange.com/questions/21315/concatenating-split-media-files-using-concat-protocol
|
fs.writeFileSync(concatFile, concat, "utf8");
|
||||||
//ffmpeg -f concat -safe 0 -i mylist.txt -c copy output
|
// https://video.stackexchange.com/questions/21315/concatenating-split-media-files-using-concat-protocol
|
||||||
command.push("-hide_banner");
|
//ffmpeg -f concat -safe 0 -i mylist.txt -c copy output
|
||||||
command.push("-loglevel");
|
command.push("-hide_banner");
|
||||||
command.push("error");
|
command.push("-loglevel");
|
||||||
command.push("-f");
|
command.push("error");
|
||||||
command.push("concat");
|
command.push("-f");
|
||||||
command.push("-safe");
|
command.push("concat");
|
||||||
command.push("0");
|
command.push("-safe");
|
||||||
command.push("-i");
|
command.push("0");
|
||||||
command.push(concatFile);
|
command.push("-i");
|
||||||
command.push("-acodec");
|
command.push(concatFile);
|
||||||
command.push("aac");
|
command.push("-acodec");
|
||||||
command.push(tmpFile);
|
command.push("aac");
|
||||||
await ffmpeg(command);
|
command.push(tmpFile);
|
||||||
|
await ffmpeg(command);
|
||||||
|
|
||||||
|
|
||||||
const chapters = createChapters(event);
|
const chapters = createChapters(event);
|
||||||
const chaptersFile = tmpEventFolder + "/CHAPTERS.txt"
|
const chaptersFile = tmpEventFolder + "/CHAPTERS.txt"
|
||||||
fs.writeFileSync(chaptersFile, chapters, "utf8");
|
fs.writeFileSync(chaptersFile, chapters, "utf8");
|
||||||
command = [];
|
command = [];
|
||||||
command.push("-hide_banner");
|
command.push("-hide_banner");
|
||||||
command.push("-loglevel");
|
command.push("-loglevel");
|
||||||
command.push("error");
|
command.push("error");
|
||||||
command.push("-i");
|
command.push("-i");
|
||||||
command.push(tmpFile)
|
command.push(tmpFile)
|
||||||
command.push("-i");
|
command.push("-i");
|
||||||
command.push(chaptersFile)
|
command.push(chaptersFile)
|
||||||
command.push("-map_metadata")
|
command.push("-map_metadata")
|
||||||
command.push("1")
|
command.push("1")
|
||||||
command.push("-codec");
|
command.push("-codec");
|
||||||
command.push("copy");
|
command.push("copy");
|
||||||
command.push(podcastFile)
|
command.push(podcastFile)
|
||||||
//console.log("Running command: " + command);
|
//console.log("Running command: " + command);
|
||||||
await ffmpeg(command);
|
await ffmpeg(command);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error creating podcast " + e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanupEvent = (folder, filename) => {
|
const cleanupEvent = (folder, filename) => {
|
||||||
@@ -382,11 +387,12 @@ exports.addNewEvent = async function (req, res, next) {
|
|||||||
const savedEvent = await event.save();
|
const savedEvent = await event.save();
|
||||||
//console.log("Event ID " + savedEvent._id);
|
//console.log("Event ID " + savedEvent._id);
|
||||||
const url = "/events/" + event._id;
|
const url = "/events/" + event._id;
|
||||||
res.contentType('json');
|
|
||||||
res.send(JSON.stringify({ url: url }));
|
|
||||||
|
|
||||||
await packageEvent(savedEvent._id);
|
await packageEvent(savedEvent._id);
|
||||||
|
|
||||||
|
res.contentType('json');
|
||||||
|
res.send(JSON.stringify({ url: url }));
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error("Error creating event" + err);
|
console.error("Error creating event" + err);
|
||||||
|
|||||||
@@ -1,13 +1 @@
|
|||||||
const Group = require("../models/group");
|
|
||||||
|
|
||||||
exports.get_groups = async function (req, res) {
|
|
||||||
const grp_results = await Group.find({ shortName: req.params.shortName.toLowerCase()}).sort("position").catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
res.status(500);
|
|
||||||
res.json({ success: false, message: err });
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
||||||
res.contentType('json');
|
|
||||||
res.send(JSON.stringify(grp_results));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,20 +1,53 @@
|
|||||||
const Talkgroup = require("../models/talkgroup");
|
const Talkgroup = require("../models/talkgroup");
|
||||||
|
const Group = require("../models/group");
|
||||||
|
|
||||||
exports.get_talkgroups = async function (req, res) {
|
let groups = {};
|
||||||
const tg_results = await Talkgroup.find({ 'shortName': req.params.shortName.toLowerCase()}).catch(err => {
|
let talkgroups = {};
|
||||||
|
|
||||||
|
async function load_talkgroups(shortName) {
|
||||||
|
const tg_results = await Talkgroup.find({ 'shortName': shortName }).catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500);
|
res.status(500);
|
||||||
res.json({ success: false, message: err });
|
res.json({ success: false, message: err });
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
|
||||||
var talkgroups = {};
|
let temp = {}
|
||||||
for (var tg in tg_results) {
|
for (var tg in tg_results) {
|
||||||
var talkgroup = {_id: tg_results[tg]._id, num: tg_results[tg].num, alpha: tg_results[tg].alpha, description: tg_results[tg].description}
|
var talkgroup = { _id: tg_results[tg]._id, num: tg_results[tg].num, alpha: tg_results[tg].alpha, description: tg_results[tg].description }
|
||||||
talkgroups[tg_results[tg].num] = talkgroup;
|
temp[tg_results[tg].num] = talkgroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.get_talkgroups = async function (req, res) {
|
||||||
|
const shortName = req.params.shortName.toLowerCase();
|
||||||
|
/*
|
||||||
|
if (talkgroups.hasOwnProperty(shortName)) {
|
||||||
|
talkgroups = talkgroups[shortName];
|
||||||
|
} else {
|
||||||
|
}*/
|
||||||
|
|
||||||
|
let talkgroups = await load_talkgroups(shortName);
|
||||||
|
|
||||||
res.contentType('json');
|
res.contentType('json');
|
||||||
res.send(JSON.stringify({
|
res.send(JSON.stringify({
|
||||||
talkgroups: talkgroups
|
talkgroups: talkgroups
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
exports.get_groups = async function (req, res) {
|
||||||
|
const shortName = req.params.shortName.toLowerCase();
|
||||||
|
|
||||||
|
const grp_results = await Group.find({ shortName: req.params.shortName.toLowerCase() }).sort("position").catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500);
|
||||||
|
res.json({ success: false, message: err });
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.contentType('json');
|
||||||
|
res.send(JSON.stringify(grp_results));
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ const client = new S3Client({
|
|||||||
|
|
||||||
exports.upload = async function (req, res, next) {
|
exports.upload = async function (req, res, next) {
|
||||||
const tracer = trace.getTracer("upload-service");
|
const tracer = trace.getTracer("upload-service");
|
||||||
|
let start_time = Date.now();
|
||||||
|
let validateSystemTime = 0;
|
||||||
|
let readFileTime = 0;
|
||||||
|
let uploadFileTime = 0;
|
||||||
|
let saveCallTime = 0;
|
||||||
|
let cleanupTime = 0;
|
||||||
|
let statsTime = 0;
|
||||||
return context.with(context.active(), async () => {
|
return context.with(context.active(), async () => {
|
||||||
const parentSpan = trace.getActiveSpan(context.active());
|
const parentSpan = trace.getActiveSpan(context.active());
|
||||||
await tracer.startActiveSpan('upload_handler', { parent: parentSpan }, async (span) => {
|
await tracer.startActiveSpan('upload_handler', { parent: parentSpan }, async (span) => {
|
||||||
@@ -108,7 +115,7 @@ exports.upload = async function (req, res, next) {
|
|||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
validateSystemTime = Date.now();
|
||||||
if (item.ignoreUnknownTalkgroup) {
|
if (item.ignoreUnknownTalkgroup) {
|
||||||
const talkgroupExists = await Talkgroup.exists({
|
const talkgroupExists = await Talkgroup.exists({
|
||||||
shortName,
|
shortName,
|
||||||
@@ -129,7 +136,7 @@ exports.upload = async function (req, res, next) {
|
|||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
|
|
||||||
const local_path = `/${shortName}/${time.getFullYear()}/${time.getMonth() + 1}/${time.getDate()}/`;
|
const local_path = `/${shortName}/${time.getFullYear()}/${time.getMonth() + 1}/${time.getDate()}/`;
|
||||||
const object_key = `media/${shortName}-${talkgroupNum}-${startTime}${path.extname(req.file.originalname)}`;
|
const object_key = `media/${shortName}/${talkgroupNum}/${shortName}-${talkgroupNum}-${startTime}${path.extname(req.file.originalname)}`;
|
||||||
const url = `${s3_public_url}/${object_key}`;
|
const url = `${s3_public_url}/${object_key}`;
|
||||||
|
|
||||||
const call = new Call({
|
const call = new Call({
|
||||||
@@ -151,10 +158,11 @@ exports.upload = async function (req, res, next) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let fileContent;
|
let fileContent;
|
||||||
|
|
||||||
await tracer.startActiveSpan('upload_to_s3', { parent: trace.getActiveSpan(context.active()) }, async (uploadSpan) => {
|
await tracer.startActiveSpan('upload_to_s3', { parent: trace.getActiveSpan(context.active()) }, async (uploadSpan) => {
|
||||||
try {
|
try {
|
||||||
fileContent = fs.readFileSync(req.file.path);
|
fileContent = fs.readFileSync(req.file.path);
|
||||||
|
readFileTime = Date.now();
|
||||||
const command = new PutObjectCommand({
|
const command = new PutObjectCommand({
|
||||||
Bucket: s3_bucket,
|
Bucket: s3_bucket,
|
||||||
Key: object_key,
|
Key: object_key,
|
||||||
@@ -177,7 +185,7 @@ exports.upload = async function (req, res, next) {
|
|||||||
uploadSpan.end();
|
uploadSpan.end();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
uploadFileTime = Date.now();
|
||||||
await tracer.startActiveSpan('save_call', { parent: trace.getActiveSpan(context.active()) }, async (saveSpan) => {
|
await tracer.startActiveSpan('save_call', { parent: trace.getActiveSpan(context.active()) }, async (saveSpan) => {
|
||||||
try {
|
try {
|
||||||
await call.save();
|
await call.save();
|
||||||
@@ -193,7 +201,9 @@ exports.upload = async function (req, res, next) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
saveCallTime = Date.now();
|
||||||
sysStats.addCall(call.toObject());
|
sysStats.addCall(call.toObject());
|
||||||
|
statsTime = Date.now();
|
||||||
|
|
||||||
if (call.len >= 1) {
|
if (call.len >= 1) {
|
||||||
req.call = call.toObject();
|
req.call = call.toObject();
|
||||||
@@ -211,7 +221,8 @@ exports.upload = async function (req, res, next) {
|
|||||||
cleanupSpan.end();
|
cleanupSpan.end();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
cleanupTime = Date.now();
|
||||||
|
//console.log(`[${call.shortName}] \t Verify System: ${validateSystemTime - start_time} \t Read file: ${readFileTime - validateSystemTime} \t Upload: ${ uploadFileTime - validateSystemTime} \t Save: ${saveCallTime - uploadFileTime} \tStats: ${statsTime - saveCallTime}\tCleanup: ${cleanupTime - statsTime} \t\t Total: ${cleanupTime - start_time}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error processing call upload: " + error);
|
console.error("Error processing call upload: " + error);
|
||||||
span.recordException(error);
|
span.recordException(error);
|
||||||
@@ -222,6 +233,7 @@ exports.upload = async function (req, res, next) {
|
|||||||
} finally {
|
} finally {
|
||||||
span.end();
|
span.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -6,8 +6,9 @@ var calls = require("./controllers/calls");
|
|||||||
var uploads = require("./controllers/uploads");
|
var uploads = require("./controllers/uploads");
|
||||||
var systems = require("./controllers/systems");
|
var systems = require("./controllers/systems");
|
||||||
var talkgroups = require("./controllers/talkgroups");
|
var talkgroups = require("./controllers/talkgroups");
|
||||||
var groups = require("./controllers/groups");
|
|
||||||
var stats = require("./controllers/stats");
|
var stats = require("./controllers/stats");
|
||||||
|
var sys_stats = require("./sys_stats");
|
||||||
var events = require("./controllers/events");
|
var events = require("./controllers/events");
|
||||||
var config = require('./config/config.json');
|
var config = require('./config/config.json');
|
||||||
let db = require('./db')
|
let db = require('./db')
|
||||||
@@ -42,9 +43,19 @@ configureExpress(app)
|
|||||||
|
|
||||||
// -------------------------------------------
|
// -------------------------------------------
|
||||||
|
|
||||||
var host = process.env['MONGO_NODE_DRIVER_HOST'] != null ? process.env['MONGO_NODE_DRIVER_HOST'] : 'mongo';
|
const mongo_host = typeof process.env['MONGO_HOST'] !== 'undefined' ? process.env['MONGO_HOST'] : 'mongo';
|
||||||
var port = process.env['MONGO_NODE_DRIVER_PORT'] != null ? process.env['MONGO_NODE_DRIVER_PORT'] : 27017;
|
const mongo_port = typeof process.env['MONGO_PORT'] !== 'undefined' ? process.env['MONGO_PORT'] : 27017;
|
||||||
var mongoUrl = 'mongodb://' + host + ':' + port + '/scanner';
|
const mongo_user = process.env['MONGO_USER'];
|
||||||
|
const mongo_password = process.env['MONGO_PASSWORD'];
|
||||||
|
|
||||||
|
let mongoUrl;
|
||||||
|
|
||||||
|
if ((typeof mongo_user !== 'undefined') && (typeof mongo_password !== 'undefined')) {
|
||||||
|
console.log("Using authentication for MongoDB - user: " + mongo_user);
|
||||||
|
mongoUrl = 'mongodb://' + mongo_user + ':' + mongo_password + '@' + mongo_host + ':' + mongo_port + '/scanner';
|
||||||
|
} else {
|
||||||
|
mongoUrl = 'mongodb://' + mongo_host + ':' + mongo_port + '/scanner';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const connect = async () => {
|
const connect = async () => {
|
||||||
@@ -60,7 +71,7 @@ const connect = async () => {
|
|||||||
console.log('Mongoose is disconnecting');
|
console.log('Mongoose is disconnecting');
|
||||||
});
|
});
|
||||||
|
|
||||||
await mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true });
|
await mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true, maxPoolSize: 50 });
|
||||||
console.log("All Done");
|
console.log("All Done");
|
||||||
}
|
}
|
||||||
connect();
|
connect();
|
||||||
@@ -94,6 +105,11 @@ function addSystemClients(req, res, next) {
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addTotalClients(req, res, next) {
|
||||||
|
|
||||||
|
req.totalClients = Object.keys(clients).length;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*------ CALLS ----------*/
|
/*------ CALLS ----------*/
|
||||||
@@ -124,7 +140,7 @@ app.post('/:shortName/authorize', systems.authorize_system);
|
|||||||
app.get('/:shortName/talkgroups', talkgroups.get_talkgroups);
|
app.get('/:shortName/talkgroups', talkgroups.get_talkgroups);
|
||||||
|
|
||||||
/*------ GROUPS ----------*/
|
/*------ GROUPS ----------*/
|
||||||
app.get('/:shortName/groups', groups.get_groups);
|
app.get('/:shortName/groups', talkgroups.get_groups);
|
||||||
|
|
||||||
|
|
||||||
/*------ EVENTS ----------*/
|
/*------ EVENTS ----------*/
|
||||||
@@ -134,6 +150,7 @@ app.get('/events/:id', events.getEvent);
|
|||||||
|
|
||||||
/*------ STATS ----------*/
|
/*------ STATS ----------*/
|
||||||
app.get('/:shortName/stats', stats.get_stats);
|
app.get('/:shortName/stats', stats.get_stats);
|
||||||
|
app.get('/stats', addTotalClients, sys_stats.siteStats)
|
||||||
|
|
||||||
|
|
||||||
function get_clients(req, res) {
|
function get_clients(req, res) {
|
||||||
|
|||||||
5601
backend/package-lock.json
generated
5601
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,11 +6,8 @@
|
|||||||
"@aws-sdk/client-s3": "^3.309.0",
|
"@aws-sdk/client-s3": "^3.309.0",
|
||||||
"@aws-sdk/credential-provider-process": "^3.306.0",
|
"@aws-sdk/credential-provider-process": "^3.306.0",
|
||||||
"@aws-sdk/credential-providers": "^3.309.0",
|
"@aws-sdk/credential-providers": "^3.309.0",
|
||||||
"@honeycombio/opentelemetry-node": "^0.7.2",
|
"@opentelemetry/auto-instrumentations-node": "^0.50.0",
|
||||||
"@opentelemetry/api": "^1.7.0",
|
"@opentelemetry/resources": "^1.26.0",
|
||||||
"@opentelemetry/auto-instrumentations-node": "^0.41.1",
|
|
||||||
"@opentelemetry/instrumentation-mongodb": "^0.46.0",
|
|
||||||
"@opentelemetry/resource-detector-docker": "^0.1.2",
|
|
||||||
"@smithy/node-http-handler": "^3.1.4",
|
"@smithy/node-http-handler": "^3.1.4",
|
||||||
"adm-zip": "^0.5.10",
|
"adm-zip": "^0.5.10",
|
||||||
"axios": "1.3.5",
|
"axios": "1.3.5",
|
||||||
|
|||||||
@@ -9,12 +9,21 @@ var uploadErrors = {};
|
|||||||
let decodeErrorsFreq = {};
|
let decodeErrorsFreq = {};
|
||||||
const timePeriod = 15; // in minutes
|
const timePeriod = 15; // in minutes
|
||||||
var spots = (24 * 60) / timePeriod; // the number of spots needed to keep track of 24 hours of stats
|
var spots = (24 * 60) / timePeriod; // the number of spots needed to keep track of 24 hours of stats
|
||||||
|
let uploadsPerMin = new Array(spots).fill(0);
|
||||||
|
let activeSystems = 0;
|
||||||
|
let totalClients = 0;
|
||||||
|
function updateUploadsPerMin(totalUploads) {
|
||||||
|
uploadsPerMin.push(totalUploads / timePeriod);
|
||||||
|
uploadsPerMin.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function updateActiveSystems() {
|
async function updateActiveSystems() {
|
||||||
// Go through all of the Systems
|
// Go through all of the Systems
|
||||||
let siteTotal = 0;
|
let siteTotal = 0;
|
||||||
let activeSystems = 0;
|
activeSystems = 0;
|
||||||
|
|
||||||
for await (let item of System.find()) {
|
for await (let item of System.find()) {
|
||||||
// go through all the systems
|
// go through all the systems
|
||||||
@@ -33,6 +42,11 @@ async function updateActiveSystems() {
|
|||||||
|
|
||||||
await item.save();
|
await item.save();
|
||||||
};
|
};
|
||||||
|
updateUploadsPerMin(siteTotal);
|
||||||
|
// Save talkgroupStats to a JSON file
|
||||||
|
const filePath = "/data/stats/uploadsPerMin.json";
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(uploadsPerMin));
|
||||||
|
console.log("Saved uploadsPerMin to JSON file");
|
||||||
console.log("Site average uploads per minute: " + siteTotal / timePeriod);
|
console.log("Site average uploads per minute: " + siteTotal / timePeriod);
|
||||||
console.log("Active Systems: " + activeSystems);
|
console.log("Active Systems: " + activeSystems);
|
||||||
}
|
}
|
||||||
@@ -72,6 +86,22 @@ exports.initStats = async function () {
|
|||||||
console.error("Decode Errors File not found: " + decodeErrorsFreqPath);
|
console.error("Decode Errors File not found: " + decodeErrorsFreqPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uploadsPerMinPath = "/data/stats/uploadsPerMin.json";
|
||||||
|
if (fs.existsSync(uploadsPerMinPath)) {
|
||||||
|
// Read the contents of the file
|
||||||
|
const fileContents = fs.readFileSync(uploadsPerMinPath, "utf8");
|
||||||
|
|
||||||
|
// Parse the JSON data
|
||||||
|
try {
|
||||||
|
uploadsPerMin = JSON.parse(fileContents);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Decode Errors - Error parsing JSON data:", error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("Decode Errors File not found: " + uploadsPerMinPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
for await (const item of SystemStat.find()) {
|
for await (const item of SystemStat.find()) {
|
||||||
const obj = item.toObject();
|
const obj = item.toObject();
|
||||||
@@ -324,6 +354,18 @@ exports.shiftStats = async function () {
|
|||||||
updateActiveSystems();
|
updateActiveSystems();
|
||||||
console.log("Finished Updating Active Systems at: " + new Date());
|
console.log("Finished Updating Active Systems at: " + new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
exports.siteStats = function(req, res) {
|
||||||
|
const siteStats ={
|
||||||
|
uploadsPerMin,
|
||||||
|
activeSystems,
|
||||||
|
totalClients: req.totalClients
|
||||||
|
}
|
||||||
|
res.contentType('json');
|
||||||
|
res.send(JSON.stringify(siteStats));
|
||||||
|
};
|
||||||
|
|
||||||
exports.callTotals = function (shortName) {
|
exports.callTotals = function (shortName) {
|
||||||
return callTotals[shortName];
|
return callTotals[shortName];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ services:
|
|||||||
- OTEL_EXPORTER_OTLP_ENDPOINT
|
- OTEL_EXPORTER_OTLP_ENDPOINT
|
||||||
- OTEL_EXPORTER_OTLP_HEADERS
|
- OTEL_EXPORTER_OTLP_HEADERS
|
||||||
- OTEL_SERVICE_NAME
|
- OTEL_SERVICE_NAME
|
||||||
- MONGO_NODE_DRIVER_HOST=mongo
|
- MONGO_HOST
|
||||||
|
- MONGO_PORT
|
||||||
|
- MONGO_USER
|
||||||
|
- MONGO_PASSWORD
|
||||||
- MAILJET_KEY
|
- MAILJET_KEY
|
||||||
- MAILJET_SECRET
|
- MAILJET_SECRET
|
||||||
- REACT_APP_ADMIN_EMAIL
|
- REACT_APP_ADMIN_EMAIL
|
||||||
@@ -117,7 +120,10 @@ services:
|
|||||||
- REACT_APP_ACCOUNT_SERVER
|
- REACT_APP_ACCOUNT_SERVER
|
||||||
- REACT_APP_FRONTEND_SERVER
|
- REACT_APP_FRONTEND_SERVER
|
||||||
- REACT_APP_SITE_NAME
|
- REACT_APP_SITE_NAME
|
||||||
- MONGO_NODE_DRIVER_HOST=mongo
|
- MONGO_HOST
|
||||||
|
- MONGO_PORT
|
||||||
|
- MONGO_USER
|
||||||
|
- MONGO_PASSWORD
|
||||||
- NODE_ENV
|
- NODE_ENV
|
||||||
depends_on:
|
depends_on:
|
||||||
- mongo
|
- mongo
|
||||||
@@ -143,7 +149,10 @@ services:
|
|||||||
- NODE_ENV
|
- NODE_ENV
|
||||||
image: "acount:${TAG}-${STAGE}"
|
image: "acount:${TAG}-${STAGE}"
|
||||||
environment:
|
environment:
|
||||||
- MONGO_NODE_DRIVER_HOST=mongo
|
- MONGO_HOST
|
||||||
|
- MONGO_PORT
|
||||||
|
- MONGO_USER
|
||||||
|
- MONGO_PASSWORD
|
||||||
- MAILJET_KEY
|
- MAILJET_KEY
|
||||||
- MAILJET_SECRET
|
- MAILJET_SECRET
|
||||||
- REACT_APP_BACKEND_SERVER
|
- REACT_APP_BACKEND_SERVER
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const tooltipStyles = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// util
|
// util
|
||||||
const formatDate = timeFormat("%b %d, %H:%m");
|
const formatDate = timeFormat("%b %d, %H:%MM");
|
||||||
|
|
||||||
// accessors
|
// accessors
|
||||||
const getDate = (d) => {
|
const getDate = (d) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import SystemCard from "../System/SystemCard";
|
import SystemCard from "../System/SystemCard";
|
||||||
import SupportModal from "../Common/SupportModal";
|
import SupportModal from "../Common/SupportModal";
|
||||||
@@ -16,12 +16,24 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
Segment,
|
Segment,
|
||||||
Sidebar,
|
Sidebar,
|
||||||
|
Loader,
|
||||||
|
Statistic,
|
||||||
Transition,
|
Transition,
|
||||||
|
|
||||||
} from 'semantic-ui-react'
|
} from 'semantic-ui-react'
|
||||||
|
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 { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
import { useGetSystemsQuery } from "../features/api/apiSlice";
|
import { useGetStatsQuery, useGetSystemsQuery, useGetSiteStatsQuery } from "../features/api/apiSlice";
|
||||||
|
|
||||||
/* Responsive component was removed from Semantic UI. This is discussed here: https://github.com/Semantic-Org/Semantic-UI-React/pull/4008 */
|
/* Responsive component was removed from Semantic UI. This is discussed here: https://github.com/Semantic-Org/Semantic-UI-React/pull/4008 */
|
||||||
|
|
||||||
@@ -201,6 +213,8 @@ const MobileContainer = (props) => {
|
|||||||
</Media>
|
</Media>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
MobileContainer.propTypes = {
|
MobileContainer.propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
@@ -213,6 +227,254 @@ const ResponsiveContainer = ({ children }) => (
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const BetterSiteStatsChart = ({siteStats}) => {
|
||||||
|
//const props = {data};
|
||||||
|
|
||||||
|
const background = '#9f0000'; //'#3b6978';
|
||||||
|
const background2 = '#9f0000'; //'#204051';
|
||||||
|
const accentColor = '#edffea';
|
||||||
|
const accentColorDark = '#ff7543'; //#75daad';
|
||||||
|
const tooltipStyles = {
|
||||||
|
...defaultStyles,
|
||||||
|
background,
|
||||||
|
border: '1px solid white',
|
||||||
|
color: 'white',
|
||||||
|
};
|
||||||
|
const formatDate = timeFormat("%b %d, %H:%MM");
|
||||||
|
// accessors
|
||||||
|
const getDate = (d) => {
|
||||||
|
return d.x;
|
||||||
|
}
|
||||||
|
const getCallActivity = (d) => {
|
||||||
|
return parseInt(d.y);
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
tooltipData,
|
||||||
|
tooltipLeft,
|
||||||
|
tooltipTop,
|
||||||
|
tooltipOpen,
|
||||||
|
showTooltip,
|
||||||
|
hideTooltip,
|
||||||
|
} = useTooltip();
|
||||||
|
|
||||||
|
const width = 500;
|
||||||
|
const height = 250;
|
||||||
|
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 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 < siteStats.length; j++) {
|
||||||
|
let spotsBack = siteStats.length - j;
|
||||||
|
let time = new Date(now - spotsBack * 15 * MS_PER_MINUTE);
|
||||||
|
callTotals.push({ y: siteStats[j], x: time });
|
||||||
|
}
|
||||||
|
callTotals.sort((a,b) => a.x-b.x);
|
||||||
|
return callTotals
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}, [siteStats]
|
||||||
|
);
|
||||||
|
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={{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}
|
||||||
|
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>
|
||||||
|
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
const SiteStatsContainer = () => {
|
||||||
|
const { data: siteStats, isSuccess: siteStatsSuccess } = useGetSiteStatsQuery();
|
||||||
|
|
||||||
|
if (!siteStatsSuccess) {
|
||||||
|
return <Loader size='large'>Loading Site Stats</Loader>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Site Activity</h2>
|
||||||
|
<BetterSiteStatsChart siteStats={siteStats.uploadsPerMin} />
|
||||||
|
Calls per Minute
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
const Main = (props) => {
|
const Main = (props) => {
|
||||||
|
|
||||||
@@ -220,6 +482,8 @@ const Main = (props) => {
|
|||||||
const [currentSystem, setCurrentSystem] = useState(0);
|
const [currentSystem, setCurrentSystem] = useState(0);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: systems, isSuccess } = useGetSystemsQuery(); //= selectAllSystems();
|
const { data: systems, isSuccess } = useGetSystemsQuery(); //= selectAllSystems();
|
||||||
|
const { data: siteStats, isSuccess: siteStatsSuccess } = useGetSiteStatsQuery();
|
||||||
|
|
||||||
|
|
||||||
function useInterval(callback, delay) {
|
function useInterval(callback, delay) {
|
||||||
const savedCallback = useRef();
|
const savedCallback = useRef();
|
||||||
@@ -316,6 +580,31 @@ const Main = (props) => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
|
||||||
|
<Segment style={{ padding: '0em' }} vertical>
|
||||||
|
<Grid columns='equal' stackable>
|
||||||
|
<Grid.Row textAlign='center'>
|
||||||
|
<Grid.Column style={{ paddingBottom: '5em', paddingTop: '5em' }}>
|
||||||
|
<SiteStatsContainer />
|
||||||
|
</Grid.Column>
|
||||||
|
<Grid.Column style={{ paddingBottom: '5em', paddingTop: '5em' }}>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', height: '100%' }}>
|
||||||
|
<Statistic>
|
||||||
|
<Statistic.Value>
|
||||||
|
{siteStats?.activeSystems || 0} <Icon name='volume up' size='small' />
|
||||||
|
</Statistic.Value>
|
||||||
|
<Statistic.Label>Active Systems</Statistic.Label>
|
||||||
|
</Statistic>
|
||||||
|
<Statistic>
|
||||||
|
<Statistic.Value>
|
||||||
|
{siteStats?.totalClients || 0} <Icon name='headphones' size='small' />
|
||||||
|
</Statistic.Value>
|
||||||
|
<Statistic.Label>People Listening</Statistic.Label>
|
||||||
|
</Statistic>
|
||||||
|
</div>
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid.Row>
|
||||||
|
</Grid>
|
||||||
|
</Segment>
|
||||||
|
|
||||||
|
|
||||||
<Segment style={{ padding: '0em' }} vertical>
|
<Segment style={{ padding: '0em' }} vertical>
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ export const apiSlice = createApi({
|
|||||||
return responseData;
|
return responseData;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
getSiteStats: builder.query({
|
||||||
|
query: () => ({ url: '/stats' }),
|
||||||
|
transformResponse: responseData => {
|
||||||
|
// You can add any data transformation here if needed
|
||||||
|
return responseData;
|
||||||
|
}
|
||||||
|
}),
|
||||||
getTalkgroups: builder.query({
|
getTalkgroups: builder.query({
|
||||||
// The URL for the request is '/fakeApi/posts'
|
// The URL for the request is '/fakeApi/posts'
|
||||||
query: (shortName) => ({ url: `/${shortName}/talkgroups` })
|
query: (shortName) => ({ url: `/${shortName}/talkgroups` })
|
||||||
@@ -69,4 +76,4 @@ export const apiSlice = createApi({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Export the auto-generated hook for the `getPosts` query endpoint
|
// Export the auto-generated hook for the `getPosts` query endpoint
|
||||||
export const { useGetGroupsQuery, useGetSystemsQuery, useGetTalkgroupsQuery, useGetStatsQuery, useGetEventsQuery, useGetEventQuery, useAddNewEventMutation, useContactSystemMutation } = apiSlice
|
export const { useGetGroupsQuery, useGetSystemsQuery, useGetTalkgroupsQuery, useGetStatsQuery, useGetSiteStatsQuery, useGetEventsQuery, useGetEventQuery, useAddNewEventMutation, useContactSystemMutation } = apiSlice
|
||||||
|
|||||||
Reference in New Issue
Block a user