Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ actix-cors = "0.7.1"
sqlx = { version = "0.8", features = [ "runtime-tokio", "tls-native-tls", "postgres", "chrono" ] }
serde = { version = "1.0", features = ["derive"] }
serde_json = {version = "1.0", features = ["preserve_order"]}
url = "2.5"
chrono = { version = "0.4", features = ["serde"] }
log = "0.4"
futures = "0.3"
Expand Down
4 changes: 2 additions & 2 deletions backend/src/api_doc.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use utoipa::OpenApi;

use crate::handlers::user_stats::get_user_stats::GetUserStatsQuery;
use crate::handlers::user_stats::get_user_stats::GetUserStatsQueryParams;

#[derive(OpenApi)]
#[openapi(
Expand All @@ -15,6 +15,6 @@ use crate::handlers::user_stats::get_user_stats::GetUserStatsQuery;
crate::handlers::user_stats::get_user_stats::exec,
crate::handlers::user_stats::get_user_stats_prometheus::exec,
),
components(schemas(GetUserStatsQuery),)
components(schemas(GetUserStatsQueryParams),)
)]
pub struct ApiDoc;
50 changes: 36 additions & 14 deletions backend/src/handlers/user_stats/get_user_stats.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
use crate::{Dasharr, error::Result, models::user_stats::UserProfileVec};
use actix_web::{
HttpResponse,
web::{Data, Query},
};
use crate::{Dasharr, models::user_stats::UserProfileVec};

Check warning on line 1 in backend/src/handlers/user_stats/get_user_stats.rs

View workflow job for this annotation

GitHub Actions / cargo fmt

Diff in /home/runner/work/Dasharr/Dasharr/backend/src/handlers/user_stats/get_user_stats.rs
use actix_web::web::{Data, Query};
use actix_web::HttpResponse;
use chrono::NaiveDateTime;
use serde::Deserialize;
use std::collections::HashMap;
use utoipa::{IntoParams, ToSchema};

