prepare sending of plot requests
- values for query and date range were not propagated to the model
This commit is contained in:
@@ -11,7 +11,7 @@ import { HelpPageComponent } from './help-page/help-page.component';
|
|||||||
import { UploadPageComponent } from './upload-page/upload-page.component';
|
import { UploadPageComponent } from './upload-page/upload-page.component';
|
||||||
import { VisualizationPageComponent } from './visualization-page/visualization-page.component';
|
import { VisualizationPageComponent } from './visualization-page/visualization-page.component';
|
||||||
|
|
||||||
import {MatAutocompleteModule} from '@angular/material/autocomplete';
|
import {MatAutocompleteModule} from '@angular/material/autocomplete';
|
||||||
import {MatButtonModule} from '@angular/material/button';
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
import {MatCheckboxModule} from '@angular/material/checkbox';
|
import {MatCheckboxModule} from '@angular/material/checkbox';
|
||||||
import {MatSelectModule} from '@angular/material/select';
|
import {MatSelectModule} from '@angular/material/select';
|
||||||
|
|||||||
@@ -15,22 +15,23 @@ export class PlotService {
|
|||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
this.plotTypes = new Array<PlotType>();
|
this.plotTypes = new Array<PlotType>();
|
||||||
this.plotTypes.push(new PlotType(
|
this.plotTypes.push(new PlotType(
|
||||||
|
"SCATTER",
|
||||||
"Scatter",
|
"Scatter",
|
||||||
"scatter-chart2",
|
"scatter-chart2",
|
||||||
true,
|
true,
|
||||||
DataType.Time,
|
DataType.Time,
|
||||||
DataType.Duration));
|
DataType.Duration));
|
||||||
this.plotTypes.push(new PlotType("Heatmap", "heatmap", false, DataType.Other, DataType.Other));
|
this.plotTypes.push(new PlotType("HEATMAP", "Heatmap", "heatmap", false, DataType.Other, DataType.Other));
|
||||||
this.plotTypes.push(new PlotType("Contour", "contour-chart", false, DataType.Time, DataType.Duration));
|
this.plotTypes.push(new PlotType("CONTOUR", "Contour", "contour-chart", false, DataType.Time, DataType.Duration));
|
||||||
this.plotTypes.push(new PlotType("Cumulative Distribution", "cumulative-distribution-chart", true, DataType.Percent, DataType.Duration));
|
this.plotTypes.push(new PlotType("CUM_DISTRIBUTION", "Cumulative Distribution", "cumulative-distribution-chart", true, DataType.Percent, DataType.Duration));
|
||||||
this.plotTypes.push(new PlotType("Histogram", "histogram", false, DataType.Group, DataType.Duration));
|
this.plotTypes.push(new PlotType("HISTOGRAM", "Histogram", "histogram", false, DataType.Group, DataType.Duration));
|
||||||
this.plotTypes.push(new PlotType("Ridgelines", "ridgelines", false, DataType.Other, DataType.Other));
|
this.plotTypes.push(new PlotType("RIDGELINES", "Ridgelines", "ridgelines", false, DataType.Other, DataType.Other));
|
||||||
this.plotTypes.push(new PlotType("Quantile-Quantile", "quantile-quantile", false, DataType.Other, DataType.Other));
|
this.plotTypes.push(new PlotType("QQ", "Quantile-Quantile", "quantile-quantile", false, DataType.Other, DataType.Other));
|
||||||
this.plotTypes.push(new PlotType("Parallel Requests", "parallel-requests-chart", true, DataType.Time, DataType.Count));
|
this.plotTypes.push(new PlotType("PARALLEL", "Parallel Requests", "parallel-requests-chart", true, DataType.Time, DataType.Count));
|
||||||
this.plotTypes.push(new PlotType("Violin", "violin-chart", false, DataType.Group, DataType.Duration));
|
this.plotTypes.push(new PlotType("VIOLIN", "Violin", "violin-chart", false, DataType.Group, DataType.Duration));
|
||||||
this.plotTypes.push(new PlotType("Strip", "strip-chart", false, DataType.Group, DataType.Duration));
|
this.plotTypes.push(new PlotType("STRIP", "Strip", "strip-chart", false, DataType.Group, DataType.Duration));
|
||||||
this.plotTypes.push(new PlotType("Pie", "pie-chart", false, DataType.Other, DataType.Other));
|
this.plotTypes.push(new PlotType("PIE", "Pie", "pie-chart", false, DataType.Other, DataType.Other));
|
||||||
this.plotTypes.push(new PlotType("Bar", "bar-chart", false, DataType.Other, DataType.Other));
|
this.plotTypes.push(new PlotType("BAR", "Bar", "bar-chart", false, DataType.Other, DataType.Other));
|
||||||
|
|
||||||
this.tagFields = new Array<TagField>();
|
this.tagFields = new Array<TagField>();
|
||||||
}
|
}
|
||||||
@@ -64,17 +65,25 @@ export class PlotService {
|
|||||||
};
|
};
|
||||||
return this.http.get<AutocompleteResult>('//'+window.location.hostname+':8080/autocomplete', options);
|
return this.http.get<AutocompleteResult>('//'+window.location.hostname+':8080/autocomplete', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendPlotRequest(plotRequest: PlotRequest): Observable<PlotResponse>{
|
||||||
|
|
||||||
|
console.log("send plot request: "+ JSON.stringify(plotRequest));
|
||||||
|
return this.http.post<PlotResponse>('//'+window.location.hostname+':8080/plots', plotRequest);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class PlotType {
|
export class PlotType {
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string
|
icon: string
|
||||||
active: boolean;
|
active: boolean;
|
||||||
xAxis: DataType;
|
xAxis: DataType;
|
||||||
yAxis: DataType;
|
yAxis: DataType;
|
||||||
|
|
||||||
constructor(name: string, icon: string, active: boolean, xAxis: DataType, yAxis: DataType) {
|
constructor(id: string, name: string, icon: string, active: boolean, xAxis: DataType, yAxis: DataType) {
|
||||||
|
this.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.icon = icon;
|
this.icon = icon;
|
||||||
this.active = active;
|
this.active = active;
|
||||||
@@ -131,6 +140,43 @@ export class AutocompleteResult{
|
|||||||
proposals: Array<Suggestion>;
|
proposals: Array<Suggestion>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class PlotRequest {
|
||||||
|
query : string;
|
||||||
|
height : number;
|
||||||
|
width : number;
|
||||||
|
thumbnailMaxWidth : number = 300;
|
||||||
|
thumbnailMaxHeight : number = 200;
|
||||||
|
groupBy : Array<string>;
|
||||||
|
limitBy : string;
|
||||||
|
yAxis : string;
|
||||||
|
limit : number;
|
||||||
|
dateRange : string;
|
||||||
|
aggregates : Array<string>;
|
||||||
|
yRangeMin : number;
|
||||||
|
yRangeMax : number;
|
||||||
|
yRangeUnit : string;
|
||||||
|
keyOutside : boolean = false;
|
||||||
|
generateThumbnail : boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PlotResponse {
|
||||||
|
imageUrl : string;
|
||||||
|
stats : PlotResponseStats;
|
||||||
|
thumbnailUrl : string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PlotResponseStats {
|
||||||
|
maxValue : number;
|
||||||
|
values : number;
|
||||||
|
average : number ;
|
||||||
|
plottedValues : number;
|
||||||
|
dataSeriesStats : Array<DataSeriesStats>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DataSeriesStats {
|
||||||
|
values : number;
|
||||||
|
maxValue : number;
|
||||||
|
average : number;
|
||||||
|
plottedValues : number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
type="text"
|
type="text"
|
||||||
id="query-autocomplete-input"
|
id="query-autocomplete-input"
|
||||||
placeholder="Query"
|
placeholder="Query"
|
||||||
[formControl]="query"
|
[formControl]="queryField"
|
||||||
[matAutocomplete]="auto"
|
[matAutocomplete]="auto"
|
||||||
(keyup)="onKey($event)"
|
(keyup)="onKey($event)"
|
||||||
(focus)="onKey($event)"/>
|
(mouseup)="onKey($event)"/>
|
||||||
<mat-autocomplete
|
<mat-autocomplete
|
||||||
#auto="matAutocomplete"
|
#auto="matAutocomplete"
|
||||||
[displayWith]="displaySuggestion"
|
[displayWith]="displaySuggestion"
|
||||||
>
|
>
|
||||||
<mat-option *ngFor="let suggestion of filteredSuggestions | async"
|
<mat-option *ngFor="let suggestion of filteredSuggestions | async"
|
||||||
[value]="suggestion">
|
[value]="suggestion">
|
||||||
{{suggestion.value}}
|
{{suggestion.value}}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-autocomplete>
|
</mat-autocomplete>
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Component, OnInit, Input } from '@angular/core';
|
import { Component, OnInit, Input, ViewChild } from '@angular/core';
|
||||||
import {FormControl} from '@angular/forms';
|
import {FormControl} from '@angular/forms';
|
||||||
import {Observable} from 'rxjs';
|
import {Observable} from 'rxjs';
|
||||||
import {startWith, map} from 'rxjs/operators';
|
import {startWith, map} from 'rxjs/operators';
|
||||||
|
import {MatAutocompleteTrigger } from '@angular/material/autocomplete';
|
||||||
import { PlotService, PlotType, AutocompleteResult, Suggestion } from '../plot.service';
|
import { PlotService, PlotType, AutocompleteResult, Suggestion } from '../plot.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -11,38 +12,65 @@ import { PlotService, PlotType, AutocompleteResult, Suggestion } from '../plot.s
|
|||||||
})
|
})
|
||||||
export class QueryAutocompleteComponent implements OnInit {
|
export class QueryAutocompleteComponent implements OnInit {
|
||||||
|
|
||||||
@Input() query = new FormControl('');
|
queryField = new FormControl('');
|
||||||
|
|
||||||
suggestions = new FormControl();
|
suggestions = new FormControl();
|
||||||
|
|
||||||
filteredSuggestions: Observable<Suggestion[]>;
|
filteredSuggestions: Observable<Suggestion[]>;
|
||||||
|
|
||||||
|
query : string;
|
||||||
|
|
||||||
|
@ViewChild(MatAutocompleteTrigger, {static: false})
|
||||||
|
autocomplete: MatAutocompleteTrigger;
|
||||||
|
|
||||||
|
|
||||||
constructor(private plotService: PlotService) {}
|
constructor(private plotService: PlotService) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
const that = this;
|
||||||
|
this.query = "";
|
||||||
|
this.queryField.valueChanges.subscribe(function(value){
|
||||||
|
if (typeof value == "string") {
|
||||||
|
that.query = value;
|
||||||
|
}else{
|
||||||
|
that.query = value.newQuery;
|
||||||
|
|
||||||
|
var el : HTMLInputElement = <HTMLInputElement>document.getElementById('query-autocomplete-input');
|
||||||
|
el.selectionStart=value.newCaretPosition;
|
||||||
|
el.selectionEnd=value.newCaretPosition;
|
||||||
|
|
||||||
|
that.fetchSuggestions(value.newCaretPosition);
|
||||||
|
}
|
||||||
|
});
|
||||||
this.filteredSuggestions = this.suggestions.valueChanges.pipe(
|
this.filteredSuggestions = this.suggestions.valueChanges.pipe(
|
||||||
map(value => value)
|
map(value => value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onKey(event: any) {
|
|
||||||
const that = this;
|
|
||||||
|
|
||||||
console.log(event);
|
onKey(event: any) {
|
||||||
|
//console.log(event);
|
||||||
if (event.key == "ArrowDown" || event.key == "ArrowUp"){
|
if (event.key == "ArrowDown" || event.key == "ArrowUp"){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const query = typeof this.query.value == "string"
|
|
||||||
? this.query.value
|
|
||||||
: this.query.value.newQuery;
|
|
||||||
const caretIndex = event.srcElement.selectionStart +1;
|
const caretIndex = event.srcElement.selectionStart +1;
|
||||||
|
|
||||||
|
this.fetchSuggestions(caretIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchSuggestions(caretIndex: number){
|
||||||
|
const that = this;
|
||||||
|
const query = typeof this.queryField.value == "string"
|
||||||
|
? this.queryField.value
|
||||||
|
: this.queryField.value.newQuery;
|
||||||
|
|
||||||
this.plotService
|
this.plotService
|
||||||
.autocomplete(query, caretIndex)
|
.autocomplete(query, caretIndex)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
(data: AutocompleteResult) => {// success path
|
(data: AutocompleteResult) => {// success path
|
||||||
console.log(JSON.stringify(data.proposals));
|
//console.log(JSON.stringify(data.proposals));
|
||||||
that.suggestions.setValue(data.proposals);
|
that.suggestions.setValue(data.proposals);
|
||||||
|
|
||||||
|
that.autocomplete.openPanel();
|
||||||
},
|
},
|
||||||
error => console.log(error)
|
error => console.log(error)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
<div id="visualization">
|
<div id="visualization">
|
||||||
<div id="query-box">
|
<div id="query-box">
|
||||||
<pdb-query-autocomplete ></pdb-query-autocomplete>
|
<pdb-query-autocomplete #query></pdb-query-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
<div id="filters">
|
<div id="filters">
|
||||||
<div id="filterpanel">
|
<div id="filterpanel">
|
||||||
<mat-form-field class="mat-field-full-width">
|
<mat-form-field class="mat-field-full-width">
|
||||||
<mat-label>Date Range:</mat-label>
|
<mat-label>Date Range:</mat-label>
|
||||||
<input matInput [formControl]="dateRange" name="dates" />
|
<input matInput id="search-date-range" value="dateRange" name="dates" (input)="changeDate($event)" />
|
||||||
|
<input type="hidden" id="hidden-search-date-range" [(ngModel)]="hiddenSearchDateRange" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field class="mat-field-full-width">
|
<mat-form-field class="mat-field-full-width">
|
||||||
<mat-label>Type:</mat-label>
|
<mat-label>Type:</mat-label>
|
||||||
<mat-select [formControl]="selectedPlotType">
|
<mat-select [formControl]="selectedPlotType">
|
||||||
<mat-option *ngFor="let plotType of plotTypes" [value]="plotType.name">
|
<mat-option *ngFor="let plotType of plotTypes" [value]="plotType">
|
||||||
<img src="assets/img/{{plotType.icon}}.svg" class="icon-select" /> {{plotType.name}}
|
<img src="assets/img/{{plotType.icon}}.svg" class="icon-select" /> {{plotType.name}}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
@@ -20,9 +21,9 @@
|
|||||||
|
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>Combine with:</mat-label>
|
<mat-label>Combine with:</mat-label>
|
||||||
<mat-select [(value)]="selectedCombinePlotType">
|
<mat-select [formControl]="selectedCombinePlotType">
|
||||||
<mat-option>-</mat-option>
|
<mat-option>-</mat-option>
|
||||||
<mat-option *ngFor="let plotType of combinePlotTypes" [value]="plotType.name">
|
<mat-option *ngFor="let plotType of combinePlotTypes" [value]="plotType">
|
||||||
<img src="assets/img/{{plotType.icon}}.svg" class="icon-select" /> {{plotType.name}}
|
<img src="assets/img/{{plotType.icon}}.svg" class="icon-select" /> {{plotType.name}}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { PlotService, PlotType } from '../plot.service';
|
import { PlotService, PlotType, PlotRequest, PlotResponse } from '../plot.service';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { FormControl, Validators } from '@angular/forms';
|
import { FormControl, Validators } from '@angular/forms';
|
||||||
import { LimitByComponent } from '../limit-by/limit-by.component';
|
import { LimitByComponent } from '../limit-by/limit-by.component';
|
||||||
import { YAxisRangeComponent } from '../y-axis-range/y-axis-range.component';
|
import { YAxisRangeComponent } from '../y-axis-range/y-axis-range.component';
|
||||||
|
import { QueryAutocompleteComponent } from '../query-autocomplete/query-autocomplete.component';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -35,7 +36,8 @@ export class VisualizationPageComponent implements OnInit {
|
|||||||
@ViewChild(YAxisRangeComponent, {static: false})
|
@ViewChild(YAxisRangeComponent, {static: false})
|
||||||
private yAxisRangeComponent : YAxisRangeComponent;
|
private yAxisRangeComponent : YAxisRangeComponent;
|
||||||
|
|
||||||
query: string;
|
@ViewChild(QueryAutocompleteComponent, {static: false})
|
||||||
|
query: QueryAutocompleteComponent;
|
||||||
|
|
||||||
|
|
||||||
enableGallery = false;
|
enableGallery = false;
|
||||||
@@ -48,9 +50,8 @@ export class VisualizationPageComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
const that = this;
|
const that = this;
|
||||||
this.query = "pod=*";
|
|
||||||
this.plotTypes = this.plotService.getPlotTypes();
|
this.plotTypes = this.plotService.getPlotTypes();
|
||||||
this.selectedPlotType.setValue(this.plotTypes[0].name);
|
this.selectedPlotType.setValue(this.plotTypes[0]);
|
||||||
|
|
||||||
this.plotTypes.forEach(pt => this.availablePlotTypes[pt.name] = pt);
|
this.plotTypes.forEach(pt => this.availablePlotTypes[pt.name] = pt);
|
||||||
|
|
||||||
@@ -59,41 +60,54 @@ export class VisualizationPageComponent implements OnInit {
|
|||||||
this.tagFields = this.plotService.getTagFields();
|
this.tagFields = this.plotService.getTagFields();
|
||||||
this.yAxis = "log";
|
this.yAxis = "log";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.selectedPlotType.valueChanges.subscribe(function(selectedMainPlotType){
|
this.selectedPlotType.valueChanges.subscribe(function(selectedMainPlotType){
|
||||||
that.combinePlotTypes = that.getCombinablePlotTypes(selectedMainPlotType);
|
that.combinePlotTypes = that.getCombinablePlotTypes(selectedMainPlotType);
|
||||||
|
if (!that.combinePlotTypes.includes(that.selectedCombinePlotType.value)){
|
||||||
|
that.selectedCombinePlotType.setValue('');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeDate(event){
|
||||||
|
console.log("changed date: " + JSON.stringify(event));
|
||||||
|
}
|
||||||
|
|
||||||
getCombinablePlotTypes(selectedMainPlotType) : Array<any>{
|
getCombinablePlotTypes(selectedMainPlotType) : Array<any>{
|
||||||
const mainPlotType = this.availablePlotTypes[selectedMainPlotType];
|
//const mainPlotType = this.availablePlotTypes[selectedMainPlotType];
|
||||||
|
const mainPlotType = selectedMainPlotType;
|
||||||
|
|
||||||
const compatiblePlotTypes = this.plotTypes.filter(pt => pt.compatible(mainPlotType));
|
const compatiblePlotTypes = this.plotTypes.filter(pt => pt.compatible(mainPlotType));
|
||||||
return compatiblePlotTypes;
|
return compatiblePlotTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
plot(){
|
plot(){
|
||||||
|
|
||||||
|
var aggregates = [];
|
||||||
|
aggregates.push(this.selectedPlotType.value.id);
|
||||||
|
if (this.selectedCombinePlotType.value){
|
||||||
|
aggregates.push(this.selectedCombinePlotType.value.id);
|
||||||
|
}
|
||||||
|
|
||||||
var request = {};
|
var request = new PlotRequest();
|
||||||
request['query'] = this.query;
|
request.query = this.query.query;
|
||||||
request['height'] = document.getElementById("results").offsetHeight;
|
request.height = document.getElementById("results").offsetHeight;
|
||||||
request['width'] = document.getElementById("results").offsetWidth;
|
request.width = document.getElementById("results").offsetWidth;
|
||||||
request['groupBy'] = this.groupBy.map(o => o.name);
|
request.groupBy = this.groupBy.map(o => o.name);
|
||||||
request['limitBy'] = this.limitbycomponent.limitBy;
|
request.limitBy = this.limitbycomponent.limitBy;
|
||||||
request['limit'] = this.limitbycomponent.limit;
|
request.limit = this.limitbycomponent.limit;
|
||||||
request['dateRange'] = this.dateRange.value;
|
request.dateRange = (<HTMLInputElement>document.getElementById("search-date-range")).value;
|
||||||
request['axisScale'] = this.yAxis;
|
request.yAxis = this.yAxis;
|
||||||
request['aggregate'] = this.selectedCombinePlotType.value;
|
request.aggregates = aggregates;
|
||||||
request['keyOutside'] = false;
|
request.keyOutside = false;
|
||||||
request['generateThumbnail'] = this.enableGallery;
|
request.generateThumbnail = this.enableGallery;
|
||||||
request['yRangeMin'] = this.yAxisRangeComponent.minYValue;
|
request.yRangeMin = this.yAxisRangeComponent.minYValue;
|
||||||
request['yRangeMax'] = this.yAxisRangeComponent.maxYValue;
|
request.yRangeMax = this.yAxisRangeComponent.maxYValue;
|
||||||
request['yRangeUnit'] = this.yAxisRangeComponent.yAxisUnit;
|
request.yRangeUnit = this.yAxisRangeComponent.yAxisUnit;
|
||||||
|
|
||||||
console.log("plot clicked: "+ JSON.stringify(request));
|
|
||||||
|
this.plotService.sendPlotRequest(request).subscribe(function(plotResponse){
|
||||||
|
console.log("response: " + JSON.stringify(plotResponse));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,10 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#search-date-range').on('apply.daterangepicker', function(ev, picker) {
|
||||||
|
const range = $('#search-date-range').val();
|
||||||
|
console.log("update date range: " + range);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user