This commit is contained in:
+13
-1
@@ -1549,6 +1549,8 @@ async fn analytics_events(
|
|||||||
let query_str = request.uri().query().unwrap_or("");
|
let query_str = request.uri().query().unwrap_or("");
|
||||||
let mut before: Option<i64> = None;
|
let mut before: Option<i64> = None;
|
||||||
let mut limit: usize = 40;
|
let mut limit: usize = 40;
|
||||||
|
let mut types: Vec<String> = Vec::new();
|
||||||
|
let mut types_present = false;
|
||||||
for pair in query_str.split('&') {
|
for pair in query_str.split('&') {
|
||||||
if let Some(v) = pair.strip_prefix("before=") {
|
if let Some(v) = pair.strip_prefix("before=") {
|
||||||
before = v.parse().ok();
|
before = v.parse().ok();
|
||||||
@@ -1556,10 +1558,20 @@ async fn analytics_events(
|
|||||||
&& let Ok(n) = v.parse::<usize>()
|
&& let Ok(n) = v.parse::<usize>()
|
||||||
{
|
{
|
||||||
limit = n.clamp(1, 100);
|
limit = n.clamp(1, 100);
|
||||||
|
} else if let Some(v) = pair.strip_prefix("types=") {
|
||||||
|
types_present = true;
|
||||||
|
if !v.is_empty() {
|
||||||
|
types.push(v.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let page = crate::analytics::recent_events(&db, before, limit).await?;
|
let allowed = if types_present {
|
||||||
|
Some(types.as_slice())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let page = crate::analytics::recent_events(&db, before, limit, allowed).await?;
|
||||||
let next_cursor = if page.len() == limit {
|
let next_cursor = if page.len() == limit {
|
||||||
page.last().map(|e| e.id.unwrap())
|
page.last().map(|e| e.id.unwrap())
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -344,14 +344,20 @@ pub async fn media_viewed_recently(
|
|||||||
/// The ORM exposes neither `ORDER BY` nor raw row reads, so ordering and slicing
|
/// The ORM exposes neither `ORDER BY` nor raw row reads, so ordering and slicing
|
||||||
/// happen in Rust. This is fine for the expected event volume of this site; if
|
/// happen in Rust. This is fine for the expected event volume of this site; if
|
||||||
/// the table ever grows very large, add a descending index and raw SQL here.
|
/// the table ever grows very large, add a descending index and raw SQL here.
|
||||||
|
/// `types`: `None` returns all event types; `Some(list)` returns only those
|
||||||
|
/// (an empty list therefore returns nothing).
|
||||||
pub async fn recent_events(
|
pub async fn recent_events(
|
||||||
db: &Database,
|
db: &Database,
|
||||||
before: Option<i64>,
|
before: Option<i64>,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
|
types: Option<&[String]>,
|
||||||
) -> cot::Result<Vec<AnalyticsEvent>> {
|
) -> cot::Result<Vec<AnalyticsEvent>> {
|
||||||
// `Auto<i64>` is not `Ord`, so the id cursor can't be pushed into the query;
|
// `Auto<i64>` is not `Ord`, so the id cursor can't be pushed into the query;
|
||||||
// filter and sort in Rust instead (acceptable for this site's volume).
|
// filter and sort in Rust instead (acceptable for this site's volume).
|
||||||
let mut events = AnalyticsEvent::objects().all(db).await?;
|
let mut events = AnalyticsEvent::objects().all(db).await?;
|
||||||
|
if let Some(types) = types {
|
||||||
|
events.retain(|e| types.iter().any(|t| t == &e.event_type));
|
||||||
|
}
|
||||||
events.sort_by_key(|e| std::cmp::Reverse(e.id.unwrap()));
|
events.sort_by_key(|e| std::cmp::Reverse(e.id.unwrap()));
|
||||||
Ok(events
|
Ok(events
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ pub struct Translations {
|
|||||||
pub analytics_summary_period: &'static str,
|
pub analytics_summary_period: &'static str,
|
||||||
pub analytics_open_dashboard: &'static str,
|
pub analytics_open_dashboard: &'static str,
|
||||||
pub analytics_log_title: &'static str,
|
pub analytics_log_title: &'static str,
|
||||||
|
pub analytics_log_show: &'static str,
|
||||||
pub analytics_log_empty: &'static str,
|
pub analytics_log_empty: &'static str,
|
||||||
pub analytics_log_end: &'static str,
|
pub analytics_log_end: &'static str,
|
||||||
pub analytics_log_visitor: &'static str,
|
pub analytics_log_visitor: &'static str,
|
||||||
@@ -537,6 +538,7 @@ static RU: Translations = Translations {
|
|||||||
analytics_summary_period: "за последние 30 дней",
|
analytics_summary_period: "за последние 30 дней",
|
||||||
analytics_open_dashboard: "Открыть подробную статистику",
|
analytics_open_dashboard: "Открыть подробную статистику",
|
||||||
analytics_log_title: "Последние события",
|
analytics_log_title: "Последние события",
|
||||||
|
analytics_log_show: "Показывать",
|
||||||
analytics_log_empty: "Событий пока нет.",
|
analytics_log_empty: "Событий пока нет.",
|
||||||
analytics_log_end: "Это все события.",
|
analytics_log_end: "Это все события.",
|
||||||
analytics_log_visitor: "Посетитель",
|
analytics_log_visitor: "Посетитель",
|
||||||
@@ -848,6 +850,7 @@ static EN: Translations = Translations {
|
|||||||
analytics_summary_period: "over the last 30 days",
|
analytics_summary_period: "over the last 30 days",
|
||||||
analytics_open_dashboard: "Open detailed analytics",
|
analytics_open_dashboard: "Open detailed analytics",
|
||||||
analytics_log_title: "Recent events",
|
analytics_log_title: "Recent events",
|
||||||
|
analytics_log_show: "Show",
|
||||||
analytics_log_empty: "No events yet.",
|
analytics_log_empty: "No events yet.",
|
||||||
analytics_log_end: "That's all events.",
|
analytics_log_end: "That's all events.",
|
||||||
analytics_log_visitor: "Visitor",
|
analytics_log_visitor: "Visitor",
|
||||||
|
|||||||
@@ -84,7 +84,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="analytics-log-card">
|
<div class="analytics-log-card">
|
||||||
<h2>{{ t.analytics_log_title }}</h2>
|
<div class="analytics-log-head">
|
||||||
|
<h2>{{ t.analytics_log_title }}</h2>
|
||||||
|
<div class="analytics-metric-toggles analytics-log-filters">
|
||||||
|
<span class="toggles-label">{{ t.analytics_log_show }}:</span>
|
||||||
|
<label><input type="checkbox" class="log-type-toggle" data-type="landing_view"> {{ t.analytics_ev_landing_view }}</label>
|
||||||
|
<label><input type="checkbox" class="log-type-toggle" data-type="portal_open" checked> {{ t.analytics_ev_portal_open }}</label>
|
||||||
|
<label><input type="checkbox" class="log-type-toggle" data-type="media_view" checked> {{ t.analytics_ev_media_view }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="analytics-log" id="anLog"></div>
|
<div class="analytics-log" id="anLog"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -117,7 +125,9 @@
|
|||||||
.an-media-missing { width:40px; height:40px; border-radius:6px; background:#f0f0f3; display:inline-flex; align-items:center; justify-content:center; color:#bbb; flex:0 0 auto; }
|
.an-media-missing { width:40px; height:40px; border-radius:6px; background:#f0f0f3; display:inline-flex; align-items:center; justify-content:center; color:#bbb; flex:0 0 auto; }
|
||||||
|
|
||||||
.analytics-log-card { background:#fff; border-radius:12px; padding:16px; box-shadow:0 1px 4px rgba(0,0,0,.06); margin-top:20px; }
|
.analytics-log-card { background:#fff; border-radius:12px; padding:16px; box-shadow:0 1px 4px rgba(0,0,0,.06); margin-top:20px; }
|
||||||
.analytics-log-card h2 { font-size:1rem; margin:0 0 10px; }
|
.analytics-log-card h2 { font-size:1rem; margin:0; }
|
||||||
|
.analytics-log-head { display:flex; flex-wrap:wrap; gap:10px 18px; align-items:center; justify-content:space-between; margin-bottom:10px; }
|
||||||
|
.analytics-log-filters { margin-bottom:0; }
|
||||||
.analytics-log { max-height:460px; overflow-y:auto; border:1px solid #f0f0f0; border-radius:8px; }
|
.analytics-log { max-height:460px; overflow-y:auto; border:1px solid #f0f0f0; border-radius:8px; }
|
||||||
.an-log-row { display:flex; gap:12px; align-items:flex-start; padding:10px 12px; border-bottom:1px solid #f5f5f5; font-size:.85rem; }
|
.an-log-row { display:flex; gap:12px; align-items:flex-start; padding:10px 12px; border-bottom:1px solid #f5f5f5; font-size:.85rem; }
|
||||||
.an-log-row:last-child { border-bottom:none; }
|
.an-log-row:last-child { border-bottom:none; }
|
||||||
@@ -496,13 +506,36 @@
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectedLogTypes() {
|
||||||
|
var types = [];
|
||||||
|
document.querySelectorAll('.log-type-toggle').forEach(function (cb) {
|
||||||
|
if (cb.checked) { types.push(cb.getAttribute('data-type')); }
|
||||||
|
});
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetLog() {
|
||||||
|
logEl.replaceChildren();
|
||||||
|
logCursor = null;
|
||||||
|
logLoading = false;
|
||||||
|
logDone = false;
|
||||||
|
loadLog();
|
||||||
|
}
|
||||||
|
|
||||||
function loadLog() {
|
function loadLog() {
|
||||||
if (logLoading || logDone) { return; }
|
if (logLoading || logDone) { return; }
|
||||||
|
var types = selectedLogTypes();
|
||||||
|
if (types.length === 0) {
|
||||||
|
logEl.replaceChildren(logStatus(L.log_empty));
|
||||||
|
logDone = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
logLoading = true;
|
logLoading = true;
|
||||||
var busy = logStatus(L.loading);
|
var busy = logStatus(L.loading);
|
||||||
logEl.appendChild(busy);
|
logEl.appendChild(busy);
|
||||||
var params = new URLSearchParams({ limit: '40' });
|
var params = new URLSearchParams({ limit: '40' });
|
||||||
if (logCursor) { params.set('before', logCursor); }
|
if (logCursor) { params.set('before', logCursor); }
|
||||||
|
types.forEach(function (t) { params.append('types', t); });
|
||||||
fetch('/admin/analytics/events?' + params.toString(), { credentials: 'same-origin' })
|
fetch('/admin/analytics/events?' + params.toString(), { credentials: 'same-origin' })
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
@@ -534,6 +567,10 @@
|
|||||||
if (logEl.scrollTop + logEl.clientHeight >= logEl.scrollHeight - 40) { loadLog(); }
|
if (logEl.scrollTop + logEl.clientHeight >= logEl.scrollHeight - 40) { loadLog(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.log-type-toggle').forEach(function (cb) {
|
||||||
|
cb.addEventListener('change', resetLog);
|
||||||
|
});
|
||||||
|
|
||||||
initChart();
|
initChart();
|
||||||
setRange(30);
|
setRange(30);
|
||||||
load();
|
load();
|
||||||
|
|||||||
Reference in New Issue
Block a user