Added link to advanced search in header, refactor, provide schema from backend
This commit is contained in:
parent
da299c0535
commit
b1635fd79c
@ -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<SchemaField>
|
||||
)
|
||||
|
||||
@GetMapping("search")
|
||||
fun getSearchSchema(): ResponseEntity<SearchSchema> {
|
||||
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<SearchResponse> {
|
||||
if(!authService.isAdmin())
|
||||
return ResponseEntity.status(401).build()
|
||||
|
||||
if (apiVersion.isBlank())
|
||||
return ResponseEntity.badRequest().build()
|
||||
|
||||
|
||||
@ -6,15 +6,16 @@
|
||||
</div>
|
||||
<div>
|
||||
<h2>/nise.moe/</h2>
|
||||
<ul style="font-size: 15px">
|
||||
<ul style="font-size: 15px; line-height: 19px;">
|
||||
<li><a [routerLink]="['/']">./home</a></li>
|
||||
<li><a [routerLink]="['/sus']">./suspicious-scores</a></li>
|
||||
<li><a [routerLink]="['/stolen']">./stolen-replays</a></li>
|
||||
<li><a [routerLink]="['/search']">./advanced-search</a></li>
|
||||
</ul>
|
||||
<form (ngSubmit)="onSubmit()">
|
||||
<input type="text" [(ngModel)]="term" [ngModelOptions]="{standalone: true}" id="nise-osu-username" required minlength="2" maxlength="50" placeholder="Search for users...">
|
||||
</form>
|
||||
<div style="margin-top: 12px">
|
||||
<div style="margin-top: 8px">
|
||||
<ng-container *ngIf="this.userService.isUserLoggedIn()">
|
||||
hi, <span class="user-details" [title]="this.userService.currentUser?.username">{{this.userService.currentUser?.username}}</span> <a [href]="this.userService.getLogoutUrl()">Logout</a>
|
||||
</ng-container>
|
||||
|
||||
@ -64,6 +64,13 @@
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="response">
|
||||
<ng-container *ngIf="response.scores.length <= 0">
|
||||
<div class="text-center alert-error">
|
||||
<p>No results for your query - try different parameters.</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="response.scores.length > 0">
|
||||
<fieldset class="mb-2">
|
||||
<legend>tools</legend>
|
||||
<div class="text-center">
|
||||
@ -128,6 +135,7 @@
|
||||
<button (click)="this.search(response.pagination.currentPage - 1)" [disabled]="response.pagination.currentPage == 1">← Previous</button>
|
||||
<button (click)="this.search(response.pagination.currentPage + 1)" [disabled]="response.pagination.currentPage == response.pagination.totalPages" style="margin-left: 5px">Next →</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
|
||||
|
||||
@ -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<SchemaResponse>(
|
||||
"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);
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
|
||||
<select [disabled]="!predicate.field">
|
||||
<option *ngFor="let operator of getOperators(predicate.field?.type)"
|
||||
[selected]="compare(operator.operatorType, predicate.operator?.operatorType)"
|
||||
[selected]="operator.operatorType === predicate.operator?.operatorType"
|
||||
(click)="predicate.operator = operator; this.queryChanged.emit()">
|
||||
{{ operator.operatorType }}
|
||||
</option>
|
||||
|
||||
@ -50,13 +50,6 @@ export class QueryComponent {
|
||||
}
|
||||
}
|
||||
|
||||
compare(a: any, b: any): boolean {
|
||||
console.warn('compare', a, b);
|
||||
console.warn('compare', a === b)
|
||||
console.error();
|
||||
return a === b;
|
||||
}
|
||||
|
||||
addPredicate(): void {
|
||||
this.query.predicates.push({ field: null, operator: null, value: null });
|
||||
this.queryChanged.emit();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user