mirror of
https://github.com/openmhz/trunk-server.git
synced 2026-11-13 17:27:37 -05:00
activity charts!
This commit is contained in:
69
frontend/src/Call/Activity.js
Normal file
69
frontend/src/Call/Activity.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { useEffect, useLayoutEffect, useState, useRef, useCallback, useMemo } from "react";
|
||||
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom';
|
||||
import FilterModal from "./components/FilterModal";
|
||||
import GroupModal from "./components/GroupModal";
|
||||
import CalendarModal from "./components/CalendarModal";
|
||||
import CallPlayer from "./CallPlayer";
|
||||
|
||||
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 { selectSystem } from "../features/systems/systemsSlice";
|
||||
import {
|
||||
Container,
|
||||
Label,
|
||||
Rail,
|
||||
Sticky,
|
||||
Menu,
|
||||
Icon,
|
||||
Sidebar,
|
||||
Header
|
||||
|
||||
} from "semantic-ui-react";
|
||||
import "./CallPlayer.css";
|
||||
import queryString from '../query-string';
|
||||
import io from 'socket.io-client';
|
||||
import { useCallLink } from "./components/CallLinks";
|
||||
|
||||
|
||||
function Activity(props) {
|
||||
const { shortName } = useParams();
|
||||
const { data: statsData, isSuccess: isStatsSuccess } = useGetStatsQuery(shortName);
|
||||
const { data: talkgroupData, isSuccess: isTalkgroupsSuccess } = useGetTalkgroupsQuery(shortName);
|
||||
|
||||
const talkgroupStats = useMemo(() => {
|
||||
if (statsData ) {
|
||||
const result =[]
|
||||
for (const tgNum in statsData.talkgroupStats) {
|
||||
let tg = tgNum;
|
||||
if (talkgroupData && talkgroupData.talkgroups[tgNum] !== undefined) {
|
||||
tg = talkgroupData.talkgroups[tgNum].description;
|
||||
}
|
||||
result.push(<ActivityChart key={tgNum} tg={tg} data={statsData.talkgroupStats[tgNum].callCountHistory} />);
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
}, [statsData, talkgroupData]);
|
||||
|
||||
|
||||
return (
|
||||
<Container style={{ marginTop: "65px"}}>
|
||||
<Header as="h2">Talkgroup Activity</Header>
|
||||
{
|
||||
talkgroupStats.map( chart => {
|
||||
return chart
|
||||
})
|
||||
}
|
||||
|
||||
</Container>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default Activity;
|
||||
102
frontend/src/Call/ActivityChart.js
Normal file
102
frontend/src/Call/ActivityChart.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import React from "react";
|
||||
import { Group } from '@visx/group';
|
||||
import { curveStep} from '@visx/curve';
|
||||
import { LinePath } from '@visx/shape';
|
||||
import { scaleLinear, scaleTime } from '@visx/scale';
|
||||
import { timeParse, timeFormat } from 'd3-time-format';
|
||||
import { AxisBottom, AxisLeft } from '@visx/axis';
|
||||
import { Loader } from "semantic-ui-react";
|
||||
import {
|
||||
Header
|
||||
} from "semantic-ui-react";
|
||||
|
||||
function ActivityChart(props) {
|
||||
|
||||
if (!props.data) {
|
||||
return (
|
||||
<Loader size='large'>Loading</Loader>
|
||||
)
|
||||
}
|
||||
|
||||
var callTotals = [];
|
||||
|
||||
|
||||
const now = new Date();
|
||||
var maxDate = now;
|
||||
var minDate = now;
|
||||
var minValue = 0;
|
||||
var maxValue = 0;
|
||||
var MS_PER_MINUTE = 60000;
|
||||
|
||||
for (let j = 0; j < props.data.length; j++) {
|
||||
let spotsBack = j;//statistic.callTotals.length - j;
|
||||
let time = new Date(now - spotsBack * 15 * MS_PER_MINUTE);
|
||||
if (time < minDate) minDate = time;
|
||||
if (props.data[j] > maxValue)
|
||||
maxValue = props.data[j];
|
||||
callTotals.push({ y: props.data[j], x: time });
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Define the graph dimensions and margins
|
||||
const width = 500; //props.width;
|
||||
const height = 250;
|
||||
const margin = { top: 20, bottom: 40, left: 40, right: 20 };
|
||||
const parseDate = timeParse("%Y%m%d");
|
||||
const format = timeFormat("%b %d");
|
||||
const formatDate = (date) => format(parseDate(date));
|
||||
const parseTime = timeParse('%Y-%m-%dT%H:%M:%S.%LZ');
|
||||
|
||||
// Then we'll create some bounds
|
||||
const xMax = width - margin.left - margin.right;
|
||||
const yMax = height - margin.top - margin.bottom;
|
||||
|
||||
var xScale, yScale;
|
||||
|
||||
// We'll use some mock data from `@vx/mock-data` for this.
|
||||
// scales
|
||||
xScale = scaleTime({
|
||||
range: [0, xMax],
|
||||
domain: [minDate, maxDate],
|
||||
tickFormat: () => (val) => formatDate(val)
|
||||
});
|
||||
yScale = scaleLinear({
|
||||
range: [yMax, 0],
|
||||
domain: [0, maxValue],
|
||||
});
|
||||
|
||||
const handleClick = (event) => {
|
||||
console.log('You clicked on:', event);
|
||||
};
|
||||
// accessors
|
||||
const x = d => xScale(d.x);
|
||||
const y = d => yScale(d.y);
|
||||
return (
|
||||
<div style={{float: 'left'}}>
|
||||
<Header as='h3'>{props.tg}</Header>
|
||||
<svg width={width} height={height}>
|
||||
<Group left={margin.left} top={margin.top}>
|
||||
<AxisBottom top={yMax} scale={xScale} numTicks={width > 520 ? 10 : 5} />
|
||||
<AxisLeft scale={yScale} />
|
||||
<LinePath
|
||||
data={callTotals}
|
||||
x={x}
|
||||
y={y}
|
||||
onClick={handleClick}
|
||||
strokeWidth={2}
|
||||
stroke="blue"
|
||||
curve={curveStep}
|
||||
/>
|
||||
</Group>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActivityChart;
|
||||
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import React, { useEffect, useLayoutEffect, useState, useRef, useCallback, useMemo } from "react";
|
||||
import React, { useEffect, useLayoutEffect, useState, useRef, useMemo } from "react";
|
||||
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom';
|
||||
import FilterModal from "./components/FilterModal";
|
||||
import GroupModal from "./components/GroupModal";
|
||||
import CalendarModal from "./components/CalendarModal";
|
||||
import CallPlayer from "./CallPlayer";
|
||||
import Activity from "./Activity";
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { setFilter, setDateFilter } from "../features/callPlayer/callPlayerSlice";
|
||||
import { getCalls, addCall, getOlderCalls, getNewerCalls } from "../features/calls/callsSlice";
|
||||
import { useGetGroupsQuery, useGetSystemsQuery, } from '../features/api/apiSlice'
|
||||
import { selectSystem } from "../features/systems/systemsSlice";
|
||||
import {
|
||||
Container,
|
||||
Label,
|
||||
Rail,
|
||||
Sticky,
|
||||
Menu,
|
||||
Icon,
|
||||
Sidebar
|
||||
@@ -21,7 +19,6 @@ import {
|
||||
import "./CallPlayer.css";
|
||||
import queryString from '../query-string';
|
||||
import io from 'socket.io-client';
|
||||
import { useCallLink } from "./components/CallLinks";
|
||||
|
||||
|
||||
|
||||
@@ -43,6 +40,7 @@ function Calls(props) {
|
||||
const [filterVisible, setFilterVisible] = useState(false);
|
||||
const [groupVisible, setGroupVisible] = useState(false);
|
||||
const [calendarVisible, setCalendarVisible] = useState(false);
|
||||
const [activityVisible, setActivityVisible] = useState(false);
|
||||
const [isConnected, setIsConnected] = useState(socket.connected);
|
||||
const [selectCallId, setSelectCallId] = useState(false);
|
||||
const [live, setLive] = useState(true);
|
||||
@@ -85,6 +83,7 @@ function Calls(props) {
|
||||
const handleSidebarToggle = () => setSidebarOpened(!sidebarOpened);
|
||||
const handleFilterToggle = () => setFilterVisible(!filterVisible);
|
||||
const handleCalendarToggle = () => setCalendarVisible(!calendarVisible);
|
||||
const handleActivityToggle = () => setActivityVisible(!activityVisible);
|
||||
|
||||
const getFilterDescription = () => {
|
||||
let filter = { type: 'all', code: "", filterStarred: false };
|
||||
@@ -400,6 +399,10 @@ function Calls(props) {
|
||||
{system && system.name}
|
||||
</Container>
|
||||
<Menu.Menu position="right">
|
||||
<Menu.Item name='atcivity-btn' onClick={handleActivityToggle} active={activityVisible}>
|
||||
<Icon name="line graph" />
|
||||
<span >Activity</span>
|
||||
</Menu.Item>
|
||||
<Menu.Item name='archive-btn' onClick={handleCalendarToggle} active={!live}>
|
||||
<Icon name="calendar" />
|
||||
<span className="desktop-only">
|
||||
@@ -416,7 +419,10 @@ function Calls(props) {
|
||||
</Menu.Item>
|
||||
</Menu.Menu>
|
||||
</Menu>
|
||||
<CallPlayer callsData={callsData} selectCallId={selectCallId} handleNewer={handleNewer} handleOlder={handleOlder} />
|
||||
{activityVisible
|
||||
? <Activity />
|
||||
: <CallPlayer callsData={callsData} selectCallId={selectCallId} handleNewer={handleNewer} handleOlder={handleOlder} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,10 @@ export const apiSlice = createApi({
|
||||
// The URL for the request is '/fakeApi/posts'
|
||||
query: (shortName) => ({ url: `/${shortName}/talkgroups` })
|
||||
}),
|
||||
getStats: builder.query({
|
||||
// The URL for the request is '/fakeApi/posts'
|
||||
query: (shortName) => ({ url: `/${shortName}/stats` })
|
||||
}),
|
||||
addNewEvent: builder.mutation({
|
||||
query: initialEvent => ({
|
||||
url: '/events',
|
||||
@@ -56,4 +60,4 @@ export const apiSlice = createApi({
|
||||
})
|
||||
|
||||
// Export the auto-generated hook for the `getPosts` query endpoint
|
||||
export const { useGetGroupsQuery, useGetSystemsQuery, useGetTalkgroupsQuery, useGetEventsQuery, useGetEventQuery, useAddNewEventMutation } = apiSlice
|
||||
export const { useGetGroupsQuery, useGetSystemsQuery, useGetTalkgroupsQuery, useGetStatsQuery, useGetEventsQuery, useGetEventQuery, useAddNewEventMutation } = apiSlice
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
const now = new Date();
|
||||
const now = new Date().getTime();
|
||||
const initialState = {
|
||||
isWaiting: false,
|
||||
shortName: "",
|
||||
|
||||
@@ -54,7 +54,7 @@ export const { selectAll: selectAllCalls, selectById: selectCallById } = callsAd
|
||||
return selectCallsData(state) ?? initialState
|
||||
})
|
||||
*/
|
||||
const now = new Date();
|
||||
const now = new Date().getTime();
|
||||
|
||||
export const callsAdapter = createEntityAdapter({
|
||||
selectId: (call) => call._id,
|
||||
@@ -177,7 +177,7 @@ export const getCalls = createAsyncThunk(
|
||||
'calls/getOlderCalls',
|
||||
async({},{getState, requestId} ) => {
|
||||
const {loading,oldestCallTime, newestCallTime} = getState().calls;
|
||||
const url = buildCallUrl(getState,"older",oldestCallTime.getTime());
|
||||
const url = buildCallUrl(getState,"older",oldestCallTime);
|
||||
const res = await fetch(url).then(
|
||||
(data) => data.json()
|
||||
)
|
||||
@@ -188,7 +188,7 @@ export const getCalls = createAsyncThunk(
|
||||
'calls/getNewerCalls',
|
||||
async({},{getState, requestId} ) => {
|
||||
const {loading,oldestCallTime, newestCallTime} = getState().calls;
|
||||
const url = buildCallUrl(getState,"newer",newestCallTime.getTime());
|
||||
const url = buildCallUrl(getState,"newer",newestCallTime);
|
||||
const res = await fetch(url).then(
|
||||
(data) => data.json()
|
||||
)
|
||||
@@ -223,8 +223,8 @@ export const callsSlice = createSlice({
|
||||
const firstTime = state.data.entities[first].time;
|
||||
const last = state.data.ids[state.data.ids.length-1];
|
||||
const lastTime = state.data.entities[last].time;
|
||||
state.newestCallTime = new Date(firstTime)
|
||||
state.oldestCallTime = new Date(lastTime)
|
||||
state.newestCallTime = new Date(firstTime).getTime();
|
||||
state.oldestCallTime = new Date(lastTime).getTime();
|
||||
}
|
||||
},
|
||||
[getOlderCalls.pending]: (state) => {
|
||||
@@ -242,8 +242,8 @@ export const callsSlice = createSlice({
|
||||
const firstTime = state.data.entities[first].time;
|
||||
const last = state.data.ids[state.data.ids.length-1];
|
||||
const lastTime = state.data.entities[last].time;
|
||||
state.newestCallTime = new Date(firstTime)
|
||||
state.oldestCallTime = new Date(lastTime)
|
||||
state.newestCallTime = new Date(firstTime).getTime();
|
||||
state.oldestCallTime = new Date(lastTime).getTime();
|
||||
}
|
||||
},
|
||||
[getNewerCalls.pending]: (state) => {
|
||||
@@ -261,8 +261,8 @@ export const callsSlice = createSlice({
|
||||
const firstTime = state.data.entities[first].time;
|
||||
const last = state.data.ids[state.data.ids.length-1];
|
||||
const lastTime = state.data.entities[last].time;
|
||||
state.newestCallTime = new Date(firstTime)
|
||||
state.oldestCallTime = new Date(lastTime)
|
||||
state.newestCallTime = new Date(firstTime).getTime();
|
||||
state.oldestCallTime = new Date(lastTime).getTime();
|
||||
}
|
||||
},
|
||||
[addStar.fulfilled]: (state, {payload}) => {
|
||||
|
||||
Reference in New Issue
Block a user