From 335bd0a2cfb717dc610410b33d7e45dcc362e1fe Mon Sep 17 00:00:00 2001 From: "nise.moe" Date: Sun, 25 Feb 2024 13:09:27 +0100 Subject: [PATCH] Added flag tooltip, fixed returning privileged fields when not admin, added playtime/date UI improvements, show null fields explicitly, etc --- .../nisemoe/nise/search/SearchController.kt | 3 + .../nise/search/SearchSchemaController.kt | 46 +++++----- .../com/nisemoe/nise/search/SearchService.kt | 53 +++++++++--- nise-frontend/src/app/format.ts | 8 +- .../src/app/search/search.component.css | 6 ++ .../src/app/search/search.component.html | 84 +++++++++++++------ .../src/app/search/search.component.ts | 33 ++++---- .../app/view-user/view-user.component.html | 2 +- .../query-builder/query-builder.component.ts | 7 +- .../components/query/query.component.html | 19 ++++- .../components/query/query.component.ts | 25 ++++-- 11 files changed, 194 insertions(+), 92 deletions(-) diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/search/SearchController.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/search/SearchController.kt index 0964161..66b9446 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/search/SearchController.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/search/SearchController.kt @@ -1,5 +1,6 @@ package com.nisemoe.nise.search +import com.fasterxml.jackson.annotation.JsonInclude import jakarta.validation.Valid import jakarta.validation.constraints.Min import jakarta.validation.constraints.NotBlank @@ -25,6 +26,7 @@ class SearchController( } + @JsonInclude(JsonInclude.Include.NON_NULL) data class SearchResponse( val scores: List, val pagination: SearchResponsePagination @@ -41,6 +43,7 @@ class SearchController( } + @JsonInclude(JsonInclude.Include.NON_NULL) data class SearchResponseEntry( // User fields val user_id: Long?, diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/search/SearchSchemaController.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/search/SearchSchemaController.kt index 2a7991b..0391de1 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/search/SearchSchemaController.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/search/SearchSchemaController.kt @@ -20,7 +20,7 @@ class SearchSchemaController( // User fields InternalSchemaField("user_id", "ID", Category.user, Type.number, false, "unique identifier for a user", databaseField = USERS.USER_ID), InternalSchemaField("user_username", "Username", Category.user, Type.string, true, "user's name", databaseField = USERS.USERNAME), - InternalSchemaField("user_join_date", "Join Date", Category.user, Type.string, false, "when the user joined", databaseField = USERS.JOIN_DATE), + InternalSchemaField("user_join_date", "Join Date", Category.user, Type.datetime, false, "when the user joined", databaseField = USERS.JOIN_DATE), InternalSchemaField("user_country", "Country", Category.user, Type.flag, true, "user's country flag", databaseField = USERS.COUNTRY), InternalSchemaField("user_country_rank", "Country Rank", Category.user, Type.number, false, "ranking within user's country", databaseField = USERS.COUNTRY_RANK), InternalSchemaField("user_rank", "Rank", Category.user, Type.number, false, "global ranking", databaseField = USERS.RANK), @@ -29,7 +29,7 @@ class SearchSchemaController( InternalSchemaField("user_playcount", "Playcount", Category.user, Type.number, false, "total plays", databaseField = USERS.PLAYCOUNT), InternalSchemaField("user_total_score", "Total Score", Category.user, Type.number, false, "cumulative score", databaseField = USERS.TOTAL_SCORE), InternalSchemaField("user_ranked_score", "Ranked Score", Category.user, Type.number, false, "score from ranked maps", databaseField = USERS.RANKED_SCORE), - InternalSchemaField("user_seconds_played", "Play Time", Category.user, Type.number, false, "total play time in seconds", databaseField = USERS.SECONDS_PLAYED), + InternalSchemaField("user_seconds_played", "Play Time", Category.user, Type.playtime, false, "total play time in seconds", databaseField = USERS.SECONDS_PLAYED), InternalSchemaField("user_count_300", "300s", Category.user, Type.number, false, "number of 300 hits", databaseField = USERS.COUNT_300), InternalSchemaField("user_count_100", "100s", Category.user, Type.number, false, "number of 100 hits", databaseField = USERS.COUNT_100), InternalSchemaField("user_count_50", "50s", Category.user, Type.number, false, "number of 50 hits", databaseField = USERS.COUNT_50), @@ -42,7 +42,7 @@ class SearchSchemaController( InternalSchemaField("count_100", "100s", Category.score, Type.number, false, "number of 100 hits in score", databaseField = SCORES.COUNT_100), InternalSchemaField("count_50", "50s", Category.score, Type.number, false, "number of 50 hits in score", databaseField = SCORES.COUNT_50), InternalSchemaField("count_miss", "Misses", Category.score, Type.number, false, "missed hits in score", databaseField = SCORES.COUNT_MISS), - InternalSchemaField("date", "Date", Category.score, Type.string, true, "when score was achieved", databaseField = SCORES.DATE), + InternalSchemaField("date", "Date", Category.score, Type.datetime, true, "when score was achieved", databaseField = SCORES.DATE), InternalSchemaField("max_combo", "Max Combo", Category.score, Type.number, false, "highest combo in score", databaseField = SCORES.MAX_COMBO), InternalSchemaField("mods", "Mods", Category.score, Type.number, false, "game modifiers used", databaseField = SCORES.MODS), InternalSchemaField("perfect", "Perfect", Category.score, Type.boolean, false, "if score is a full combo", databaseField = SCORES.PERFECT), @@ -50,24 +50,24 @@ class SearchSchemaController( InternalSchemaField("rank", "Rank", Category.score, Type.grade, false, "score grade", databaseField = SCORES.RANK), InternalSchemaField("replay_id", "Replay ID", Category.score, Type.number, false, "identifier for replay", databaseField = SCORES.REPLAY_ID), InternalSchemaField("score", "Score", Category.score, Type.number, false, "score value", databaseField = SCORES.SCORE), - InternalSchemaField("ur", "UR", Category.score, Type.number, false, "unstable rate", databaseField = SCORES.UR), - InternalSchemaField("frametime", "Frame Time", Category.score, Type.number, false, "median frame time during play", databaseField = SCORES.FRAMETIME), - InternalSchemaField("edge_hits", "Edge Hits", Category.score, Type.number, false, "hits at the edge of the hitobject (<1px)", databaseField = SCORES.EDGE_HITS), - InternalSchemaField("snaps", "Snaps", Category.score, Type.number, false, "rapid cursor movements", databaseField = SCORES.SNAPS), - InternalSchemaField("adjusted_ur", "Adj. UR", Category.score, Type.number, true, "adjusted unstable rate", databaseField = SCORES.ADJUSTED_UR), - InternalSchemaField("mean_error", "Mean Error", Category.score, Type.number, false, "average timing error", databaseField = SCORES.MEAN_ERROR), - InternalSchemaField("error_variance", "Error Var.", Category.score, Type.number, false, "variability of error in scores", databaseField = SCORES.ERROR_VARIANCE), - InternalSchemaField("error_standard_deviation", "Error SD", Category.score, Type.number, false, "standard deviation of error", databaseField = SCORES.ERROR_STANDARD_DEVIATION), - InternalSchemaField("minimum_error", "Min Error", Category.score, Type.number, false, "smallest error recorded", databaseField = SCORES.MINIMUM_ERROR), - InternalSchemaField("maximum_error", "Max Error", Category.score, Type.number, false, "largest error recorded", databaseField = SCORES.MAXIMUM_ERROR), - InternalSchemaField("error_range", "Error Range", Category.score, Type.number, false, "range between min and max error", databaseField = SCORES.ERROR_RANGE), - InternalSchemaField("error_coefficient_of_variation", "Error CV", Category.score, Type.number, false, "relative variability of error", databaseField = SCORES.ERROR_COEFFICIENT_OF_VARIATION), - InternalSchemaField("error_kurtosis", "Kurtosis", Category.score, Type.number, false, "peakedness of error distribution", databaseField = SCORES.ERROR_KURTOSIS), - InternalSchemaField("error_skewness", "Skewness", Category.score, Type.number, false, "asymmetry of error distribution", databaseField = SCORES.ERROR_SKEWNESS), - InternalSchemaField("keypresses_median_adjusted", "KP Median Adj.", Category.score, Type.number, false, "median of adjusted keypresses", isPrivileged = true, databaseField = SCORES.KEYPRESSES_MEDIAN_ADJUSTED), - InternalSchemaField("keypresses_standard_deviation_adjusted", "KP std. Adj.", Category.score, Type.number, false, "std. dev of adjusted keypresses", isPrivileged = true, databaseField = SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED), - InternalSchemaField("sliderend_release_median_adjusted", "Sliderend Median Adj.", Category.score, Type.number, false, "median of adjusted sliderend releases", isPrivileged = true, databaseField = SCORES.SLIDEREND_RELEASE_MEDIAN_ADJUSTED), - InternalSchemaField("sliderend_release_standard_deviation_adjusted", "Sliderend std. Adj.", Category.score, Type.number, false, "std. dev of adjusted sliderend releases", isPrivileged = true, databaseField = SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED), + InternalSchemaField("ur", "UR", Category.metrics, Type.number, false, "unstable rate", databaseField = SCORES.UR), + InternalSchemaField("frametime", "Frame Time", Category.metrics, Type.number, false, "median frame time during play", databaseField = SCORES.FRAMETIME), + InternalSchemaField("edge_hits", "Edge Hits", Category.metrics, Type.number, false, "hits at the edge of the hitobject (<1px)", databaseField = SCORES.EDGE_HITS), + InternalSchemaField("snaps", "Snaps", Category.metrics, Type.number, false, "rapid cursor movements", databaseField = SCORES.SNAPS), + InternalSchemaField("adjusted_ur", "Adj. UR", Category.metrics, Type.number, true, "adjusted unstable rate", databaseField = SCORES.ADJUSTED_UR), + InternalSchemaField("mean_error", "Mean Error", Category.metrics, Type.number, false, "average timing error", databaseField = SCORES.MEAN_ERROR), + InternalSchemaField("error_variance", "Error Var.", Category.metrics, Type.number, false, "variability of error in scores", databaseField = SCORES.ERROR_VARIANCE), + InternalSchemaField("error_standard_deviation", "Error SD", Category.metrics, Type.number, false, "standard deviation of error", databaseField = SCORES.ERROR_STANDARD_DEVIATION), + InternalSchemaField("minimum_error", "Min Error", Category.metrics, Type.number, false, "smallest error recorded", databaseField = SCORES.MINIMUM_ERROR), + InternalSchemaField("maximum_error", "Max Error", Category.metrics, Type.number, false, "largest error recorded", databaseField = SCORES.MAXIMUM_ERROR), + InternalSchemaField("error_range", "Error Range", Category.metrics, Type.number, false, "range between min and max error", databaseField = SCORES.ERROR_RANGE), + InternalSchemaField("error_coefficient_of_variation", "Error CV", Category.metrics, Type.number, false, "relative variability of error", databaseField = SCORES.ERROR_COEFFICIENT_OF_VARIATION), + InternalSchemaField("error_kurtosis", "Kurtosis", Category.metrics, Type.number, false, "peakedness of error distribution", databaseField = SCORES.ERROR_KURTOSIS), + InternalSchemaField("error_skewness", "Skewness", Category.metrics, Type.number, false, "asymmetry of error distribution", databaseField = SCORES.ERROR_SKEWNESS), + InternalSchemaField("keypresses_median_adjusted", "KP Median Adj.", Category.metrics, Type.number, false, "median of adjusted keypresses", isPrivileged = true, databaseField = SCORES.KEYPRESSES_MEDIAN_ADJUSTED), + InternalSchemaField("keypresses_standard_deviation_adjusted", "KP std. Adj.", Category.metrics, Type.number, false, "std. dev of adjusted keypresses", isPrivileged = true, databaseField = SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED), + InternalSchemaField("sliderend_release_median_adjusted", "Sliderend Median Adj.", Category.metrics, Type.number, false, "median of adjusted sliderend releases", isPrivileged = true, databaseField = SCORES.SLIDEREND_RELEASE_MEDIAN_ADJUSTED), + InternalSchemaField("sliderend_release_standard_deviation_adjusted", "Sliderend std. Adj.", Category.metrics, Type.number, false, "std. dev of adjusted sliderend releases", isPrivileged = true, databaseField = SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED), // Beatmap fields InternalSchemaField("beatmap_artist", "Artist", Category.beatmap, Type.string, false, "artist of the beatmap", databaseField = BEATMAPS.ARTIST), @@ -103,11 +103,11 @@ class SearchSchemaController( ) enum class Category { - user, score, beatmap + user, score, beatmap, metrics } enum class Type { - number, string, flag, grade, boolean + number, string, flag, grade, boolean, datetime, playtime } data class SearchSchema( diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/search/SearchService.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/search/SearchService.kt index 8b8c0c7..68688fd 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/search/SearchService.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/search/SearchService.kt @@ -4,6 +4,7 @@ import com.nisemoe.generated.tables.references.BEATMAPS import com.nisemoe.generated.tables.references.SCORES import com.nisemoe.generated.tables.references.USERS import com.nisemoe.nise.Format +import com.nisemoe.nise.service.AuthService import org.jooq.* import org.jooq.impl.DSL import org.springframework.stereotype.Service @@ -11,7 +12,8 @@ import kotlin.math.roundToInt @Service class SearchService( - private val dslContext: DSLContext + private val dslContext: DSLContext, + private val authService: AuthService ) { fun search(request: SearchController.SearchRequest): SearchController.SearchResponse { @@ -25,8 +27,8 @@ class SearchService( } } - val results = dslContext.select( - // User fields + val selectFields = mutableListOf>( + // Common fields list USERS.USERNAME, USERS.USER_ID, USERS.JOIN_DATE, @@ -72,10 +74,6 @@ class SearchService( SCORES.ERROR_COEFFICIENT_OF_VARIATION, SCORES.ERROR_KURTOSIS, SCORES.ERROR_SKEWNESS, - SCORES.KEYPRESSES_MEDIAN_ADJUSTED, - SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED, - SCORES.SLIDEREND_RELEASE_MEDIAN_ADJUSTED, - SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED, // Beatmaps fields BEATMAPS.ARTIST, @@ -86,6 +84,21 @@ class SearchService( BEATMAPS.TITLE, BEATMAPS.VERSION ) + + // Conditionally add admin-specific fields + if (authService.isAdmin()) { + selectFields.addAll( + listOf( + SCORES.KEYPRESSES_MEDIAN_ADJUSTED, + SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED, + SCORES.SLIDEREND_RELEASE_MEDIAN_ADJUSTED, + SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED + ) + ) + } + + val query = dslContext + .select(selectFields) .from(SCORES) .join(USERS).on(SCORES.USER_ID.eq(USERS.USER_ID)) .join(BEATMAPS).on(SCORES.BEATMAP_ID.eq(BEATMAPS.BEATMAP_ID)) @@ -96,8 +109,12 @@ class SearchService( } .offset((request.page - 1) * SearchController.RESULTS_PER_PAGE) .limit(SearchController.RESULTS_PER_PAGE) + + val results = query .fetch() + println(query.toString()) + // Get total results val totalResults = dslContext.selectCount() .from(SCORES) @@ -166,10 +183,12 @@ class SearchService( error_coefficient_of_variation = it.get(SCORES.ERROR_COEFFICIENT_OF_VARIATION), error_kurtosis = it.get(SCORES.ERROR_KURTOSIS), error_skewness = it.get(SCORES.ERROR_SKEWNESS), - keypresses_median_adjusted = it.get(SCORES.KEYPRESSES_MEDIAN_ADJUSTED), - keypresses_standard_deviation_adjusted = it.get(SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED), - sliderend_release_median_adjusted = it.get(SCORES.SLIDEREND_RELEASE_MEDIAN_ADJUSTED), - sliderend_release_standard_deviation_adjusted = it.get(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED), + + // Admin-specific fields, conditional based on isAdmin + keypresses_median_adjusted = if (authService.isAdmin()) it.get(SCORES.KEYPRESSES_MEDIAN_ADJUSTED) else null, + keypresses_standard_deviation_adjusted = if (authService.isAdmin()) it.get(SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED) else null, + sliderend_release_median_adjusted = if (authService.isAdmin()) it.get(SCORES.SLIDEREND_RELEASE_MEDIAN_ADJUSTED) else null, + sliderend_release_standard_deviation_adjusted = if (authService.isAdmin()) it.get(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED) else null, // Beatmap fields beatmap_artist = it.get(BEATMAPS.ARTIST), @@ -199,7 +218,7 @@ class SearchService( query.childQueries.forEach { childQuery -> val childCondition = buildCondition(childQuery) // Recursively build condition for child queries - baseCondition = when (query.logicalOperator.lowercase()) { + baseCondition = when (childQuery.logicalOperator.lowercase()) { "and" -> baseCondition.and(childCondition) "or" -> baseCondition.or(childCondition) else -> throw IllegalArgumentException("Invalid logical operator") @@ -226,6 +245,8 @@ class SearchService( "boolean" -> buildBooleanCondition(field as Field, predicate.operator.operatorType, predicate.value.toBoolean()) "flag" -> buildStringCondition(field as Field, predicate.operator.operatorType, predicate.value) "grade" -> buildGradeCondition(field as Field, predicate.operator.operatorType, predicate.value) + "datetime" -> buildDatetimeCondition(field as Field, predicate.operator.operatorType, predicate.value) + "playtime" -> buildNumberCondition(field as Field, predicate.operator.operatorType, predicate.value.toDouble()) else -> throw IllegalArgumentException("Invalid field type") } } @@ -288,4 +309,12 @@ class SearchService( } } + private fun buildDatetimeCondition(field: Field, operator: String, value: String): Condition { + return when (operator.lowercase()) { + "before" -> field.lessThan(value) + "after" -> field.greaterThan(value) + else -> throw IllegalArgumentException("Invalid operator") + } + } + } \ No newline at end of file diff --git a/nise-frontend/src/app/format.ts b/nise-frontend/src/app/format.ts index 176b28b..98243bf 100644 --- a/nise-frontend/src/app/format.ts +++ b/nise-frontend/src/app/format.ts @@ -1,6 +1,12 @@ import {ReplayData} from "./replays"; -export function formatDuration(seconds: number): string { +export function formatDuration(seconds: number): string | null { + if(!seconds) { + return null; + } + + console.log(seconds); + const days = Math.floor(seconds / (3600 * 24)); const hours = Math.floor((seconds % (3600 * 24)) / 3600); const minutes = Math.floor((seconds % 3600) / 60); diff --git a/nise-frontend/src/app/search/search.component.css b/nise-frontend/src/app/search/search.component.css index 97cf837..084322b 100644 --- a/nise-frontend/src/app/search/search.component.css +++ b/nise-frontend/src/app/search/search.component.css @@ -19,3 +19,9 @@ .score-entry:hover { background-color: rgba(179, 184, 195, 0.15); } + +code { + background-color: rgba(227, 232, 255, 0.1); + padding: 2px; + border-radius: 3px; +} diff --git a/nise-frontend/src/app/search/search.component.html b/nise-frontend/src/app/search/search.component.html index 0b68a60..bf89baf 100644 --- a/nise-frontend/src/app/search/search.component.html +++ b/nise-frontend/src/app/search/search.component.html @@ -9,7 +9,7 @@
Table columns - +
{{ category }} @@ -26,18 +26,27 @@
- +
sorting