#[derive(Debug, Deserialize, IntoParams, ToSchema)]
pub struct GetUserStatsQuery {
pub indexer_id: i64,
pub struct GetUserStatsQueryParams {
pub indexer_ids: Vec<i64>,

#[param(value_type = String, format = DateTime)]
pub date_from: NaiveDateTime,

#[param(value_type = String, format = DateTime)]
pub date_to: NaiveDateTime,
}
Expand All @@ -21,18 +22,39 @@
operation_id = "Get user stats",
tag = "User stats",
path = "/api/user-stats",
params(GetUserStatsQuery),
params(GetUserStatsQueryParams),
responses(
(status = 200, description = "Successfully got user stats", body=UserProfileVec),
(status = 200, description = "Successfully got user stats", body=HashMap<i32, UserProfileVec>),
)
)]
pub async fn exec(arc: Data<Dasharr>, query: Query<GetUserStatsQuery>) -> Result<HttpResponse> {
pub async fn exec(
arc: Data<Dasharr>,
query: Query<GetUserStatsQueryParams>,
) -> std::result::Result<HttpResponse, actix_web::Error> {
let query = query.into_inner();

if query.indexer_ids.is_empty() {
return Err(actix_web::error::ErrorBadRequest("indexer_ids is required"));
}

let user_stats = arc
.pool
.find_user_stats(query.indexer_id, &query.date_from, &query.date_to)
.find_user_stats(&query.indexer_ids, &query.date_from, &query.date_to)
.await?;
// let test: UserProfileVec = user_stats.into();
let user_stats_reduced = UserProfileVec::from_vec(user_stats);

Ok(HttpResponse::Ok().json(user_stats_reduced))
let mut grouped_profiles: HashMap<i32, Vec<crate::models::user_stats::UserProfile>> =
HashMap::new();
for profile in user_stats {
grouped_profiles
.entry(profile.indexer_id)
.or_default()
.push(profile);
}

let grouped_stats: HashMap<i32, UserProfileVec> = grouped_profiles
.into_iter()
.map(|(indexer_id, profiles)| (indexer_id, UserProfileVec::from_vec(profiles)))
.collect();

Ok(HttpResponse::Ok().json(grouped_stats))
}
8 changes: 4 additions & 4 deletions backend/src/repositories/user_stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ impl ConnectionPool {

pub async fn find_user_stats(
&self,
indexer_id: i64,
indexer_ids: &[i64],
date_from: &NaiveDateTime,
date_to: &NaiveDateTime,
) -> Result<Vec<UserProfile>> {
Expand All @@ -88,11 +88,11 @@ impl ConnectionPool {
let indexers: Vec<UserProfile> = sqlx::query_as(
r#"
SELECT * FROM user_profiles
WHERE indexer_id = $1 AND scraped_at BETWEEN $2 AND $3
ORDER BY scraped_at ASC
WHERE indexer_id = ANY($1) AND scraped_at BETWEEN $2 AND $3
ORDER BY indexer_id, scraped_at ASC
"#,
)
.bind(indexer_id)
.bind(indexer_ids)
.bind(date_from)
.bind(date_to)
.fetch_all(self.borrow())
Expand Down
17 changes: 17 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 41 additions & 24 deletions frontend/src/components/user-stats/IndexerStatsEvolutions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@
</div>
</template>
<script lang="ts" setup>
import type { UserProfileVec, UserProfileScrapedVec } from '@/services/api/userStatsService'
import type { MultiIndexerUserStats, UserProfileScrapedVec } from '@/services/api/userStatsService'
import type { IndexerEnriched } from '@/services/api/indexerService'
import Chart from 'primevue/chart'
import ContentContainer from '../ContentContainer.vue'
import 'chartjs-adapter-date-fns'
import { getTrackerColor } from '@/utils/trackerColors'

const props = defineProps<{
userStats: UserProfileVec
userStats: MultiIndexerUserStats
indexerMetadata: Record<number, IndexerEnriched>
selectedValues: (keyof UserProfileScrapedVec)[]
}>()
const documentStyle = getComputedStyle(document.documentElement)

const chartOptions = (value: keyof UserProfileScrapedVec) => {
let unit = ''
Expand Down Expand Up @@ -50,28 +52,43 @@ const chartOptions = (value: keyof UserProfileScrapedVec) => {
}

const chartData = (value: keyof UserProfileScrapedVec) => {
let data: number[] = []
switch (value) {
case 'uploaded':
case 'downloaded':
case 'seed_size':
case 'uploaded_real':
case 'downloaded_real':
data = props.userStats.profile[value].map((val) => (val ?? 0) / 1024 / 1024 / 1024)
break
default:
data = props.userStats.profile[value] as number[]
}
const datasets = Object.entries(props.userStats).map(([indexerIdStr, userStatsVec]) => {
const indexerId = parseInt(indexerIdStr)
const indexer = props.indexerMetadata[indexerId]
const trackerName = indexer?.name || `Tracker ${indexerId}`
const color = getTrackerColor(trackerName)

const rawData = userStatsVec.profile[value]
let processedData: number[] = []
switch (value) {
case 'uploaded':
case 'downloaded':
processedData = (rawData as number[]).map((val) => val / 1024 / 1024 / 1024)
break
case 'seed_size':
case 'uploaded_real':
case 'downloaded_real':
processedData = (rawData as (number | null)[]).map((val) => ((val ?? 0) as number) / 1024 / 1024 / 1024)
break
default:
processedData = rawData as number[]
}

const dataPoints = processedData.map((val, idx) => ({
x: new Date(userStatsVec.scraped_at[idx]).toISOString(),
y: val,
}))

return {
borderColor: color,
label: `${value} - ${trackerName}`,
data: dataPoints,
tension: 0.2,
}
})

return {
labels: props.userStats.scraped_at.map((date) => new Date(date).toISOString()),
datasets: [
{
// tension: 0.2,
borderColor: documentStyle.getPropertyValue('--p-button-primary-border-color'),
label: value,
data: data,
},
],
datasets: datasets,
}
}
</script>
Expand Down
18 changes: 14 additions & 4 deletions frontend/src/components/user-stats/IndexerStatsIncreases.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,28 @@
</div>
</template>
<script lang="ts" setup>
import type { UserProfileScrapedVec, UserProfileVec } from '@/services/api/userStatsService'
import type { UserProfileScrapedVec, MultiIndexerUserStats } from '@/services/api/userStatsService'
import ContentContainer from '../ContentContainer.vue'
import { bytesToReadable } from '@/services/helpers'

const props = defineProps<{
userStats: UserProfileVec
userStats: MultiIndexerUserStats
selectedValues: (keyof UserProfileScrapedVec)[]
}>()

const postProcessStat = (value: keyof UserProfileScrapedVec) => {
let result: number | string =
((props.userStats.profile[value]?.[props.userStats.profile[value].length - 1] as number) ?? 0) - ((props.userStats.profile[value]?.[0] as number) ?? 0)
let totalIncrease = 0

Object.values(props.userStats).forEach((userStatsVec) => {
const profileArray = userStatsVec.profile[value]
if (profileArray && profileArray.length > 0) {
const first = (profileArray[0] as number) ?? 0
const last = (profileArray[profileArray.length - 1] as number) ?? 0
totalIncrease += last - first
}
})

let result: number | string = totalIncrease
switch (value) {
case 'uploaded':
case 'downloaded':
Expand Down
Loading
Loading