Merge pull request #9020 from rusikv/feature/timeseries-api-improvements-ui
Refactoring of delete timeseries ui
This commit is contained in:
commit
009c4b4c70
@ -85,20 +85,12 @@
|
|||||||
'attribute.selected-telemetry' : 'attribute.selected-attributes') | translate:{count: dataSource.selection.selected.length} }}
|
'attribute.selected-telemetry' : 'attribute.selected-attributes') | translate:{count: dataSource.selection.selected.length} }}
|
||||||
</span>
|
</span>
|
||||||
<span fxFlex></span>
|
<span fxFlex></span>
|
||||||
<button [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)"
|
<button [fxShow]="attributeScope !== attributeScopeTypes.CLIENT_SCOPE"
|
||||||
class="button-widget-action"
|
class="button-widget-action"
|
||||||
mat-icon-button [disabled]="isLoading$ | async"
|
mat-icon-button [disabled]="isLoading$ | async"
|
||||||
matTooltip="{{ 'action.delete' | translate }}"
|
matTooltip="{{ 'action.delete' | translate }}"
|
||||||
matTooltipPosition="above"
|
matTooltipPosition="above"
|
||||||
(click)="deleteAttributes($event)">
|
(click)="deleteTelemetry($event)">
|
||||||
<mat-icon>delete</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button [fxShow]="attributeScope === latestTelemetryTypes.LATEST_TELEMETRY"
|
|
||||||
class="button-widget-action"
|
|
||||||
mat-icon-button [disabled]="isLoading$ | async"
|
|
||||||
matTooltip="{{ 'action.delete' | translate }}"
|
|
||||||
matTooltipPosition="above"
|
|
||||||
(click)="deleteTimeseries($event)">
|
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<button mat-raised-button color="accent"
|
<button mat-raised-button color="accent"
|
||||||
|
|||||||
@ -104,6 +104,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
|
|||||||
isClientSideTelemetryTypeMap = isClientSideTelemetryType;
|
isClientSideTelemetryTypeMap = isClientSideTelemetryType;
|
||||||
|
|
||||||
latestTelemetryTypes = LatestTelemetry;
|
latestTelemetryTypes = LatestTelemetry;
|
||||||
|
attributeScopeTypes = AttributeScope;
|
||||||
|
|
||||||
mode: 'default' | 'widget' = 'default';
|
mode: 'default' | 'widget' = 'default';
|
||||||
|
|
||||||
@ -423,29 +424,29 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
|
|||||||
this.viewContainerRef, injector));
|
this.viewContainerRef, injector));
|
||||||
componentRef.onDestroy(() => {
|
componentRef.onDestroy(() => {
|
||||||
if (componentRef.instance.result !== null) {
|
if (componentRef.instance.result !== null) {
|
||||||
const strategy = componentRef.instance.result;
|
const result = componentRef.instance.result;
|
||||||
const deleteTimeseries = attribute ? [attribute]: this.dataSource.selection.selected;
|
const deleteTimeseries = attribute ? [attribute]: this.dataSource.selection.selected;
|
||||||
let deleteAllDataForKeys = false;
|
let deleteAllDataForKeys = false;
|
||||||
let rewriteLatestIfDeleted = false;
|
let rewriteLatestIfDeleted = false;
|
||||||
let startTs = null;
|
let startTs = null;
|
||||||
let endTs = null;
|
let endTs = null;
|
||||||
let deleteLatest = true;
|
let deleteLatest = true;
|
||||||
if (strategy === TimeseriesDeleteStrategy.DELETE_ALL_DATA) {
|
if (result.strategy === TimeseriesDeleteStrategy.DELETE_ALL_DATA) {
|
||||||
deleteAllDataForKeys = true;
|
deleteAllDataForKeys = true;
|
||||||
}
|
}
|
||||||
if (strategy === TimeseriesDeleteStrategy.DELETE_ALL_DATA_EXCEPT_LATEST_VALUE) {
|
if (result.strategy === TimeseriesDeleteStrategy.DELETE_ALL_DATA_EXCEPT_LATEST_VALUE) {
|
||||||
deleteAllDataForKeys = true;
|
deleteAllDataForKeys = true;
|
||||||
deleteLatest = false;
|
deleteLatest = false;
|
||||||
}
|
}
|
||||||
if (strategy === TimeseriesDeleteStrategy.DELETE_LATEST_VALUE) {
|
if (result.strategy === TimeseriesDeleteStrategy.DELETE_LATEST_VALUE) {
|
||||||
rewriteLatestIfDeleted = componentRef.instance.rewriteLatestIfDeleted;
|
rewriteLatestIfDeleted = result.rewriteLatest;
|
||||||
startTs = deleteTimeseries[0].lastUpdateTs;
|
startTs = deleteTimeseries[0].lastUpdateTs;
|
||||||
endTs = startTs + 1;
|
endTs = startTs + 1;
|
||||||
}
|
}
|
||||||
if (strategy === TimeseriesDeleteStrategy.DELETE_ALL_DATA_FOR_TIME_PERIOD) {
|
if (result.strategy === TimeseriesDeleteStrategy.DELETE_ALL_DATA_FOR_TIME_PERIOD) {
|
||||||
startTs = componentRef.instance.startDateTime.getTime();
|
startTs = result.startDateTime.getTime();
|
||||||
endTs = componentRef.instance.endDateTime.getTime();
|
endTs = result.endDateTime.getTime();
|
||||||
rewriteLatestIfDeleted = componentRef.instance.rewriteLatestIfDeleted;
|
rewriteLatestIfDeleted = result.rewriteLatest;
|
||||||
}
|
}
|
||||||
this.attributeService.deleteEntityTimeseries(this.entityIdValue, deleteTimeseries, deleteAllDataForKeys,
|
this.attributeService.deleteEntityTimeseries(this.entityIdValue, deleteTimeseries, deleteAllDataForKeys,
|
||||||
startTs, endTs, rewriteLatestIfDeleted, deleteLatest).subscribe(() => this.reloadAttributes());
|
startTs, endTs, rewriteLatestIfDeleted, deleteLatest).subscribe(() => this.reloadAttributes());
|
||||||
@ -477,6 +478,14 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteTelemetry($event: Event) {
|
||||||
|
if (this.attributeScope === this.latestTelemetryTypes.LATEST_TELEMETRY) {
|
||||||
|
this.deleteTimeseries($event);
|
||||||
|
} else {
|
||||||
|
this.deleteAttributes($event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enterWidgetMode() {
|
enterWidgetMode() {
|
||||||
this.mode = 'widget';
|
this.mode = 'widget';
|
||||||
this.widgetsList = [];
|
this.widgetsList = [];
|
||||||
|
|||||||
@ -16,20 +16,20 @@
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<div class="mat-elevation-z1" style="max-width: 488px">
|
<form [formGroup]="deleteTimeseriesFormGroup" style="max-width: 488px">
|
||||||
<mat-toolbar>
|
<mat-toolbar>
|
||||||
<h2>{{ "attribute.delete-timeseries.delete-strategy" | translate }}</h2>
|
<h2>{{ "attribute.delete-timeseries.delete-strategy" | translate }}</h2>
|
||||||
<span fxFlex></span>
|
<span fxFlex></span>
|
||||||
<button mat-icon-button
|
<button mat-icon-button
|
||||||
(click)="cancel()"
|
type="button"
|
||||||
type="button">
|
(click)="cancel()">
|
||||||
<mat-icon class="material-icons">close</mat-icon>
|
<mat-icon class="material-icons">close</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</mat-toolbar>
|
</mat-toolbar>
|
||||||
<div style="padding: 16px; min-height: 188px">
|
<div style="padding: 16px; min-height: 188px">
|
||||||
<mat-form-field fxFlex class="mat-block">
|
<mat-form-field fxFlex class="mat-block">
|
||||||
<mat-label translate>attribute.delete-timeseries.strategy</mat-label>
|
<mat-label translate>attribute.delete-timeseries.strategy</mat-label>
|
||||||
<mat-select [(ngModel)]="strategy">
|
<mat-select formControlName="strategy">
|
||||||
<mat-option *ngFor="let strategy of strategiesTranslationsMap.keys()" [value]="strategy">
|
<mat-option *ngFor="let strategy of strategiesTranslationsMap.keys()" [value]="strategy">
|
||||||
{{ strategiesTranslationsMap.get(strategy) | translate }}
|
{{ strategiesTranslationsMap.get(strategy) | translate }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
@ -41,25 +41,24 @@
|
|||||||
<mat-label translate>attribute.delete-timeseries.start-time</mat-label>
|
<mat-label translate>attribute.delete-timeseries.start-time</mat-label>
|
||||||
<mat-datetimepicker-toggle [for]="startDateTimePicker" matPrefix></mat-datetimepicker-toggle>
|
<mat-datetimepicker-toggle [for]="startDateTimePicker" matPrefix></mat-datetimepicker-toggle>
|
||||||
<mat-datetimepicker #startDateTimePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
|
<mat-datetimepicker #startDateTimePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
|
||||||
<input required matInput [(ngModel)]="startDateTime" [matDatetimepicker]="startDateTimePicker"
|
<input required matInput formControlName="startDateTime" [matDatetimepicker]="startDateTimePicker">
|
||||||
(ngModelChange)="onStartDateTimeChange($event)">
|
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="mat-block">
|
<mat-form-field class="mat-block">
|
||||||
<mat-label translate>attribute.delete-timeseries.ends-on</mat-label>
|
<mat-label translate>attribute.delete-timeseries.ends-on</mat-label>
|
||||||
<mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle>
|
<mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle>
|
||||||
<mat-datetimepicker #endDatePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
|
<mat-datetimepicker #endDatePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
|
||||||
<input required matInput [(ngModel)]="endDateTime" [matDatetimepicker]="endDatePicker"
|
<input required matInput formControlName="endDateTime" [matDatetimepicker]="endDatePicker">
|
||||||
(ngModelChange)="onEndDateTimeChange($event)">
|
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="isPeriodStrategy() || isDeleteLatestStrategy()">
|
<div *ngIf="isPeriodStrategy() || isDeleteLatestStrategy()">
|
||||||
<mat-slide-toggle [(ngModel)]="rewriteLatestIfDeleted">
|
<mat-slide-toggle formControlName="rewriteLatest">
|
||||||
{{ "attribute.delete-timeseries.rewrite-latest-value" | translate }}
|
{{ "attribute.delete-timeseries.rewrite-latest-value" | translate }}
|
||||||
</mat-slide-toggle>
|
</mat-slide-toggle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div fxLayout="row" class="tb-panel-actions">
|
</form>
|
||||||
|
<div fxLayout="row" class="tb-panel-actions">
|
||||||
<span fxFlex></span>
|
<span fxFlex></span>
|
||||||
<button mat-button color="primary"
|
<button mat-button color="primary"
|
||||||
type="button"
|
type="button"
|
||||||
@ -67,10 +66,9 @@
|
|||||||
{{ 'action.cancel' | translate }}
|
{{ 'action.cancel' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button mat-button mat-raised-button color="primary"
|
<button mat-button mat-raised-button color="primary"
|
||||||
type="submit"
|
type="button"
|
||||||
(click)="delete()">
|
(click)="delete()"
|
||||||
|
[disabled]="deleteTimeseriesFormGroup.invalid">
|
||||||
{{ 'action.apply' | translate }}
|
{{ 'action.apply' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep{
|
:host ::ng-deep{
|
||||||
div .mat-toolbar {
|
form .mat-toolbar {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,13 +14,16 @@
|
|||||||
/// limitations under the License.
|
/// limitations under the License.
|
||||||
///
|
///
|
||||||
|
|
||||||
import { Component, Inject, InjectionToken, OnInit } from '@angular/core';
|
import { Component, Inject, InjectionToken, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { OverlayRef } from '@angular/cdk/overlay';
|
import { OverlayRef } from '@angular/cdk/overlay';
|
||||||
import {
|
import {
|
||||||
TimeseriesDeleteStrategy,
|
TimeseriesDeleteStrategy,
|
||||||
timeseriesDeleteStrategyTranslations
|
timeseriesDeleteStrategyTranslations
|
||||||
} from '@shared/models/telemetry/telemetry.models';
|
} from '@shared/models/telemetry/telemetry.models';
|
||||||
import { MINUTE } from '@shared/models/time/time.models';
|
import { MINUTE } from '@shared/models/time/time.models';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
export const DELETE_TIMESERIES_PANEL_DATA = new InjectionToken<any>('DeleteTimeseriesPanelData');
|
export const DELETE_TIMESERIES_PANEL_DATA = new InjectionToken<any>('DeleteTimeseriesPanelData');
|
||||||
|
|
||||||
@ -28,22 +31,23 @@ export interface DeleteTimeseriesPanelData {
|
|||||||
isMultipleDeletion: boolean;
|
isMultipleDeletion: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeleteTimeseriesPanelResult {
|
||||||
|
strategy: TimeseriesDeleteStrategy;
|
||||||
|
startDateTime: Date;
|
||||||
|
endDateTime: Date;
|
||||||
|
rewriteLatest: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-delete-timeseries-panel',
|
selector: 'tb-delete-timeseries-panel',
|
||||||
templateUrl: './delete-timeseries-panel.component.html',
|
templateUrl: './delete-timeseries-panel.component.html',
|
||||||
styleUrls: ['./delete-timeseries-panel.component.scss']
|
styleUrls: ['./delete-timeseries-panel.component.scss']
|
||||||
})
|
})
|
||||||
export class DeleteTimeseriesPanelComponent implements OnInit {
|
export class DeleteTimeseriesPanelComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
strategy: string = TimeseriesDeleteStrategy.DELETE_ALL_DATA;
|
deleteTimeseriesFormGroup: FormGroup;
|
||||||
|
|
||||||
result: string = null;
|
result: DeleteTimeseriesPanelResult = null;
|
||||||
|
|
||||||
startDateTime: Date;
|
|
||||||
|
|
||||||
endDateTime: Date;
|
|
||||||
|
|
||||||
rewriteLatestIfDeleted: boolean = true;
|
|
||||||
|
|
||||||
strategiesTranslationsMap = timeseriesDeleteStrategyTranslations;
|
strategiesTranslationsMap = timeseriesDeleteStrategyTranslations;
|
||||||
|
|
||||||
@ -52,24 +56,60 @@ export class DeleteTimeseriesPanelComponent implements OnInit {
|
|||||||
TimeseriesDeleteStrategy.DELETE_ALL_DATA_EXCEPT_LATEST_VALUE
|
TimeseriesDeleteStrategy.DELETE_ALL_DATA_EXCEPT_LATEST_VALUE
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
constructor(@Inject(DELETE_TIMESERIES_PANEL_DATA) public data: DeleteTimeseriesPanelData,
|
constructor(@Inject(DELETE_TIMESERIES_PANEL_DATA) public data: DeleteTimeseriesPanelData,
|
||||||
public overlayRef: OverlayRef) { }
|
public overlayRef: OverlayRef,
|
||||||
|
public fb: FormBuilder) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
let today = new Date();
|
const today = new Date();
|
||||||
this.startDateTime = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate());
|
|
||||||
this.endDateTime = today;
|
|
||||||
if (this.data.isMultipleDeletion) {
|
if (this.data.isMultipleDeletion) {
|
||||||
this.strategiesTranslationsMap = new Map(Array.from(this.strategiesTranslationsMap.entries())
|
this.strategiesTranslationsMap = new Map(Array.from(this.strategiesTranslationsMap.entries())
|
||||||
.filter(([strategy]) => {
|
.filter(([strategy]) => {
|
||||||
return this.multipleDeletionStrategies.includes(strategy);
|
return this.multipleDeletionStrategies.includes(strategy);
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
this.deleteTimeseriesFormGroup = this.fb.group({
|
||||||
|
strategy: [TimeseriesDeleteStrategy.DELETE_ALL_DATA],
|
||||||
|
startDateTime: [
|
||||||
|
{ value: new Date(today.getFullYear(), today.getMonth() - 1, today.getDate()), disabled: true },
|
||||||
|
[Validators.required]
|
||||||
|
],
|
||||||
|
endDateTime: [{ value: today, disabled: true }, [Validators.required]],
|
||||||
|
rewriteLatest: [true]
|
||||||
|
})
|
||||||
|
this.deleteTimeseriesFormGroup.get('strategy').valueChanges.pipe(
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
).subscribe(value => {
|
||||||
|
if (value === TimeseriesDeleteStrategy.DELETE_ALL_DATA_FOR_TIME_PERIOD) {
|
||||||
|
this.deleteTimeseriesFormGroup.get('startDateTime').enable({onlySelf: true, emitEvent: false});
|
||||||
|
this.deleteTimeseriesFormGroup.get('endDateTime').enable({onlySelf: true, emitEvent: false});
|
||||||
|
} else {
|
||||||
|
this.deleteTimeseriesFormGroup.get('startDateTime').disable({onlySelf: true, emitEvent: false});
|
||||||
|
this.deleteTimeseriesFormGroup.get('endDateTime').disable({onlySelf: true, emitEvent: false});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.deleteTimeseriesFormGroup.get('startDateTime').valueChanges.pipe(
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
).subscribe(value => this.onStartDateTimeChange(value));
|
||||||
|
this.deleteTimeseriesFormGroup.get('endDateTime').valueChanges.pipe(
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
).subscribe(value => this.onEndDateTimeChange(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(): void {
|
delete(): void {
|
||||||
this.result = this.strategy;
|
if (this.deleteTimeseriesFormGroup.valid) {
|
||||||
|
this.result = this.deleteTimeseriesFormGroup.value;
|
||||||
this.overlayRef.dispose();
|
this.overlayRef.dispose();
|
||||||
|
} else {
|
||||||
|
this.deleteTimeseriesFormGroup.markAllAsTouched();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(): void {
|
cancel(): void {
|
||||||
@ -77,28 +117,36 @@ export class DeleteTimeseriesPanelComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isPeriodStrategy(): boolean {
|
isPeriodStrategy(): boolean {
|
||||||
return this.strategy === TimeseriesDeleteStrategy.DELETE_ALL_DATA_FOR_TIME_PERIOD;
|
return this.deleteTimeseriesFormGroup.get('strategy').value === TimeseriesDeleteStrategy.DELETE_ALL_DATA_FOR_TIME_PERIOD;
|
||||||
}
|
}
|
||||||
|
|
||||||
isDeleteLatestStrategy(): boolean {
|
isDeleteLatestStrategy(): boolean {
|
||||||
return this.strategy === TimeseriesDeleteStrategy.DELETE_LATEST_VALUE;
|
return this.deleteTimeseriesFormGroup.get('strategy').value === TimeseriesDeleteStrategy.DELETE_LATEST_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
onStartDateTimeChange(newStartDateTime: Date) {
|
onStartDateTimeChange(newStartDateTime: Date) {
|
||||||
const endDateTimeTs = this.endDateTime.getTime();
|
if (newStartDateTime) {
|
||||||
|
const endDateTimeTs = this.deleteTimeseriesFormGroup.get('endDateTime').value.getTime();
|
||||||
if (newStartDateTime.getTime() >= endDateTimeTs) {
|
if (newStartDateTime.getTime() >= endDateTimeTs) {
|
||||||
this.startDateTime = new Date(endDateTimeTs - MINUTE);
|
this.deleteTimeseriesFormGroup.get('startDateTime')
|
||||||
|
.patchValue(new Date(endDateTimeTs - MINUTE), {onlySelf: true, emitEvent: false});
|
||||||
} else {
|
} else {
|
||||||
this.startDateTime = newStartDateTime;
|
this.deleteTimeseriesFormGroup.get('startDateTime')
|
||||||
|
.patchValue(newStartDateTime, {onlySelf: true, emitEvent: false});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onEndDateTimeChange(newEndDateTime: Date) {
|
onEndDateTimeChange(newEndDateTime: Date) {
|
||||||
const startDateTimeTs = this.startDateTime.getTime();
|
if (newEndDateTime) {
|
||||||
|
const startDateTimeTs = this.deleteTimeseriesFormGroup.get('startDateTime').value.getTime();
|
||||||
if (newEndDateTime.getTime() <= startDateTimeTs) {
|
if (newEndDateTime.getTime() <= startDateTimeTs) {
|
||||||
this.endDateTime = new Date(startDateTimeTs + MINUTE);
|
this.deleteTimeseriesFormGroup.get('endDateTime')
|
||||||
|
.patchValue(new Date(startDateTimeTs + MINUTE), {onlySelf: true, emitEvent: false});
|
||||||
} else {
|
} else {
|
||||||
this.endDateTime = newEndDateTime;
|
this.deleteTimeseriesFormGroup.get('endDateTime')
|
||||||
|
.patchValue(newEndDateTime, {onlySelf: true, emitEvent: false});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user