mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-18 16:25:16 +01:00
Refactor scan all page
Signed-off-by: sshijun <sshijun@vmware.com>
This commit is contained in:
parent
473b453616
commit
2225417e1f
@ -159,3 +159,14 @@ export class Configuration {
|
|||||||
this.storage_per_project = new NumberValueItem(-1, true);
|
this.storage_per_project = new NumberValueItem(-1, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ScanningMetrics {
|
||||||
|
total?: number;
|
||||||
|
completed?: number;
|
||||||
|
metrics: {
|
||||||
|
[key: string]: number;
|
||||||
|
};
|
||||||
|
requester?: string;
|
||||||
|
isScheduled?: boolean;
|
||||||
|
ongoing: boolean;
|
||||||
|
}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<clr-tab>
|
<clr-tab>
|
||||||
<button id="config-vulnerability" clrTabLink>{{'CONFIG.VULNERABILITY' | translate}}</button>
|
<button id="config-vulnerability" clrTabLink>{{'CONFIG.VULNERABILITY' | translate}}</button>
|
||||||
<clr-tab-content id="vulnerability" *clrIfActive>
|
<clr-tab-content id="vulnerability" *clrIfActive>
|
||||||
<vulnerability-config #vulnerabilityConfig [showSubTitle]="true"></vulnerability-config>
|
<vulnerability-config #vulnerabilityConfig></vulnerability-config>
|
||||||
</clr-tab-content>
|
</clr-tab-content>
|
||||||
</clr-tab>
|
</clr-tab>
|
||||||
<clr-tab>
|
<clr-tab>
|
||||||
|
@ -59,10 +59,9 @@ export class RegistryConfigComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isValid(): boolean {
|
isValid(): boolean {
|
||||||
return this.systemSettings &&
|
return !!(this.systemSettings &&
|
||||||
this.systemSettings.isValid &&
|
this.systemSettings.isValid &&
|
||||||
this.vulnerabilityCfg &&
|
this.vulnerabilityCfg);
|
||||||
this.vulnerabilityCfg.isValid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasChanges(): boolean {
|
hasChanges(): boolean {
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import {Observable, throwError as observableThrowError} from 'rxjs';
|
||||||
import { ScanApiRepository } from './scanAll.api.repository';
|
import { ScanApiRepository } from './scanAll.api.repository';
|
||||||
import { ErrorHandler } from '../../error-handler/index';
|
import { ErrorHandler } from '../../error-handler/index';
|
||||||
|
import {HttpClient} from "@angular/common/http";
|
||||||
|
import {ScanningMetrics} from "../config";
|
||||||
|
import {catchError, map} from "rxjs/operators";
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ScanAllRepoService {
|
export class ScanAllRepoService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
private scanApiRepository: ScanApiRepository,
|
private scanApiRepository: ScanApiRepository,
|
||||||
private errorHandler: ErrorHandler) {
|
private errorHandler: ErrorHandler) {
|
||||||
}
|
}
|
||||||
@ -46,4 +50,14 @@ export class ScanAllRepoService {
|
|||||||
|
|
||||||
return this.scanApiRepository.putSchedule(param);
|
return this.scanApiRepository.putSchedule(param);
|
||||||
}
|
}
|
||||||
|
getScheduleMetrics(): Observable<ScanningMetrics> {
|
||||||
|
return this.http.get('/api/scans/schedule/metrics')
|
||||||
|
.pipe(catchError(error => observableThrowError(error)))
|
||||||
|
.pipe(map(response => response as ScanningMetrics));
|
||||||
|
}
|
||||||
|
getManualMetrics(): Observable<ScanningMetrics> {
|
||||||
|
return this.http.get('/api/scans/all/metrics')
|
||||||
|
.pipe(catchError(error => observableThrowError(error)))
|
||||||
|
.pipe(map(response => response as ScanningMetrics));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,37 @@
|
|||||||
<form #systemConfigFrom="ngForm" class="compact">
|
<section class="form-block">
|
||||||
<section class="form-block">
|
<div class="button-group">
|
||||||
<div class="button-group">
|
<cron-selection [labelCurrent]="getLabelCurrent" [labelEdit]='getLabelCurrent'
|
||||||
<cron-selection #CronScheduleComponent [labelCurrent]="getLabelCurrent" [labelEdit]='getLabelCurrent' [originCron]='originCron' (inputvalue)="scanAll($event)"></cron-selection>
|
[originCron]='originCron' (inputvalue)="saveSchedule($event)"></cron-selection>
|
||||||
<div class="btn-scan-right btn-scan">
|
</div>
|
||||||
<button class="btn btn-outline btn-sm btn-scan" (click)="scanNow()" [disabled]="!scanAvailable">{{ 'CONFIG.SCANNING.SCAN_NOW' | translate }}</button><br>
|
</section>
|
||||||
</div>
|
<div class="clr-row">
|
||||||
|
<div class="clr-col-2 flex-200">
|
||||||
|
<div class="btn-scan-right btn-scan margin-top-16px">
|
||||||
|
<button id="scan-now" class="btn btn-outline btn-sm btn-scan" (click)="scanNow()"
|
||||||
|
[disabled]="!scanAvailable">
|
||||||
|
<span *ngIf="scanAvailable">{{ 'CONFIG.SCANNING.SCAN_NOW' | translate }}</span>
|
||||||
|
<span *ngIf="!scanAvailable">{{ 'CONFIG.SCANNING.SCAN' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
<span [hidden]="!isOnScanning()" class="spinner spinner-inline margin-left-5 v-mid"></span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
</form>
|
<div class="clr-col" *ngIf="scanningMetrics && scanningMetrics.total">
|
||||||
|
<div class="total" [style.width]="totalWidth+'px'">
|
||||||
|
<span class="float-left" *ngIf="!scanningMetrics?.isScheduled">{{ 'CONFIG.SCANNING.MANUAL' | translate }}</span>
|
||||||
|
<span class="float-left" *ngIf="scanningMetrics?.isScheduled">{{ 'CONFIG.SCANNING.SCHEDULED' | translate }}</span>
|
||||||
|
<span>{{ 'SCANNER.TOTAL' | translate }}</span>
|
||||||
|
<span class="margin-left-5">{{scanningMetrics?.total}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="container" [style.width]="totalWidth+'px'">
|
||||||
|
<div class="error h-100" [style.width]="errorWidth()"></div>
|
||||||
|
<div class="finished h-100" [style.width]="finishedWidth()"></div>
|
||||||
|
<div class="in-progress h-100" [style.width]="runningWidth()"></div>
|
||||||
|
</div>
|
||||||
|
<div class="state-container" [style.width]="totalWidth+'px'">
|
||||||
|
<div class="clr-row m-0" *ngFor="let item of scanningMetrics?.metrics | keyvalue">
|
||||||
|
<div class="state">{{getI18nKey(item?.key)|translate}}</div>
|
||||||
|
<div class="value">{{item?.value}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@ -48,4 +48,53 @@
|
|||||||
font-size: .541667rem;
|
font-size: .541667rem;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
.margin-left-5 {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.margin-top-16px {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.v-mid {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.flex-200 {
|
||||||
|
flex: 0 0 200px;
|
||||||
|
}
|
||||||
|
.total {
|
||||||
|
text-align: right;
|
||||||
|
line-height: 15px;
|
||||||
|
height: 15px;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-top: 7px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
height: 18px;
|
||||||
|
line-height: 18px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
height: 18px;
|
||||||
|
display: flex;
|
||||||
|
background-color:#eee;
|
||||||
|
.error {
|
||||||
|
background-color: #e62700;
|
||||||
|
}
|
||||||
|
.finished {
|
||||||
|
background-color: #62a420;
|
||||||
|
}
|
||||||
|
.in-progress {
|
||||||
|
background-color: #0079b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.state {
|
||||||
|
flex: 2;
|
||||||
|
padding-left: 3px;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
flex: 3;
|
||||||
|
}
|
||||||
|
.float-left {
|
||||||
|
float: left;
|
||||||
}
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
import { ComponentFixture, TestBed, async, fakeAsync, tick, ComponentFixtureAutoDetect } from '@angular/core/testing';
|
||||||
|
import { VulnerabilityConfigComponent } from "./vulnerability-config.component";
|
||||||
|
import { ErrorHandler, IServiceConfig, ScanningMetrics, SERVICE_CONFIG, SharedModule } from "../..";
|
||||||
|
import { ScanAllRepoService } from "./scanAll.service";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
||||||
|
|
||||||
|
let component: VulnerabilityConfigComponent;
|
||||||
|
let fixture: ComponentFixture<VulnerabilityConfigComponent>;
|
||||||
|
let config: IServiceConfig = {
|
||||||
|
configurationEndpoint: '/api/configurations/testing'
|
||||||
|
};
|
||||||
|
let mockedSchedule = {"schedule": null};
|
||||||
|
let mockedScheduledMetrics: ScanningMetrics = {
|
||||||
|
total: 50,
|
||||||
|
completed: 50,
|
||||||
|
metrics: {
|
||||||
|
"Success": 20,
|
||||||
|
"Error": 30,
|
||||||
|
},
|
||||||
|
ongoing: false
|
||||||
|
};
|
||||||
|
let mockedManualMetrics: ScanningMetrics = {
|
||||||
|
total: 100,
|
||||||
|
completed: 20,
|
||||||
|
metrics: {
|
||||||
|
"Error": 10,
|
||||||
|
"Success": 20,
|
||||||
|
"Running": 70
|
||||||
|
},
|
||||||
|
ongoing: true
|
||||||
|
};
|
||||||
|
let fakedScanAllRepoService = {
|
||||||
|
getSchedule() {
|
||||||
|
return of(mockedSchedule);
|
||||||
|
},
|
||||||
|
getScheduleMetrics() {
|
||||||
|
return of(mockedScheduledMetrics);
|
||||||
|
},
|
||||||
|
getManualMetrics() {
|
||||||
|
return of(mockedManualMetrics);
|
||||||
|
},
|
||||||
|
manualScan() {
|
||||||
|
return of(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let fakedErrorHandler = {
|
||||||
|
info() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('VulnerabilityConfigComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
SharedModule,
|
||||||
|
],
|
||||||
|
schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
VulnerabilityConfigComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{provide: ErrorHandler, useValue: fakedErrorHandler},
|
||||||
|
{provide: ScanAllRepoService, useValue: fakedScanAllRepoService},
|
||||||
|
{ provide: SERVICE_CONFIG, useValue: config },
|
||||||
|
// open auto detect
|
||||||
|
{ provide: ComponentFixtureAutoDetect, useValue: true }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(VulnerabilityConfigComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
const ele = fixture.nativeElement.querySelector('.finished');
|
||||||
|
expect(ele.style.width).toEqual('40px');
|
||||||
|
});
|
||||||
|
it('should loop scheduled metrics if scheduled scanning is on going', () => {
|
||||||
|
component.scanningMetrics.isScheduled = true;
|
||||||
|
component.scanningMetrics.ongoing = true;
|
||||||
|
expect(component.scanAvailable).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('will trigger scan now and get manual metrics', () => {
|
||||||
|
const button = fixture.nativeElement.querySelector('#scan-now');
|
||||||
|
button.click();
|
||||||
|
const ele = fixture.nativeElement.querySelector('.finished');
|
||||||
|
expect(ele.style.width).toEqual('40px');
|
||||||
|
});
|
||||||
|
it('should stop looping manual metrics if manual scanning is finished', () => {
|
||||||
|
component.scanningMetrics.isScheduled = false;
|
||||||
|
component.scanningMetrics.ongoing = false;
|
||||||
|
expect(component.scanAvailable).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -1,64 +1,93 @@
|
|||||||
import { Component, Input, Output, EventEmitter, ViewChild, OnInit } from '@angular/core';
|
import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core';
|
||||||
import { NgForm } from '@angular/forms';
|
import { finalize } from "rxjs/operators";
|
||||||
import { map, catchError, finalize } from "rxjs/operators";
|
import { ScanningMetrics } from '../config';
|
||||||
import { Observable, throwError as observableThrowError, of } from "rxjs";
|
|
||||||
import { Configuration } from '../config';
|
|
||||||
import {
|
|
||||||
ScanningResultService,
|
|
||||||
SystemInfo,
|
|
||||||
SystemInfoService,
|
|
||||||
ConfigurationService
|
|
||||||
} from '../../service/index';
|
|
||||||
import { ErrorHandler } from '../../error-handler/index';
|
import { ErrorHandler } from '../../error-handler/index';
|
||||||
import { isEmptyObject, clone} from '../../utils';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { ClairDetail } from '../../service/interface';
|
|
||||||
import { ScanAllRepoService } from './scanAll.service';
|
import { ScanAllRepoService } from './scanAll.service';
|
||||||
import { OriginCron } from '../../service/interface';
|
import { OriginCron } from '../../service/interface';
|
||||||
import { CronScheduleComponent } from "../../cron-schedule/cron-schedule.component";
|
import { CronScheduleComponent } from "../../cron-schedule/cron-schedule.component";
|
||||||
const ONE_HOUR_SECONDS: number = 3600;
|
import { VULNERABILITY_SCAN_STATUS } from "../../utils";
|
||||||
const ONE_DAY_SECONDS: number = 24 * ONE_HOUR_SECONDS;
|
import { errorHandler as errorHandFn} from "../../shared/shared.utils";
|
||||||
|
|
||||||
const SCHEDULE_TYPE_NONE = "None";
|
const SCHEDULE_TYPE_NONE = "None";
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'vulnerability-config',
|
selector: 'vulnerability-config',
|
||||||
templateUrl: './vulnerability-config.component.html',
|
templateUrl: './vulnerability-config.component.html',
|
||||||
styleUrls: ['./vulnerability-config.component.scss', '../registry-config.component.scss']
|
styleUrls: ['./vulnerability-config.component.scss', '../registry-config.component.scss']
|
||||||
})
|
})
|
||||||
export class VulnerabilityConfigComponent implements OnInit {
|
export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
|
||||||
onGoing: boolean;
|
onGoing: boolean;
|
||||||
_localTime: Date = new Date();
|
|
||||||
originCron: OriginCron;
|
originCron: OriginCron;
|
||||||
schedule: any;
|
schedule: any;
|
||||||
onSubmitting: boolean = false;
|
onSubmitting: boolean = false;
|
||||||
config: Configuration;
|
|
||||||
openState: boolean = false;
|
openState: boolean = false;
|
||||||
getLabelCurrent: string;
|
getLabelCurrent: string;
|
||||||
|
|
||||||
@ViewChild(CronScheduleComponent, {static: false})
|
@ViewChild(CronScheduleComponent, {static: false})
|
||||||
CronScheduleComponent: CronScheduleComponent;
|
CronScheduleComponent: CronScheduleComponent;
|
||||||
|
gettingMetrics: boolean;
|
||||||
@Input()
|
private _scanningMetrics: ScanningMetrics;
|
||||||
|
totalWidth: number = 200;
|
||||||
@Input() showSubTitle: boolean = false;
|
i18nKeyMap = {
|
||||||
@Input() showScanningNamespaces: boolean = false;
|
"Pending": "CONFIG.SCANNING.STATUS.PENDING",
|
||||||
@Output() loadingStatus = new EventEmitter<boolean>();
|
"Running": "CONFIG.SCANNING.STATUS.RUNNING",
|
||||||
systemInfo: SystemInfo;
|
"Stopped": "CONFIG.SCANNING.STATUS.STOPPED",
|
||||||
|
"Error": "CONFIG.SCANNING.STATUS.ERROR",
|
||||||
|
"Success": "CONFIG.SCANNING.STATUS.SUCCESS",
|
||||||
|
"Scheduled": "CONFIG.SCANNING.STATUS.SCHEDULED"
|
||||||
|
};
|
||||||
|
private _loopScheduleInterval;
|
||||||
|
private _loopManualInterval;
|
||||||
constructor(
|
constructor(
|
||||||
// private scanningService: ScanningResultService,
|
|
||||||
private scanningService: ScanAllRepoService,
|
private scanningService: ScanAllRepoService,
|
||||||
private errorHandler: ErrorHandler,
|
private errorHandler: ErrorHandler,
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
private systemInfoService: SystemInfoService,
|
|
||||||
private configService: ConfigurationService
|
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
get scanningMetrics(): ScanningMetrics {
|
||||||
|
return this._scanningMetrics;
|
||||||
|
}
|
||||||
|
set scanningMetrics(metrics: ScanningMetrics) {
|
||||||
|
// start looping scheduled metrics
|
||||||
|
if (metrics && metrics.ongoing && metrics.isScheduled) {
|
||||||
|
if (!this._loopScheduleInterval) {
|
||||||
|
this._loopScheduleInterval = setInterval(() => {
|
||||||
|
this.getScheduleMetrics();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// stop looping scheduled metrics
|
||||||
|
if (metrics && !metrics.ongoing && metrics.isScheduled) {
|
||||||
|
if (this._loopScheduleInterval) {
|
||||||
|
clearInterval(this._loopScheduleInterval);
|
||||||
|
this._loopScheduleInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// start looping manual metrics
|
||||||
|
if (metrics && metrics.ongoing && !metrics.isScheduled) {
|
||||||
|
if (!this._loopManualInterval) {
|
||||||
|
this._loopManualInterval = setInterval(() => {
|
||||||
|
this.getManualMetrics();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// stop looping manual metrics
|
||||||
|
if (metrics && !metrics.ongoing && !metrics.isScheduled) {
|
||||||
|
if (this._loopManualInterval) {
|
||||||
|
clearInterval(this._loopManualInterval);
|
||||||
|
this._loopManualInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._scanningMetrics = metrics;
|
||||||
|
}
|
||||||
get scanAvailable(): boolean {
|
get scanAvailable(): boolean {
|
||||||
return !this.onSubmitting;
|
return !this.onSubmitting
|
||||||
|
&& !this.gettingMetrics
|
||||||
|
&& !this.isOnScanning();
|
||||||
}
|
}
|
||||||
|
|
||||||
getScanText() {
|
getScanText() {
|
||||||
this.translate.get('CONFIG.SCANNING.SCAN_ALL').subscribe((res: string) => {
|
this.translate.get('CONFIG.SCANNING.SCHEDULE_TO_SCAN_ALL').subscribe((res: string) => {
|
||||||
this.getLabelCurrent = res;
|
this.getLabelCurrent = res;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -67,7 +96,6 @@ export class VulnerabilityConfigComponent implements OnInit {
|
|||||||
this.scanningService.getSchedule()
|
this.scanningService.getSchedule()
|
||||||
.pipe(finalize(() => {
|
.pipe(finalize(() => {
|
||||||
this.onGoing = false;
|
this.onGoing = false;
|
||||||
this.loadingStatus.emit(this.onGoing);
|
|
||||||
}))
|
}))
|
||||||
.subscribe(schedule => {
|
.subscribe(schedule => {
|
||||||
this.initSchedule(schedule);
|
this.initSchedule(schedule);
|
||||||
@ -87,31 +115,103 @@ export class VulnerabilityConfigComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ViewChild("systemConfigFrom", {static: false}) systemSettingsForm: NgForm;
|
|
||||||
|
|
||||||
get isValid(): boolean {
|
|
||||||
return this.systemSettingsForm && this.systemSettingsForm.valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.getSystemInfo();
|
|
||||||
this.getScanText();
|
this.getScanText();
|
||||||
this.getSchedule();
|
this.getSchedule();
|
||||||
|
this.initMetrics();
|
||||||
}
|
}
|
||||||
|
|
||||||
getSystemInfo(): void {
|
ngOnDestroy() {
|
||||||
this.systemInfoService.getSystemInfo()
|
if (this._loopScheduleInterval) {
|
||||||
.subscribe((info: SystemInfo) => (this.systemInfo = info)
|
clearInterval(this._loopScheduleInterval);
|
||||||
, error => this.errorHandler.error(error));
|
this._loopScheduleInterval = null;
|
||||||
|
}
|
||||||
|
if (this._loopManualInterval) {
|
||||||
|
clearInterval(this._loopManualInterval);
|
||||||
|
this._loopManualInterval = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
isOnScanning(): boolean {
|
||||||
convertToLocalTime(utcTime: number): Date {
|
return this.scanningMetrics
|
||||||
let dt: Date = new Date();
|
&& this.scanningMetrics.ongoing;
|
||||||
dt.setTime(utcTime * 1000);
|
}
|
||||||
|
getScheduleMetrics() {
|
||||||
return dt;
|
this.gettingMetrics = true;
|
||||||
|
this.scanningService.getScheduleMetrics()
|
||||||
|
.pipe(finalize(() => this.gettingMetrics = false))
|
||||||
|
.subscribe(response => {
|
||||||
|
if (response) {
|
||||||
|
response.isScheduled = true;
|
||||||
|
this.scanningMetrics = response;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getManualMetrics() {
|
||||||
|
this.gettingMetrics = true;
|
||||||
|
this.scanningService.getManualMetrics()
|
||||||
|
.pipe(finalize(() => this.gettingMetrics = false))
|
||||||
|
.subscribe(response => {
|
||||||
|
if (response) {
|
||||||
|
response.isScheduled = false;
|
||||||
|
this.scanningMetrics = response;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
initMetrics() {
|
||||||
|
// get scheduled metrics first
|
||||||
|
this.scanningService.getScheduleMetrics()
|
||||||
|
.pipe(finalize(() => this.gettingMetrics = false))
|
||||||
|
.subscribe(response => {
|
||||||
|
// if scheduled scanning is on going
|
||||||
|
if (response && response.ongoing) {
|
||||||
|
response.isScheduled = true;
|
||||||
|
this.scanningMetrics = response;
|
||||||
|
} else {
|
||||||
|
this.getManualMetrics();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
this.errorHandler.error(error);
|
||||||
|
// if error, get manual metrics
|
||||||
|
this.getManualMetrics();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getI18nKey(str: string): string {
|
||||||
|
if (str && this.i18nKeyMap[str]) {
|
||||||
|
return this.i18nKeyMap[str];
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
errorWidth() {
|
||||||
|
if (this.scanningMetrics
|
||||||
|
&& this.scanningMetrics.metrics
|
||||||
|
&& this.scanningMetrics.total
|
||||||
|
&& this.scanningMetrics.metrics[VULNERABILITY_SCAN_STATUS.ERROR]) {
|
||||||
|
return this.scanningMetrics.metrics[VULNERABILITY_SCAN_STATUS.ERROR] /
|
||||||
|
this.scanningMetrics.total * this.totalWidth + 'px';
|
||||||
|
}
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
finishedWidth() {
|
||||||
|
if (this.scanningMetrics
|
||||||
|
&& this.scanningMetrics.metrics
|
||||||
|
&& this.scanningMetrics.total
|
||||||
|
&& this.scanningMetrics.metrics[VULNERABILITY_SCAN_STATUS.SUCCESS]) {
|
||||||
|
return this.scanningMetrics.metrics[VULNERABILITY_SCAN_STATUS.SUCCESS] /
|
||||||
|
this.scanningMetrics.total * this.totalWidth + 'px';
|
||||||
|
}
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
runningWidth() {
|
||||||
|
if (this.scanningMetrics
|
||||||
|
&& this.scanningMetrics.metrics
|
||||||
|
&& this.scanningMetrics.total
|
||||||
|
&& this.scanningMetrics.metrics[VULNERABILITY_SCAN_STATUS.RUNNING]) {
|
||||||
|
return this.scanningMetrics.metrics[VULNERABILITY_SCAN_STATUS.RUNNING] /
|
||||||
|
this.scanningMetrics.total * this.totalWidth + 'px';
|
||||||
|
}
|
||||||
|
return '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
scanNow(): void {
|
scanNow(): void {
|
||||||
if (this.onSubmitting) {
|
if (this.onSubmitting) {
|
||||||
return; // Aoid duplicated submitting
|
return; // Aoid duplicated submitting
|
||||||
@ -123,28 +223,22 @@ export class VulnerabilityConfigComponent implements OnInit {
|
|||||||
|
|
||||||
this.onSubmitting = true;
|
this.onSubmitting = true;
|
||||||
this.scanningService.manualScan()
|
this.scanningService.manualScan()
|
||||||
|
.pipe(finalize(() => this.onSubmitting = false))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.translate.get("CONFIG.SCANNING.TRIGGER_SCAN_ALL_SUCCESS").subscribe((res: string) => {
|
this.translate.get("CONFIG.SCANNING.TRIGGER_SCAN_ALL_SUCCESS").subscribe((res: string) => {
|
||||||
this.errorHandler.info(res);
|
this.errorHandler.info(res);
|
||||||
});
|
});
|
||||||
|
this.getManualMetrics();
|
||||||
// Update system info
|
|
||||||
this.systemInfoService.getSystemInfo()
|
|
||||||
.subscribe(() => {
|
|
||||||
this.onSubmitting = false;
|
|
||||||
}, error => {
|
|
||||||
this.onSubmitting = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
, error => {
|
, error => {
|
||||||
if (error && error.status && error.status === 412) {
|
if (error && error.status && error.status === 412) {
|
||||||
this.translate.get("CONFIG.SCANNING.TRIGGER_SCAN_ALL_FAIL", { error: '' + error }).subscribe((res: string) => {
|
this.translate.get("CONFIG.SCANNING.TRIGGER_SCAN_ALL_FAIL",
|
||||||
|
{ error: '' + errorHandFn(error) }).subscribe((res: string) => {
|
||||||
this.errorHandler.error(res);
|
this.errorHandler.error(res);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.errorHandler.error(error);
|
this.errorHandler.error(error);
|
||||||
}
|
}
|
||||||
this.onSubmitting = false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,7 +251,7 @@ export class VulnerabilityConfigComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
scanAll(cron: string): void {
|
saveSchedule(cron: string): void {
|
||||||
let schedule = this.schedule;
|
let schedule = this.schedule;
|
||||||
if (schedule && schedule.schedule && schedule.schedule.type !== SCHEDULE_TYPE_NONE) {
|
if (schedule && schedule.schedule && schedule.schedule.type !== SCHEDULE_TYPE_NONE) {
|
||||||
this.scanningService.putSchedule(this.CronScheduleComponent.scheduleType, cron)
|
this.scanningService.putSchedule(this.CronScheduleComponent.scheduleType, cron)
|
||||||
|
@ -41,7 +41,7 @@ export class RecentLogComponent implements OnInit {
|
|||||||
defaultFilter = "username";
|
defaultFilter = "username";
|
||||||
isOpenFilterTag: boolean;
|
isOpenFilterTag: boolean;
|
||||||
@Input() withTitle: boolean = false;
|
@Input() withTitle: boolean = false;
|
||||||
pageSize: number = 3;
|
pageSize: number = 15;
|
||||||
currentPage: number = 1; // Double bound to pagination component
|
currentPage: number = 1; // Double bound to pagination component
|
||||||
constructor(
|
constructor(
|
||||||
private logService: AccessLogService,
|
private logService: AccessLogService,
|
||||||
|
@ -876,16 +876,28 @@
|
|||||||
},
|
},
|
||||||
"SCANNING": {
|
"SCANNING": {
|
||||||
"TRIGGER_SCAN_ALL_SUCCESS": "Trigger scan all successfully!",
|
"TRIGGER_SCAN_ALL_SUCCESS": "Trigger scan all successfully!",
|
||||||
"TRIGGER_SCAN_ALL_FAIL": "Failed to trigger scan all with error: {{error}",
|
"TRIGGER_SCAN_ALL_FAIL": "Failed to trigger scan all with error: {{error}}",
|
||||||
"TITLE": "Vulnerability Scanning",
|
"TITLE": "Vulnerability Scanning",
|
||||||
"SCAN_ALL": "Scan All",
|
"SCAN_ALL": "Scan All",
|
||||||
|
"SCHEDULE_TO_SCAN_ALL": "Schedule to scan all",
|
||||||
"SCAN_NOW": "SCAN NOW",
|
"SCAN_NOW": "SCAN NOW",
|
||||||
|
"SCAN": "SCAN",
|
||||||
"NONE_POLICY": "None",
|
"NONE_POLICY": "None",
|
||||||
"DAILY_POLICY": "Daily At",
|
"DAILY_POLICY": "Daily At",
|
||||||
"REFRESH_POLICY": "Upon Refresh",
|
"REFRESH_POLICY": "Upon Refresh",
|
||||||
"DB_REFRESH_TIME": "Database updated on",
|
"DB_REFRESH_TIME": "Database updated on",
|
||||||
"DB_NOT_READY": "Vulnerability database might not be fully ready!",
|
"DB_NOT_READY": "Vulnerability database might not be fully ready!",
|
||||||
"NEXT_SCAN": "Available after"
|
"NEXT_SCAN": "Available after",
|
||||||
|
"STATUS": {
|
||||||
|
"PENDING": "Pending",
|
||||||
|
"RUNNING": "Running",
|
||||||
|
"STOPPED": "Stopped",
|
||||||
|
"ERROR": "Error",
|
||||||
|
"SUCCESS": "Success",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
|
},
|
||||||
|
"MANUAL": "Manual",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
},
|
},
|
||||||
"TEST_MAIL_SUCCESS": "Connection to mail server is verified.",
|
"TEST_MAIL_SUCCESS": "Connection to mail server is verified.",
|
||||||
"TEST_LDAP_SUCCESS": "Connection to LDAP server is verified.",
|
"TEST_LDAP_SUCCESS": "Connection to LDAP server is verified.",
|
||||||
|
@ -875,16 +875,28 @@
|
|||||||
},
|
},
|
||||||
"SCANNING": {
|
"SCANNING": {
|
||||||
"TRIGGER_SCAN_ALL_SUCCESS": "Trigger scan all successfully!",
|
"TRIGGER_SCAN_ALL_SUCCESS": "Trigger scan all successfully!",
|
||||||
"TRIGGER_SCAN_ALL_FAIL": "Failed to trigger scan all with error: {{error}",
|
"TRIGGER_SCAN_ALL_FAIL": "Failed to trigger scan all with error: {{error}}",
|
||||||
"TITLE": "Vulnerability Scanning",
|
"TITLE": "Vulnerability Scanning",
|
||||||
"SCAN_ALL": "Scan All",
|
"SCAN_ALL": "Scan All",
|
||||||
|
"SCHEDULE_TO_SCAN_ALL": "Schedule to scan all",
|
||||||
"SCAN_NOW": "SCAN NOW",
|
"SCAN_NOW": "SCAN NOW",
|
||||||
|
"SCAN": "SCAN",
|
||||||
"NONE_POLICY": "None",
|
"NONE_POLICY": "None",
|
||||||
"DAILY_POLICY": "Daily At",
|
"DAILY_POLICY": "Daily At",
|
||||||
"REFRESH_POLICY": "Upon Refresh",
|
"REFRESH_POLICY": "Upon Refresh",
|
||||||
"DB_REFRESH_TIME": "Database updated on",
|
"DB_REFRESH_TIME": "Database updated on",
|
||||||
"DB_NOT_READY": "Vulnerability database might not be fully ready!",
|
"DB_NOT_READY": "Vulnerability database might not be fully ready!",
|
||||||
"NEXT_SCAN": "Available after"
|
"NEXT_SCAN": "Available after",
|
||||||
|
"STATUS": {
|
||||||
|
"PENDING": "Pending",
|
||||||
|
"RUNNING": "Running",
|
||||||
|
"STOPPED": "Stopped",
|
||||||
|
"ERROR": "Error",
|
||||||
|
"SUCCESS": "Success",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
|
},
|
||||||
|
"MANUAL": "Manual",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
},
|
},
|
||||||
"TEST_MAIL_SUCCESS": "La conexión al servidor de correo ha sido verificada.",
|
"TEST_MAIL_SUCCESS": "La conexión al servidor de correo ha sido verificada.",
|
||||||
"TEST_LDAP_SUCCESS": "La conexión al servidor LDAP ha sido verificada.",
|
"TEST_LDAP_SUCCESS": "La conexión al servidor LDAP ha sido verificada.",
|
||||||
|
@ -849,16 +849,28 @@
|
|||||||
},
|
},
|
||||||
"SCANNING": {
|
"SCANNING": {
|
||||||
"TRIGGER_SCAN_ALL_SUCCESS": "Déclenchement d'analyse globale avec succès !",
|
"TRIGGER_SCAN_ALL_SUCCESS": "Déclenchement d'analyse globale avec succès !",
|
||||||
"TRIGGER_SCAN_ALL_FAIL": "Echec du déclenchement d'analyse globale avec des erreurs : {{error}",
|
"TRIGGER_SCAN_ALL_FAIL": "Echec du déclenchement d'analyse globale avec des erreurs : {{error}}",
|
||||||
"TITLE": "Analyse de vulnérabilité",
|
"TITLE": "Analyse de vulnérabilité",
|
||||||
"SCAN_ALL": "Analyser tout",
|
"SCAN_ALL": "Analyser tout",
|
||||||
|
"SCHEDULE_TO_SCAN_ALL": "Schedule to scan all",
|
||||||
"SCAN_NOW": "ANALYSER MAINTENANT",
|
"SCAN_NOW": "ANALYSER MAINTENANT",
|
||||||
|
"SCAN": "SCAN",
|
||||||
"NONE_POLICY": "Aucune",
|
"NONE_POLICY": "Aucune",
|
||||||
"DAILY_POLICY": "Tous les jours à",
|
"DAILY_POLICY": "Tous les jours à",
|
||||||
"REFRESH_POLICY": "Lors de la mise à jour",
|
"REFRESH_POLICY": "Lors de la mise à jour",
|
||||||
"DB_REFRESH_TIME": "Base de données mise à jour le",
|
"DB_REFRESH_TIME": "Base de données mise à jour le",
|
||||||
"DB_NOT_READY": "La base de données sur les vulnérabilités pourrait ne pas être entièrement prête !",
|
"DB_NOT_READY": "La base de données sur les vulnérabilités pourrait ne pas être entièrement prête !",
|
||||||
"NEXT_SCAN": "Disponible à partir de"
|
"NEXT_SCAN": "Disponible à partir de",
|
||||||
|
"STATUS": {
|
||||||
|
"PENDING": "Pending",
|
||||||
|
"RUNNING": "Running",
|
||||||
|
"STOPPED": "Stopped",
|
||||||
|
"ERROR": "Error",
|
||||||
|
"SUCCESS": "Success",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
|
},
|
||||||
|
"MANUAL": "Manual",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
},
|
},
|
||||||
"TEST_MAIL_SUCCESS": "La connexion au serveur de mail est vérifiée.",
|
"TEST_MAIL_SUCCESS": "La connexion au serveur de mail est vérifiée.",
|
||||||
"TEST_LDAP_SUCCESS": "La connexion au serveur LDAP est vérifiée.",
|
"TEST_LDAP_SUCCESS": "La connexion au serveur LDAP est vérifiée.",
|
||||||
|
@ -870,16 +870,28 @@
|
|||||||
},
|
},
|
||||||
"SCANNING": {
|
"SCANNING": {
|
||||||
"TRIGGER_SCAN_ALL_SUCCESS": "Disparo de análise geral efetuado com sucesso!",
|
"TRIGGER_SCAN_ALL_SUCCESS": "Disparo de análise geral efetuado com sucesso!",
|
||||||
"TRIGGER_SCAN_ALL_FAIL": "Falha ao disparar análise geral com erro: {{error}",
|
"TRIGGER_SCAN_ALL_FAIL": "Falha ao disparar análise geral com erro: {{error}}",
|
||||||
"TITLE": "Análise de vulnerabilidades",
|
"TITLE": "Análise de vulnerabilidades",
|
||||||
"SCAN_ALL": "Analisar todos",
|
"SCAN_ALL": "Analisar todos",
|
||||||
|
"SCHEDULE_TO_SCAN_ALL": "Schedule to scan all",
|
||||||
"SCAN_NOW": "ANALISAR AGORA",
|
"SCAN_NOW": "ANALISAR AGORA",
|
||||||
|
"SCAN": "SCAN",
|
||||||
"NONE_POLICY": "Nenhum",
|
"NONE_POLICY": "Nenhum",
|
||||||
"DAILY_POLICY": "Diário em",
|
"DAILY_POLICY": "Diário em",
|
||||||
"REFRESH_POLICY": "na atualização",
|
"REFRESH_POLICY": "na atualização",
|
||||||
"DB_REFRESH_TIME": "Banco de dados atualizado em",
|
"DB_REFRESH_TIME": "Banco de dados atualizado em",
|
||||||
"DB_NOT_READY": "Banco de dados de vulnerabilidade pode não estar totalmente preparado!",
|
"DB_NOT_READY": "Banco de dados de vulnerabilidade pode não estar totalmente preparado!",
|
||||||
"NEXT_SCAN": "Disponível após"
|
"NEXT_SCAN": "Disponível após",
|
||||||
|
"STATUS": {
|
||||||
|
"PENDING": "Pending",
|
||||||
|
"RUNNING": "Running",
|
||||||
|
"STOPPED": "Stopped",
|
||||||
|
"ERROR": "Error",
|
||||||
|
"SUCCESS": "Success",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
|
},
|
||||||
|
"MANUAL": "Manual",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
},
|
},
|
||||||
"TEST_MAIL_SUCCESS": "Conexão ao servidor de Email foi verificada.",
|
"TEST_MAIL_SUCCESS": "Conexão ao servidor de Email foi verificada.",
|
||||||
"TEST_LDAP_SUCCESS": "Conexão ao servidor de LDAP foi verificada.",
|
"TEST_LDAP_SUCCESS": "Conexão ao servidor de LDAP foi verificada.",
|
||||||
|
@ -875,16 +875,28 @@
|
|||||||
},
|
},
|
||||||
"SCANNING": {
|
"SCANNING": {
|
||||||
"TRIGGER_SCAN_ALL_SUCCESS": "Tümünü başarılı bir şekilde tara!",
|
"TRIGGER_SCAN_ALL_SUCCESS": "Tümünü başarılı bir şekilde tara!",
|
||||||
"TRIGGER_SCAN_ALL_FAIL": "Tüm taramayı hatayla tetikleyemedi:{{error}",
|
"TRIGGER_SCAN_ALL_FAIL": "Tüm taramayı hatayla tetikleyemedi:{{error}}",
|
||||||
"TITLE": "Güvenlik açığı taraması",
|
"TITLE": "Güvenlik açığı taraması",
|
||||||
"SCAN_ALL": "Hepsini Tara",
|
"SCAN_ALL": "Hepsini Tara",
|
||||||
|
"SCHEDULE_TO_SCAN_ALL": "Schedule to scan all",
|
||||||
"SCAN_NOW": "ŞİMDİ TARA",
|
"SCAN_NOW": "ŞİMDİ TARA",
|
||||||
|
"SCAN": "SCAN",
|
||||||
"NONE_POLICY": "Hiçbiri",
|
"NONE_POLICY": "Hiçbiri",
|
||||||
"DAILY_POLICY": "Günlük",
|
"DAILY_POLICY": "Günlük",
|
||||||
"REFRESH_POLICY": "Yenileme üzerine",
|
"REFRESH_POLICY": "Yenileme üzerine",
|
||||||
"DB_REFRESH_TIME": "Veri tabanı güncellendi",
|
"DB_REFRESH_TIME": "Veri tabanı güncellendi",
|
||||||
"DB_NOT_READY": "Güvenlik açığı veritabanı tam olarak hazır olmayabilir!",
|
"DB_NOT_READY": "Güvenlik açığı veritabanı tam olarak hazır olmayabilir!",
|
||||||
"NEXT_SCAN": "Sonra kullanılabilir"
|
"NEXT_SCAN": "Sonra kullanılabilir",
|
||||||
|
"STATUS": {
|
||||||
|
"PENDING": "Pending",
|
||||||
|
"RUNNING": "Running",
|
||||||
|
"STOPPED": "Stopped",
|
||||||
|
"ERROR": "Error",
|
||||||
|
"SUCCESS": "Success",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
|
},
|
||||||
|
"MANUAL": "Manual",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
},
|
},
|
||||||
"TEST_MAIL_SUCCESS": "Posta sunucusuyla bağlantı doğrulandı.",
|
"TEST_MAIL_SUCCESS": "Posta sunucusuyla bağlantı doğrulandı.",
|
||||||
"TEST_LDAP_SUCCESS": "LDAP sunucusuna bağlantı doğrulandı.",
|
"TEST_LDAP_SUCCESS": "LDAP sunucusuna bağlantı doğrulandı.",
|
||||||
|
@ -875,16 +875,28 @@
|
|||||||
},
|
},
|
||||||
"SCANNING": {
|
"SCANNING": {
|
||||||
"TRIGGER_SCAN_ALL_SUCCESS": "启动扫描所有镜像任务成功!",
|
"TRIGGER_SCAN_ALL_SUCCESS": "启动扫描所有镜像任务成功!",
|
||||||
"TRIGGER_SCAN_ALL_FAIL": "启动扫描所有镜像任务失败:{{error}",
|
"TRIGGER_SCAN_ALL_FAIL": "启动扫描所有镜像任务失败:{{error}}",
|
||||||
"TITLE": "缺陷扫描",
|
"TITLE": "缺陷扫描",
|
||||||
"SCAN_ALL": "扫描所有",
|
"SCAN_ALL": "扫描所有",
|
||||||
|
"SCHEDULE_TO_SCAN_ALL": "定时扫描所有",
|
||||||
"SCAN_NOW": "开始扫描",
|
"SCAN_NOW": "开始扫描",
|
||||||
|
"SCAN": "扫描",
|
||||||
"NONE_POLICY": "无",
|
"NONE_POLICY": "无",
|
||||||
"DAILY_POLICY": "每日定时",
|
"DAILY_POLICY": "每日定时",
|
||||||
"REFRESH_POLICY": "缺陷库刷新后",
|
"REFRESH_POLICY": "缺陷库刷新后",
|
||||||
"DB_REFRESH_TIME": "数据库更新于",
|
"DB_REFRESH_TIME": "数据库更新于",
|
||||||
"DB_NOT_READY": "缺陷数据库可能没有完全准备好!",
|
"DB_NOT_READY": "缺陷数据库可能没有完全准备好!",
|
||||||
"NEXT_SCAN": "下次可用时间"
|
"NEXT_SCAN": "下次可用时间",
|
||||||
|
"STATUS": {
|
||||||
|
"PENDING": "等待",
|
||||||
|
"RUNNING": "扫描中",
|
||||||
|
"STOPPED": "中止",
|
||||||
|
"ERROR": "出错",
|
||||||
|
"SUCCESS": "成功",
|
||||||
|
"SCHEDULED": "已入计划"
|
||||||
|
},
|
||||||
|
"MANUAL": "手动触发",
|
||||||
|
"SCHEDULED": "定时触发"
|
||||||
},
|
},
|
||||||
"TEST_MAIL_SUCCESS": "邮件服务器的连通正常。",
|
"TEST_MAIL_SUCCESS": "邮件服务器的连通正常。",
|
||||||
"TEST_LDAP_SUCCESS": "LDAP服务器的连通正常。",
|
"TEST_LDAP_SUCCESS": "LDAP服务器的连通正常。",
|
||||||
|
Loading…
Reference in New Issue
Block a user