mirror of
https://github.com/goharbor/harbor.git
synced 2025-02-21 14:21:36 +01:00
Fix bugs for scanner UI testing round 1
Signed-off-by: sshijun <sshijun@vmware.com>
This commit is contained in:
parent
148cb95363
commit
06013065ff
@ -104,7 +104,7 @@
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">
|
||||
<div class="cell">
|
||||
<hbr-copy-input class="margin-top-m4" #copyInput (onCopyError)="onCpError($event)" iconMode="true" defaultValue="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}"></hbr-copy-input>
|
||||
<hbr-copy-input #copyInput (onCopyError)="onCpError($event)" iconMode="true" defaultValue="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}"></hbr-copy-input>
|
||||
</div>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
|
@ -252,7 +252,13 @@ clr-datagrid {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.margin-top-m4{
|
||||
margin-top: -4px;
|
||||
// todo :can be improved
|
||||
:host::ng-deep clr-dg-row {
|
||||
.datagrid-select {
|
||||
.clr-checkbox-wrapper {
|
||||
input[type=checkbox] + label {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -256,6 +256,18 @@ export const VULNERABILITY_SEVERITY = {
|
||||
CRITICAL: "Critical",
|
||||
NONE: "None"
|
||||
};
|
||||
/**
|
||||
* The level of vulnerability severity for comparing
|
||||
*/
|
||||
export const SEVERITY_LEVEL_MAP = {
|
||||
"Critical": 6,
|
||||
"High": 5,
|
||||
"Medium": 4,
|
||||
"Low": 3,
|
||||
"Negligible": 2,
|
||||
"Unknown": 1,
|
||||
"None": 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate page number by state
|
||||
|
@ -12,7 +12,7 @@
|
||||
<div>{{'VULNERABILITY.STATE.SCANNING' | translate}}</div>
|
||||
<div class="progress loop loop-height"><progress></progress></div>
|
||||
</div>
|
||||
<div *ngIf="completed" class="bar-state bar-state-chart margin-top-m15">
|
||||
<div *ngIf="completed" class="bar-state bar-state-chart">
|
||||
<hbr-result-tip-histogram [vulnerabilitySummary]="summary"></hbr-result-tip-histogram>
|
||||
</div>
|
||||
<div *ngIf="otherStatus" class="bar-state">
|
||||
|
@ -13,7 +13,7 @@
|
||||
<button type="button" class="btn btn-sm btn-secondary" [clrLoading]="scanBtnState" [disabled]="!hasScanImagePermission || !hasEnabledScanner" (click)="scanNow()"><clr-icon shape="shield-check" size="16"></clr-icon> {{'VULNERABILITY.SCAN_NOW' | translate}}</button>
|
||||
</clr-dg-action-bar>
|
||||
<clr-dg-column [clrDgField]="'id'">{{'VULNERABILITY.GRID.COLUMN_ID' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'severity'">{{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="severitySort">{{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'package'">{{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'version'">{{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'fix_version'">{{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}}</clr-dg-column>
|
||||
@ -21,7 +21,7 @@
|
||||
<clr-dg-placeholder>{{'VULNERABILITY.CHART.TOOLTIPS_TITLE_ZERO' | translate}}</clr-dg-placeholder>
|
||||
<clr-dg-row *clrDgItems="let res of scanningResults">
|
||||
<clr-dg-cell>
|
||||
<span *ngIf="!res.links">{{res.id}}</span>
|
||||
<span *ngIf="!res.links || res.links.length === 0">{{res.id}}</span>
|
||||
<a *ngIf="res.links && res.links.length === 1" href="{{res.links[0]}}" target="_blank">{{res.id}}</a>
|
||||
<span *ngIf="res.links && res.links.length > 1">
|
||||
{{res.id}}
|
||||
|
@ -9,10 +9,9 @@ import { forkJoin } from "rxjs";
|
||||
import { ChannelService } from "../channel/channel.service";
|
||||
import { UserPermissionService } from "../service/permission.service";
|
||||
import { USERSTATICPERMISSION } from "../service/permission-static";
|
||||
import { DEFAULT_SUPPORTED_MIME_TYPE, VULNERABILITY_SEVERITY } from '../utils';
|
||||
import { finalize, map } from "rxjs/operators";
|
||||
import { ClrLoadingState } from "@clr/angular";
|
||||
|
||||
import { DEFAULT_SUPPORTED_MIME_TYPE, SEVERITY_LEVEL_MAP, VULNERABILITY_SEVERITY } from '../utils';
|
||||
import { finalize } from "rxjs/operators";
|
||||
import { ClrDatagridComparatorInterface, ClrLoadingState } from "@clr/angular";
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-vulnerabilities-grid',
|
||||
@ -30,12 +29,20 @@ export class ResultGridComponent implements OnInit {
|
||||
hasScanImagePermission: boolean;
|
||||
hasEnabledScanner: boolean = false;
|
||||
scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
severitySort: ClrDatagridComparatorInterface<VulnerabilityItem>;
|
||||
constructor(
|
||||
private scanningService: ScanningResultService,
|
||||
private channel: ChannelService,
|
||||
private userPermissionService: UserPermissionService,
|
||||
private errorHandler: ErrorHandler,
|
||||
) { }
|
||||
) {
|
||||
const that = this;
|
||||
this.severitySort = {
|
||||
compare(a: VulnerabilityItem, b: VulnerabilityItem): number {
|
||||
return that.getLevel(a) - that.getLevel(b);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadResults(this.repositoryId, this.tagId);
|
||||
@ -44,6 +51,12 @@ export class ResultGridComponent implements OnInit {
|
||||
this.loadResults(this.repositoryId, this.tagId);
|
||||
});
|
||||
}
|
||||
getLevel(v: VulnerabilityItem): number {
|
||||
if (v && v.severity && SEVERITY_LEVEL_MAP[v.severity]) {
|
||||
return SEVERITY_LEVEL_MAP[v.severity];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
getProjectScanner(): void {
|
||||
this.hasEnabledScanner = false;
|
||||
this.scanBtnState = ClrLoadingState.LOADING;
|
||||
@ -78,7 +91,7 @@ export class ResultGridComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, error => { this.errorHandler.error(error); });
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Should query from back-end service
|
||||
|
@ -18,13 +18,13 @@
|
||||
<div class="black-point-container margin-left-10">
|
||||
<div class="black-point"></div>
|
||||
</div>
|
||||
<span class="margin-left-10 font-weight-800">{{total}}</span>
|
||||
<span class="margin-left-5">{{'SCANNER.TOTAL' | translate}}</span>
|
||||
<span class="margin-left-10 font-weight-800 font-size-14">{{total}}</span>
|
||||
<span class="margin-left-5 font-size-14">{{'SCANNER.TOTAL' | translate}}</span>
|
||||
<div class="black-point-container margin-left-10">
|
||||
<div class="black-point "></div>
|
||||
</div>
|
||||
<span class="margin-left-10 font-weight-800 color-green">{{fixableCount}}</span>
|
||||
<span class="margin-left-5 color-green">{{'SCANNER.FIXABLE' | translate}}</span>
|
||||
<span class="margin-left-10 font-weight-800 color-green font-size-12">{{fixableCount}}</span>
|
||||
<span class="margin-left-5 color-green font-size-12">{{'SCANNER.FIXABLE' | translate}}</span>
|
||||
</div>
|
||||
<div *ngIf="isNone" class="margin-left-5 tip-wrapper bar-block-none shadow-none width-150">{{'VULNERABILITY.NO_VULNERABILITY' | translate }}</div>
|
||||
</div>
|
||||
|
@ -42,7 +42,8 @@ $twenty-two-pixel: 22px;
|
||||
}
|
||||
|
||||
.tip-wrapper {
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
@ -291,3 +292,9 @@ hr {
|
||||
line-height: $thirty-pixel;
|
||||
position: relative;
|
||||
}
|
||||
.font-size-14 {
|
||||
font-size: 14px;
|
||||
}
|
||||
.font-size-12 {
|
||||
font-size: 12px;
|
||||
}
|
@ -98,11 +98,11 @@ export class ResultTipHistogramComponent implements OnInit {
|
||||
let str = '';
|
||||
const min = Math.floor(this.vulnerabilitySummary.duration / MIN);
|
||||
if (min) {
|
||||
str += min + MIN_STR;
|
||||
str += min + ' ' + MIN_STR;
|
||||
}
|
||||
const sec = this.vulnerabilitySummary.duration % MIN;
|
||||
if (sec) {
|
||||
str += sec + SEC_STR;
|
||||
str += sec + ' ' + SEC_STR;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
@ -161,9 +161,6 @@ hr{
|
||||
background-color: grey;
|
||||
color:#bad7ba;
|
||||
}
|
||||
.margin-top-m15{
|
||||
margin-top: -15px;
|
||||
}
|
||||
.no-border {
|
||||
border: none;
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ export class ConfigurationScannerComponent implements OnInit, OnDestroy {
|
||||
if (this.selectedRow) {
|
||||
// Confirm deletion
|
||||
let msg: ConfirmationMessage = new ConfirmationMessage(
|
||||
"Confirm Scanner deletion",
|
||||
"SCANNER.CONFIRM_DELETION",
|
||||
"SCANNER.DELETION_SUMMARY",
|
||||
this.selectedRow.name,
|
||||
[this.selectedRow],
|
||||
|
@ -110,10 +110,10 @@
|
||||
<clr-checkbox-wrapper>
|
||||
<input name="scanner-skipCertVerify" clrCheckbox formControlName="skipCertVerify"
|
||||
type="checkbox" id="scanner-skipCertVerify">
|
||||
<label class="width-10rem" for="scanner-skipCertVerify">{{"SCANNER.SKIP" | translate}}
|
||||
<label for="scanner-skipCertVerify">{{"SCANNER.SKIP" | translate}}
|
||||
<clr-tooltip>
|
||||
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
|
||||
<clr-tooltip-content class="width-14rem" clrPosition="top-left" clrSize="lg" *clrIfOpen>
|
||||
<clr-tooltip-content clrPosition="top-left" clrSize="md" *clrIfOpen>
|
||||
{{'SCANNER.SKIP_CERT_VERIFY' | translate}}
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
@ -122,10 +122,10 @@
|
||||
<clr-checkbox-wrapper>
|
||||
<input name="scanner-use-inner" clrCheckbox formControlName="useInner"
|
||||
type="checkbox" id="scanner-use-inner">
|
||||
<label class="width-10rem" for="scanner-use-inner">{{"SCANNER.USE_INNER" | translate}}
|
||||
<label for="scanner-use-inner">{{"SCANNER.USE_INNER" | translate}}
|
||||
<clr-tooltip>
|
||||
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
|
||||
<clr-tooltip-content class="width-14rem" clrPosition="top-left" clrSize="lg" *clrIfOpen>
|
||||
<clr-tooltip-content clrPosition="top-left" clrSize="md" *clrIfOpen>
|
||||
{{"SCANNER.USE_INNER_TIP" | translate}}
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
|
@ -3,11 +3,4 @@
|
||||
}
|
||||
.padding-top-3 {
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.width-10rem {
|
||||
width: 10rem;
|
||||
}
|
||||
.width-14rem {
|
||||
width: 14rem;
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import { ClrLoadingState } from "@clr/angular";
|
||||
import { finalize } from "rxjs/operators";
|
||||
import { InlineAlertComponent } from "../../../shared/inline-alert/inline-alert.component";
|
||||
import { MessageHandlerService } from "../../../shared/message-handler/message-handler.service";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
|
||||
@Component({
|
||||
selector: "new-scanner-modal",
|
||||
@ -29,7 +30,8 @@ export class NewScannerModalComponent {
|
||||
@ViewChild(InlineAlertComponent, { static: false }) inlineAlert: InlineAlertComponent;
|
||||
constructor(
|
||||
private configScannerService: ConfigScannerService,
|
||||
private msgHandler: MessageHandlerService
|
||||
private msgHandler: MessageHandlerService,
|
||||
private translate: TranslateService,
|
||||
) {}
|
||||
open(): void {
|
||||
// reset
|
||||
@ -192,8 +194,12 @@ export class NewScannerModalComponent {
|
||||
this.checkBtnState = ClrLoadingState.SUCCESS;
|
||||
this.testMap[this.newScannerFormComponent.newScannerForm.get('url').value] = true;
|
||||
}, error => {
|
||||
this.inlineAlert.showInlineError({
|
||||
message: "SCANNER.TEST_FAILED"
|
||||
this.translate.get("SCANNER.TEST_FAILED",
|
||||
{
|
||||
name: this.newScannerFormComponent.newScannerForm.get('name').value,
|
||||
url: this.newScannerFormComponent.newScannerForm.get('url').value
|
||||
}).subscribe((res: string) => {
|
||||
this.inlineAlert.showInlineError(res);
|
||||
});
|
||||
this.checkBtnState = ClrLoadingState.ERROR;
|
||||
});
|
||||
|
@ -135,9 +135,16 @@ export class AddRuleComponent implements OnInit, OnDestroy {
|
||||
this.rule.scope_selectors.repository[0].pattern = this.rule.scope_selectors.repository[0].pattern.replace(/\s+/g, "");
|
||||
this.rule.tag_selectors[0].pattern = this.rule.tag_selectors[0].pattern.replace(/\s+/g, "");
|
||||
if (this.rule.scope_selectors.repository[0].decoration !== "repoMatches"
|
||||
&& this.rule.scope_selectors.repository[0].pattern.indexOf("**") !== -1) {
|
||||
this.inlineAlert.showInlineError(INVALID_RULE);
|
||||
return;
|
||||
&& this.rule.scope_selectors.repository[0].pattern) {
|
||||
let str = this.rule.scope_selectors.repository[0].pattern;
|
||||
str = str.replace(/[{}]/g, "");
|
||||
const arr = str.split(',');
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i] && arr[i].trim() && arr[i] === "**") {
|
||||
this.inlineAlert.showInlineError(INVALID_RULE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.isExistingRule()) {
|
||||
this.inlineAlert.showInlineError(EXISTING_RULE);
|
||||
|
@ -184,9 +184,16 @@ export class AddRuleComponent implements OnInit, OnDestroy {
|
||||
this.rule.scope_selectors.repository[0].pattern = this.rule.scope_selectors.repository[0].pattern.replace(/\s+/g, "");
|
||||
this.rule.tag_selectors[0].pattern = this.rule.tag_selectors[0].pattern.replace(/\s+/g, "");
|
||||
if (this.rule.scope_selectors.repository[0].decoration !== "repoMatches"
|
||||
&& this.rule.scope_selectors.repository[0].pattern.indexOf("**") !== -1) {
|
||||
this.inlineAlert.showInlineError(INVALID_RULE);
|
||||
return;
|
||||
&& this.rule.scope_selectors.repository[0].pattern) {
|
||||
let str = this.rule.scope_selectors.repository[0].pattern;
|
||||
str = str.replace(/[{}]/g, "");
|
||||
const arr = str.split(',');
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i] && arr[i].trim() && arr[i] === "**") {
|
||||
this.inlineAlert.showInlineError(INVALID_RULE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.isExistingRule()) {
|
||||
this.inlineAlert.showInlineError(EXISTING_RULE);
|
||||
|
@ -1291,7 +1291,7 @@
|
||||
"TEST_CONNECTION": "TEST CONNECTION",
|
||||
"ADD_SUCCESS": "Successfully added ",
|
||||
"TEST_PASS": "Test passed",
|
||||
"TEST_FAILED": "Test failed",
|
||||
"TEST_FAILED": "Ping: registration {{name}}:{{url}} is unreachable",
|
||||
"UPDATE_SUCCESS": "Successfully updated",
|
||||
"SCANNER_COLON": "Scanner:",
|
||||
"NAME_COLON": "Name:",
|
||||
@ -1327,6 +1327,7 @@
|
||||
"OPTIONS": "Options",
|
||||
"USE_INNER": "Use internal registry address",
|
||||
"USE_INNER_TIP": "If the option is checked, the scanner will be forced to use the internal registry address to access the related contents.",
|
||||
"VULNERABILITY_SEVERITY": "Vulnerability severity:"
|
||||
"VULNERABILITY_SEVERITY": "Vulnerability severity:",
|
||||
"CONFIRM_DELETION": "Confirm Scanner deletion"
|
||||
}
|
||||
}
|
||||
|
@ -1288,7 +1288,7 @@
|
||||
"TEST_CONNECTION": "TEST CONNECTION",
|
||||
"ADD_SUCCESS": "Successfully added ",
|
||||
"TEST_PASS": "Test passed",
|
||||
"TEST_FAILED": "Test failed",
|
||||
"TEST_FAILED": "Ping: registration {{name}}:{{url}} is unreachable",
|
||||
"UPDATE_SUCCESS": "Successfully updated",
|
||||
"SCANNER_COLON": "Scanner:",
|
||||
"NAME_COLON": "Name:",
|
||||
@ -1324,6 +1324,7 @@
|
||||
"OPTIONS": "Options",
|
||||
"USE_INNER": "Use internal registry address",
|
||||
"USE_INNER_TIP": "If the option is checked, the scanner will be forced to use the internal registry address to access the related contents.",
|
||||
"VULNERABILITY_SEVERITY": "Vulnerability severity:"
|
||||
"VULNERABILITY_SEVERITY": "Vulnerability severity:",
|
||||
"CONFIRM_DELETION": "Confirm Scanner deletion"
|
||||
}
|
||||
}
|
||||
|
@ -1260,7 +1260,7 @@
|
||||
"TEST_CONNECTION": "TEST CONNECTION",
|
||||
"ADD_SUCCESS": "Successfully added ",
|
||||
"TEST_PASS": "Test passed",
|
||||
"TEST_FAILED": "Test failed",
|
||||
"TEST_FAILED": "Ping: registration {{name}}:{{url}} is unreachable",
|
||||
"UPDATE_SUCCESS": "Successfully updated",
|
||||
"SCANNER_COLON": "Scanner:",
|
||||
"NAME_COLON": "Name:",
|
||||
@ -1296,6 +1296,7 @@
|
||||
"OPTIONS": "Options",
|
||||
"USE_INNER": "Use internal registry address",
|
||||
"USE_INNER_TIP": "If the option is checked, the scanner will be forced to use the internal registry address to access the related contents.",
|
||||
"VULNERABILITY_SEVERITY": "Vulnerability severity:"
|
||||
"VULNERABILITY_SEVERITY": "Vulnerability severity:",
|
||||
"CONFIRM_DELETION": "Confirm Scanner deletion"
|
||||
}
|
||||
}
|
||||
|
@ -1285,7 +1285,7 @@
|
||||
"TEST_CONNECTION": "TEST CONNECTION",
|
||||
"ADD_SUCCESS": "Successfully added ",
|
||||
"TEST_PASS": "Test passed",
|
||||
"TEST_FAILED": "Test failed",
|
||||
"TEST_FAILED": "Ping: registration {{name}}:{{url}} is unreachable",
|
||||
"UPDATE_SUCCESS": "Successfully updated",
|
||||
"SCANNER_COLON": "Scanner:",
|
||||
"NAME_COLON": "Name:",
|
||||
@ -1321,7 +1321,8 @@
|
||||
"OPTIONS": "Options",
|
||||
"USE_INNER": "Use internal registry address",
|
||||
"USE_INNER_TIP": "If the option is checked, the scanner will be forced to use the internal registry address to access the related contents.",
|
||||
"VULNERABILITY_SEVERITY": "Vulnerability severity:"
|
||||
"VULNERABILITY_SEVERITY": "Vulnerability severity:",
|
||||
"CONFIRM_DELETION": "Confirm Scanner deletion"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1290,7 +1290,7 @@
|
||||
"TEST_CONNECTION": "TEST CONNECTION",
|
||||
"ADD_SUCCESS": "Successfully added ",
|
||||
"TEST_PASS": "Test passed",
|
||||
"TEST_FAILED": "Test failed",
|
||||
"TEST_FAILED": "Ping: registration {{name}}:{{url}} is unreachable",
|
||||
"UPDATE_SUCCESS": "Successfully updated",
|
||||
"SCANNER_COLON": "Scanner:",
|
||||
"NAME_COLON": "Name:",
|
||||
@ -1326,6 +1326,7 @@
|
||||
"OPTIONS": "Options",
|
||||
"USE_INNER": "Use internal registry address",
|
||||
"USE_INNER_TIP": "If the option is checked, the scanner will be forced to use the internal registry address to access the related contents.",
|
||||
"VULNERABILITY_SEVERITY": "Vulnerability severity:"
|
||||
"VULNERABILITY_SEVERITY": "Vulnerability severity:",
|
||||
"CONFIRM_DELETION": "Confirm Scanner deletion"
|
||||
}
|
||||
}
|
||||
|
@ -1287,7 +1287,7 @@
|
||||
"TEST_CONNECTION": "测试连接",
|
||||
"ADD_SUCCESS": "添加成功",
|
||||
"TEST_PASS": "测试成功",
|
||||
"TEST_FAILED": "测试失败",
|
||||
"TEST_FAILED": "Ping: 目标地址{{name}}:{{url}}连接失败",
|
||||
"UPDATE_SUCCESS": "更新成功",
|
||||
"SCANNER_COLON": "扫描器:",
|
||||
"NAME_COLON": "Name:",
|
||||
@ -1323,6 +1323,7 @@
|
||||
"OPTIONS": "选项",
|
||||
"USE_INNER": "使用仓库内部地址",
|
||||
"USE_INNER_TIP": "选中此项,扫描器将使用仓库内部地址访问其相关内容",
|
||||
"VULNERABILITY_SEVERITY": "漏洞严重度:"
|
||||
"VULNERABILITY_SEVERITY": "漏洞严重度:",
|
||||
"CONFIRM_DELETION": "删除扫描器确认"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user