add ingest analytics page
This commit is contained in:
parent
2665a6d25f
commit
1e41262d70
7 changed files with 270 additions and 10 deletions
|
@ -1,11 +1,50 @@
|
|||
use async_graphql::Object;
|
||||
use async_graphql::{futures_util::TryStreamExt, Context, Object, SimpleObject};
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::{query, Pool, Postgres, Row};
|
||||
|
||||
pub struct Analytics {}
|
||||
|
||||
#[derive(SimpleObject, Debug, Clone)]
|
||||
pub struct Event {
|
||||
pub time: DateTime<Utc>,
|
||||
pub event_name: String,
|
||||
pub world_id: i32,
|
||||
pub count: i64,
|
||||
}
|
||||
|
||||
#[Object]
|
||||
impl Analytics {
|
||||
async fn population(&self) -> i32 {
|
||||
0
|
||||
/// Get all events in analytics, bucket_size is in seconds
|
||||
async fn events<'ctx>(
|
||||
&self,
|
||||
ctx: &Context<'ctx>,
|
||||
#[graphql(default = 60)] bucket_size: u64,
|
||||
world_id: Option<i32>,
|
||||
) -> Vec<Event> {
|
||||
let pool = ctx.data::<Pool<Postgres>>().unwrap();
|
||||
|
||||
let sql = format!("SELECT time_bucket('{} seconds', time) AS bucket, count(*), event_name, world_id FROM analytics WHERE time > now() - interval '1 day' {} GROUP BY bucket, world_id, event_name ORDER BY bucket ASC",
|
||||
bucket_size,
|
||||
if let Some(world_id) = world_id {
|
||||
format!("AND world_id = {}", world_id)
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
let mut result = query(sql.as_str()).fetch(pool);
|
||||
|
||||
let mut events = Vec::new();
|
||||
while let Some(row) = result.try_next().await.unwrap() {
|
||||
events.push(Event {
|
||||
time: row.get("bucket"),
|
||||
event_name: row.get("event_name"),
|
||||
world_id: row.get("world_id"),
|
||||
count: row.get("count"),
|
||||
});
|
||||
}
|
||||
|
||||
events
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,213 @@
|
|||
color: #cead42;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 50vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.smaller {
|
||||
height: 33vh;
|
||||
}
|
||||
</style>
|
||||
<h1>Ingest Stats</h1>
|
||||
<p>sorry wip</p>
|
||||
<div>
|
||||
<h3>All Events by Type</h3>
|
||||
<div class="chart-container">
|
||||
<canvas id="all-events-by-type" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Events by World</h3>
|
||||
<div class="chart-container">
|
||||
<canvas id="events-by-world" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Connery</h3>
|
||||
<div class="chart-container smaller">
|
||||
<canvas id="connery" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Miller</h3>
|
||||
<div class="chart-container smaller">
|
||||
<canvas id="miller" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Cobalt</h3>
|
||||
<div class="chart-container smaller">
|
||||
<canvas id="cobalt" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Emerald</h3>
|
||||
<div class="chart-container smaller">
|
||||
<canvas id="emerald" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Jaeger</h3>
|
||||
<div class="chart-container smaller">
|
||||
<canvas id="jaeger" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>SolTech</h3>
|
||||
<div class="chart-container smaller">
|
||||
<canvas id="soltech" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Genudine</h3>
|
||||
<div class="chart-container smaller">
|
||||
<canvas id="genudine" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Ceres</h3>
|
||||
<div class="chart-container smaller">
|
||||
<canvas id="ceres" />
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
|
||||
<script>
|
||||
const renderChart = (id, data, config = {}) => {};
|
||||
|
||||
const allEventsByType = (id, events) => {
|
||||
let allEvents = events.reduce(
|
||||
(acc, ev) => {
|
||||
acc[ev.eventName][ev.time] = (acc[ev.time] || 0) + ev.count;
|
||||
return acc;
|
||||
},
|
||||
{ Death: {}, VehicleDestroy: {} }
|
||||
);
|
||||
|
||||
new Chart(document.getElementById(id), {
|
||||
type: "line",
|
||||
options: {
|
||||
scales: {
|
||||
y: { beginAtZero: true, suggestedMin: 0 },
|
||||
x: { stacked: true, type: "timeseries" },
|
||||
},
|
||||
},
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: "Deaths",
|
||||
data: allEvents.Death,
|
||||
// backgroundColor: "#cead42",
|
||||
},
|
||||
{
|
||||
label: "Vehicle Destroys",
|
||||
data: allEvents.VehicleDestroy,
|
||||
// backgroundColor: "#7842ce",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const eventsByWorld = (events) => {
|
||||
let allEvents = events.reduce((acc, ev) => {
|
||||
acc[ev.worldId] = acc[ev.worldId] || {};
|
||||
acc[ev.worldId][ev.time] = (acc[ev.time] || 0) + ev.count;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
new Chart(document.getElementById("events-by-world"), {
|
||||
type: "line",
|
||||
options: {
|
||||
scales: {
|
||||
y: { beginAtZero: true },
|
||||
x: {
|
||||
type: "timeseries",
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: "Connery",
|
||||
data: allEvents["1"],
|
||||
},
|
||||
{
|
||||
label: "Miller",
|
||||
data: allEvents["10"],
|
||||
},
|
||||
{
|
||||
label: "Cobalt",
|
||||
data: allEvents["13"],
|
||||
},
|
||||
{
|
||||
label: "Emerald",
|
||||
data: allEvents["17"],
|
||||
},
|
||||
{
|
||||
label: "Jaeger",
|
||||
data: allEvents["19"],
|
||||
},
|
||||
{
|
||||
label: "SolTech",
|
||||
data: allEvents["40"],
|
||||
},
|
||||
{
|
||||
label: "Genudine",
|
||||
data: allEvents["1000"],
|
||||
},
|
||||
{
|
||||
label: "Ceres",
|
||||
data: allEvents["2000"],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
(async () => {
|
||||
let resp = await fetch("/graphql", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
{
|
||||
analytics {
|
||||
events(bucketSize: ${60 * 5}) {
|
||||
eventName
|
||||
count
|
||||
time
|
||||
worldId
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
});
|
||||
|
||||
let body = await resp.json();
|
||||
|
||||
let events = body.data.analytics.events;
|
||||
window.events = events;
|
||||
|
||||
allEventsByType("all-events-by-type", events);
|
||||
eventsByWorld(events);
|
||||
[
|
||||
["connery", 1],
|
||||
["miller", 10],
|
||||
["cobalt", 13],
|
||||
["emerald", 17],
|
||||
["jaeger", 19],
|
||||
["soltech", 40],
|
||||
["genudine", 1000],
|
||||
["ceres", 2000],
|
||||
].forEach(([world, id]) => {
|
||||
let worldEvents = events.filter((ev) => ev.worldId === id);
|
||||
allEventsByType(world, worldEvents);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
classes::ClassesQuery, health::HealthQuery, population::PopulationQuery,
|
||||
vehicles::VehicleQuery, world::WorldQuery, zone::ZoneQuery,
|
||||
analytics::AnalyticsQuery, classes::ClassesQuery, health::HealthQuery,
|
||||
population::PopulationQuery, vehicles::VehicleQuery, world::WorldQuery, zone::ZoneQuery,
|
||||
};
|
||||
use async_graphql::MergedObject;
|
||||
|
||||
|
@ -12,4 +12,5 @@ pub struct Query(
|
|||
WorldQuery,
|
||||
ZoneQuery,
|
||||
HealthQuery,
|
||||
AnalyticsQuery,
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue