From b1635fd79cb0268c44ad0b360865359c8ebe5e65 Mon Sep 17 00:00:00 2001 From: "nise.moe" Date: Sat, 24 Feb 2024 18:21:27 +0100 Subject: [PATCH] Added link to advanced search in header, refactor, provide schema from backend --- .../nise/controller/SearchController.kt | 94 ++++++++++++- nise-frontend/src/app/app.component.html | 5 +- .../src/app/search/search.component.html | 130 ++++++++++-------- .../src/app/search/search.component.ts | 88 ++++-------- .../components/query/query.component.html | 2 +- .../components/query/query.component.ts | 7 - 6 files changed, 190 insertions(+), 136 deletions(-) diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/SearchController.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/SearchController.kt index 5da9ba9..73f41cf 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/SearchController.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/SearchController.kt @@ -13,6 +13,7 @@ import org.jooq.Record import org.jooq.Result import org.jooq.impl.DSL import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader @@ -143,11 +144,98 @@ class SearchController( val type: String ) + data class SchemaField( + val name: String, + val shortName: String, + val category: Category, + val type: Type, + val active: Boolean, + val description: String + ) + + // Define the Category and Type enums + enum class Category { + user, score, beatmap + } + + enum class Type { + number, string, flag, grade, boolean + } + + + data class SearchSchema( + val fields: List + ) + + @GetMapping("search") + fun getSearchSchema(): ResponseEntity { + val fields = listOf( + // User fields + SchemaField("user_id", "ID", Category.user, Type.number, true, "unique identifier for a user"), + SchemaField("user_username", "Username", Category.user, Type.string, true, "user's name"), + SchemaField("user_join_date", "Join Date", Category.user, Type.string, true, "when the user joined"), + SchemaField("user_country", "Country", Category.user, Type.flag, true, "user's country flag"), + SchemaField("user_country_rank", "Country Rank", Category.user, Type.number, true, "ranking within user's country"), + SchemaField("user_rank", "Rank", Category.user, Type.number, true, "global ranking"), + SchemaField("user_pp_raw", "User PP", Category.user, Type.number, true, "performance points"), + SchemaField("user_accuracy", "User Accuracy", Category.user, Type.number, true, "hit accuracy percentage"), + SchemaField("user_playcount", "Playcount", Category.user, Type.number, true, "total plays"), + SchemaField("user_total_score", "Total Score", Category.user, Type.number, true, "cumulative score"), + SchemaField("user_ranked_score", "Ranked Score", Category.user, Type.number, true, "score from ranked maps"), + SchemaField("user_seconds_played", "Play Time", Category.user, Type.number, true, "total play time in seconds"), + SchemaField("user_count_300", "300s", Category.user, Type.number, true, "number of 300 hits"), + SchemaField("user_count_100", "100s", Category.user, Type.number, true, "number of 100 hits"), + SchemaField("user_count_50", "50s", Category.user, Type.number, true, "number of 50 hits"), + SchemaField("user_count_miss", "Misses", Category.user, Type.number, true, "missed hits"), + + // Score fields + SchemaField("beatmap_id", "Beatmap ID", Category.score, Type.number, true, "identifies the beatmap"), + SchemaField("count_300", "300s", Category.score, Type.number, true, "number of 300 hits in score"), + SchemaField("count_100", "100s", Category.score, Type.number, true, "number of 100 hits in score"), + SchemaField("count_50", "50s", Category.score, Type.number, true, "number of 50 hits in score"), + SchemaField("count_miss", "Misses", Category.score, Type.number, true, "missed hits in score"), + SchemaField("date", "Date", Category.score, Type.number, true, "when score was achieved"), + SchemaField("max_combo", "Max Combo", Category.score, Type.number, true, "highest combo in score"), + SchemaField("mods", "Mods", Category.score, Type.number, true, "game modifiers used"), + SchemaField("perfect", "Perfect", Category.score, Type.boolean, true, "if score is a full combo"), + SchemaField("pp", "Score PP", Category.score, Type.number, true, "performance points for score"), + SchemaField("rank", "Rank", Category.score, Type.grade, true, "score grade"), + SchemaField("replay_id", "Replay ID", Category.score, Type.number, true, "identifier for replay"), + SchemaField("score", "Score", Category.score, Type.number, true, "score value"), + SchemaField("ur", "UR", Category.score, Type.number, true, "unstable rate"), + SchemaField("frametime", "Frame Time", Category.score, Type.number, true, "average frame time during play"), + SchemaField("edge_hits", "Edge Hits", Category.score, Type.number, true, "hits at the edge of hit window"), + SchemaField("snaps", "Snaps", Category.score, Type.number, true, "rapid cursor movements"), + SchemaField("adjusted_ur", "Adj. UR", Category.score, Type.number, true, "adjusted unstable rate"), + SchemaField("mean_error", "Mean Error", Category.score, Type.number, true, "average timing error"), + SchemaField("error_variance", "Error Var.", Category.score, Type.number, true, "variability of error in scores"), + SchemaField("error_standard_deviation", "Error SD", Category.score, Type.number, true, "standard deviation of error"), + SchemaField("minimum_error", "Min Error", Category.score, Type.number, true, "smallest error recorded"), + SchemaField("maximum_error", "Max Error", Category.score, Type.number, true, "largest error recorded"), + SchemaField("error_range", "Error Range", Category.score, Type.number, true, "range between min and max error"), + SchemaField("error_coefficient_of_variation", "Error CV", Category.score, Type.number, true, "relative variability of error"), + SchemaField("error_kurtosis", "Kurtosis", Category.score, Type.number, true, "peakedness of error distribution"), + SchemaField("error_skewness", "Skewness", Category.score, Type.number, true, "asymmetry of error distribution"), + SchemaField("keypresses_median_adjusted", "KP Median Adj.", Category.score, Type.number, true, "median of adjusted keypresses"), + SchemaField("keypresses_standard_deviation_adjusted", "KP std. Adj.", Category.score, Type.number, true, "std. dev of adjusted keypresses"), + SchemaField("sliderend_release_median_adjusted", "Sliderend Median Adj.", Category.score, Type.number, true, "median of adjusted sliderend releases"), + SchemaField("sliderend_release_standard_deviation_adjusted", "Sliderend std. Adj.", Category.score, Type.number, true, "std. dev of adjusted sliderend releases"), + + // Beatmap fields + SchemaField("beatmap_artist", "Artist", Category.beatmap, Type.string, true, "artist of the beatmap"), + SchemaField("beatmap_beatmapset_id", "Set ID", Category.beatmap, Type.number, true, "id of the beatmap set"), + SchemaField("beatmap_creator", "Creator", Category.beatmap, Type.string, true, "creator of the beatmap"), + SchemaField("beatmap_source", "Source", Category.beatmap, Type.string, true, "source of the beatmap music"), + SchemaField("beatmap_star_rating", "Stars", Category.beatmap, Type.number, true, "(★) difficulty rating of the beatmap"), + SchemaField("beatmap_title", "Title", Category.beatmap, Type.string, true, "title of the beatmap"), + SchemaField("beatmap_version", "Version", Category.beatmap, Type.string, true, "version or difficulty name of the beatmap") + ) + val schema = SearchSchema(fields) + return ResponseEntity.ok(schema) + } + @PostMapping("search") fun doSearch(@RequestBody request: SearchRequest, @RequestHeader("X-NISE-API") apiVersion: String): ResponseEntity { - if(!authService.isAdmin()) - return ResponseEntity.status(401).build() - if (apiVersion.isBlank()) return ResponseEntity.badRequest().build() diff --git a/nise-frontend/src/app/app.component.html b/nise-frontend/src/app/app.component.html index 4a41834..fd4b692 100644 --- a/nise-frontend/src/app/app.component.html +++ b/nise-frontend/src/app/app.component.html @@ -6,15 +6,16 @@

/nise.moe/

-
    +
    -
    +
    hi, {{this.userService.currentUser?.username}} Logout diff --git a/nise-frontend/src/app/search/search.component.html b/nise-frontend/src/app/search/search.component.html index d93342d..092de06 100644 --- a/nise-frontend/src/app/search/search.component.html +++ b/nise-frontend/src/app/search/search.component.html @@ -64,70 +64,78 @@
    -
    - tools -
    - - - + +
    +

    No results for your query - try different parameters.

    -
    -
    - - - - - - - - - - - -
    - {{ column.short_name }} -
    - - {{ getValue(entry, column.name) | number }} - - - {{ countryCodeToFlag(getValue(entry, column.name)) }} - - - - - - - ✓ - - - ✗ - - - - {{ getValue(entry, column.name) }} - -
    -
    +
    -
    -

    Total results: {{ response.pagination.totalResults | number }}

    -

    Page: {{ response.pagination.currentPage | number }} / {{ response.pagination.totalPages | number }}

    -
    - - ... - - ... - + +
    + tools +
    + + + +
    +
    +
    + + + + + + + + + + + +
    + {{ column.short_name }} +
    + + {{ getValue(entry, column.name) | number }} + + + {{ countryCodeToFlag(getValue(entry, column.name)) }} + + + + + + + ✓ + + + ✗ + + + + {{ getValue(entry, column.name) }} + +
    - - -
    + +
    +

    Total results: {{ response.pagination.totalResults | number }}

    +

    Page: {{ response.pagination.currentPage | number }} / {{ response.pagination.totalPages | number }}

    +
    + + ... + + ... + +
    + + +
    + diff --git a/nise-frontend/src/app/search/search.component.ts b/nise-frontend/src/app/search/search.component.ts index 6d165d0..ad2ad46 100644 --- a/nise-frontend/src/app/search/search.component.ts +++ b/nise-frontend/src/app/search/search.component.ts @@ -9,6 +9,7 @@ import {Field, Query, QueryBuilderComponent} from "../../corelib/components/quer import {RouterLink} from "@angular/router"; import {CalculatePageRangePipe} from "../../corelib/calculate-page-range.pipe"; import {DownloadFilesService} from "../../corelib/service/download-files.service"; +import {LocalCacheService} from "../../corelib/service/local-cache.service"; interface SchemaField { name: string; @@ -19,6 +20,10 @@ interface SchemaField { description: string; } +interface SchemaResponse { + fields: SchemaField[]; +} + interface SearchResponse { scores: SearchResponseEntry[]; pagination: SearchPagination; @@ -93,78 +98,37 @@ interface SearchResponseEntry { }) export class SearchComponent implements OnInit { - constructor(private httpClient: HttpClient, public downloadFilesService: DownloadFilesService) { } + constructor(private httpClient: HttpClient, + private localCacheService: LocalCacheService, + public downloadFilesService: DownloadFilesService) { } isError = false; isLoading = false; response: SearchResponse | null = null; - fields: SchemaField[] = [ - // User fields - { name: "user_id", short_name: "ID", category: "user", type: "number", active: true, description: "unique identifier for a user" }, - { name: "user_username", short_name: "Username", category: "user", type: "string", active: true, description: "user's name" }, - { name: "user_join_date", short_name: "Join Date", category: "user", type: "string", active: true, description: "when the user joined" }, - { name: "user_country", short_name: "Country", category: "user", type: "flag", active: true, description: "user's country flag" }, - { name: "user_country_rank", short_name: "Country Rank", category: "user", type: "number", active: true, description: "ranking within user's country" }, - { name: "user_rank", short_name: "Rank", category: "user", type: "number", active: true, description: "global ranking" }, - { name: "user_pp_raw", short_name: "User PP", category: "user", type: "number", active: true, description: "performance points" }, - { name: "user_accuracy", short_name: "User Accuracy", category: "user", type: "number", active: true, description: "hit accuracy percentage" }, - { name: "user_playcount", short_name: "Playcount", category: "user", type: "number", active: true, description: "total plays" }, - { name: "user_total_score", short_name: "Total Score", category: "user", type: "number", active: true, description: "cumulative score" }, - { name: "user_ranked_score", short_name: "Ranked Score", category: "user", type: "number", active: true, description: "score from ranked maps" }, - { name: "user_seconds_played", short_name: "Play Time", category: "user", type: "number", active: true, description: "total play time in seconds" }, - { name: "user_count_300", short_name: "300s", category: "user", type: "number", active: true, description: "number of 300 hits" }, - { name: "user_count_100", short_name: "100s", category: "user", type: "number", active: true, description: "number of 100 hits" }, - { name: "user_count_50", short_name: "50s", category: "user", type: "number", active: true, description: "number of 50 hits" }, - { name: "user_count_miss", short_name: "Misses", category: "user", type: "number", active: true, description: "missed hits" }, - - // Score fields - { name: "beatmap_id", short_name: "Beatmap ID", category: "score", type: "number", active: true, description: "identifies the beatmap" }, - { name: "count_300", short_name: "300s", category: "score", type: "number", active: true, description: "number of 300 hits in score" }, - { name: "count_100", short_name: "100s", category: "score", type: "number", active: true, description: "number of 100 hits in score" }, - { name: "count_50", short_name: "50s", category: "score", type: "number", active: true, description: "number of 50 hits in score" }, - { name: "count_miss", short_name: "Misses", category: "score", type: "number", active: true, description: "missed hits in score" }, - { name: "date", short_name: "Date", category: "score", type: "string", active: true, description: "when score was achieved" }, - { name: "max_combo", short_name: "Max Combo", category: "score", type: "number", active: true, description: "highest combo in score" }, - { name: "mods", short_name: "Mods", category: "score", type: "number", active: true, description: "game modifiers used" }, - { name: "perfect", short_name: "Perfect", category: "score", type: "boolean", active: true, description: "if score is a full combo" }, - { name: "pp", short_name: "Score PP", category: "score", type: "number", active: true, description: "performance points for score" }, - { name: "rank", short_name: "Rank", category: "score", type: "grade", active: true, description: "score grade" }, - { name: "replay_id", short_name: "Replay ID", category: "score", type: "number", active: true, description: "identifier for replay" }, - { name: "score", short_name: "Score", category: "score", type: "number", active: true, description: "score value" }, - { name: "ur", short_name: "UR", category: "score", type: "number", active: true, description: "unstable rate" }, - { name: "frametime", short_name: "Frame Time", category: "score", type: "number", active: true, description: "average frame time during play" }, - { name: "edge_hits", short_name: "Edge Hits", category: "score", type: "number", active: true, description: "hits at the edge of hit window" }, - { name: "snaps", short_name: "Snaps", category: "score", type: "number", active: true, description: "rapid cursor movements" }, - { name: "adjusted_ur", short_name: "Adj. UR", category: "score", type: "number", active: true, description: "adjusted unstable rate" }, - { name: "mean_error", short_name: "Mean Error", category: "score", type: "number", active: true, description: "average timing error" }, - { name: 'error_variance', short_name: 'Error Var.', category: 'score', type: 'number', active: true, description: 'variability of error in scores' }, - { name: 'error_standard_deviation', short_name: 'Error SD', category: 'score', type: 'number', active: true, description: 'standard deviation of error' }, - { name: 'minimum_error', short_name: 'Min Error', category: 'score', type: 'number', active: true, description: 'smallest error recorded' }, - { name: 'maximum_error', short_name: 'Max Error', category: 'score', type: 'number', active: true, description: 'largest error recorded' }, - { name: 'error_range', short_name: 'Error Range', category: 'score', type: 'number', active: true, description: 'range between min and max error' }, - { name: 'error_coefficient_of_variation', short_name: 'Error CV', category: 'score', type: 'number', active: true, description: 'relative variability of error' }, - { name: 'error_kurtosis', short_name: 'Kurtosis', category: 'score', type: 'number', active: true, description: 'peakedness of error distribution' }, - { name: 'error_skewness', short_name: 'Skewness', category: 'score', type: 'number', active: true, description: 'asymmetry of error distribution' }, - { name: 'keypresses_median_adjusted', short_name: 'KP Median Adj.', category: 'score', type: 'number', active: true, description: 'median of adjusted keypresses' }, - { name: 'keypresses_standard_deviation_adjusted', short_name: 'KP std. Adj.', category: 'score', type: 'number', active: true, description: 'std. dev of adjusted keypresses' }, - { name: 'sliderend_release_median_adjusted', short_name: 'Sliderend Median Adj.', category: 'score', type: 'number', active: true, description: 'median of adjusted sliderend releases' }, - { name: 'sliderend_release_standard_deviation_adjusted', short_name: 'Sliderend std. Adj.', category: 'score', type: 'number', active: true, description: 'std. dev of adjusted sliderend releases' }, - - // Beatmap fields - { name: 'beatmap_artist', short_name: 'Artist', category: 'beatmap', type: 'string', active: true, description: 'artist of the beatmap' }, - { name: 'beatmap_beatmapset_id', short_name: 'Set ID', category: 'beatmap', type: 'number', active: true, description: 'id of the beatmap set' }, - { name: 'beatmap_creator', short_name: 'Creator', category: 'beatmap', type: 'string', active: true, description: 'creator of the beatmap' }, - { name: 'beatmap_source', short_name: 'Source', category: 'beatmap', type: 'string', active: true, description: 'source of the beatmap music' }, - { name: 'beatmap_star_rating', short_name: 'Stars', category: 'beatmap', type: 'number', active: true, description: '(★) difficulty rating of the beatmap' }, - { name: 'beatmap_title', short_name: 'Title', category: 'beatmap', type: 'string', active: true, description: 'title of the beatmap' }, - { name: 'beatmap_version', short_name: 'Version', category: 'beatmap', type: 'string', active: true, description: 'version or difficulty name of the beatmap' } - ]; + fields: SchemaField[] = []; sortingOrder: Sorting | null = null; queries: Query[] | null = null; ngOnInit(): void { + + this.localCacheService.fetchData( + "search-schema", + `${environment.apiUrl}/search`, + 60 + ).subscribe({ + next: (response) => { + this.fields = response.fields; + this.loadPreviousFromLocalStorage(); + }, + error: () => { + alert('Error fetching schema'); + } + }); + } + + private loadPreviousFromLocalStorage() { const storedQueries = localStorage.getItem('search_queries'); if (storedQueries) { this.queries = JSON.parse(storedQueries); diff --git a/nise-frontend/src/corelib/components/query/query.component.html b/nise-frontend/src/corelib/components/query/query.component.html index 72def22..1fc3caf 100644 --- a/nise-frontend/src/corelib/components/query/query.component.html +++ b/nise-frontend/src/corelib/components/query/query.component.html @@ -20,7 +20,7 @@