Add stop scan functionality (#15528)

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
孙世军 2021-09-15 17:00:08 +08:00 committed by GitHub
parent c5003f38ba
commit 9e9c4a03bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 466 additions and 244 deletions

View File

@ -13,10 +13,10 @@
<div class="clr-row"> <div class="clr-row">
<div class="clr-col-2 flex-200"> <div class="clr-col-2 flex-200">
<div class="btn-scan-right btn-scan margin-top-16px"> <div class="btn-scan-right btn-scan margin-top-16px">
<button id="scan-now" class="btn btn-primary btn-scan" (click)="scanNow()" <button id="scan" class="btn btn-primary btn-scan" (click)="scanOrStop()"
[disabled]="!scanAvailable"> [disabled]="onSubmitting || !hasDefaultScanner">
<span *ngIf="scanAvailable">{{ 'CONFIG.SCANNING.SCAN_NOW' | translate }}</span> <span *ngIf="!isOnScanning()">{{ 'CONFIG.SCANNING.SCAN_NOW' | translate }}</span>
<span *ngIf="!scanAvailable">{{ 'CONFIG.SCANNING.SCAN' | translate }}</span> <span *ngIf="isOnScanning()">{{ 'VULNERABILITY.STOP_NOW' | translate }}</span>
</button> </button>
<span [hidden]="!isOnScanning()" class="spinner spinner-inline margin-left-5 v-mid"></span> <span [hidden]="!isOnScanning()" class="spinner spinner-inline margin-left-5 v-mid"></span>
</div> </div>

View File

@ -4,9 +4,10 @@ import { ScanAllRepoService } from "./scanAll.service";
import { of } from "rxjs"; import { of } from "rxjs";
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { ScanningMetrics, Triggers } from "../../config/config"; import { ScanningMetrics, Triggers } from "../../config/config";
import { ErrorHandler } from "../../../../shared/units/error-handler";
import { Scanner } from "../scanner/scanner"; import { Scanner } from "../scanner/scanner";
import { SharedTestingModule } from "../../../../shared/shared.module"; import { SharedTestingModule } from "../../../../shared/shared.module";
import { delay } from "rxjs/operators";
import { ScanAllService } from "../../../../../../ng-swagger-gen/services/scan-all.service";
let component: VulnerabilityConfigComponent; let component: VulnerabilityConfigComponent;
let fixture: ComponentFixture<VulnerabilityConfigComponent>; let fixture: ComponentFixture<VulnerabilityConfigComponent>;
@ -51,9 +52,10 @@ let fakedScanAllRepoService = {
return of(null); return of(null);
} }
}; };
let fakedErrorHandler = {
info() { const fakedScanAllService = {
return null; stopScanAll() {
return of(null);
} }
}; };
@ -70,8 +72,8 @@ describe('VulnerabilityConfigComponent', () => {
VulnerabilityConfigComponent VulnerabilityConfigComponent
], ],
providers: [ providers: [
{provide: ErrorHandler, useValue: fakedErrorHandler},
{provide: ScanAllRepoService, useValue: fakedScanAllRepoService}, {provide: ScanAllRepoService, useValue: fakedScanAllRepoService},
{provide: ScanAllService, useValue: fakedScanAllService},
// open auto detect // open auto detect
{ provide: ComponentFixtureAutoDetect, useValue: true } { provide: ComponentFixtureAutoDetect, useValue: true }
] ]
@ -94,7 +96,7 @@ describe('VulnerabilityConfigComponent', () => {
expect(component.scanAvailable).toBeFalsy(); expect(component.scanAvailable).toBeFalsy();
}); });
it('will trigger scan now and get manual metrics', () => { it('will trigger scan now and get manual metrics', () => {
const button = fixture.nativeElement.querySelector('#scan-now'); const button = fixture.nativeElement.querySelector('#scan');
button.click(); button.click();
const ele = fixture.nativeElement.querySelector('.finished'); const ele = fixture.nativeElement.querySelector('.finished');
expect(ele.style.width).toEqual('80px'); expect(ele.style.width).toEqual('80px');
@ -104,5 +106,11 @@ describe('VulnerabilityConfigComponent', () => {
component.scanningMetrics.ongoing = false; component.scanningMetrics.ongoing = false;
component.hasDefaultScanner = true; component.hasDefaultScanner = true;
expect(component.scanAvailable).toBeTruthy(); expect(component.scanAvailable).toBeTruthy();
const spnManual: HTMLSpanElement = fixture.nativeElement.querySelector(".float-left");
expect(spnManual.innerText).toEqual('CONFIG.SCANNING.SCHEDULED');
const scanBtn: HTMLButtonElement = fixture.nativeElement.querySelector("#scan");
scanBtn.click();
const spnSchedule: HTMLSpanElement = fixture.nativeElement.querySelector(".float-left");
expect(spnSchedule.innerText).toEqual('CONFIG.SCANNING.MANUAL');
}); });
}); });

View File

@ -1,4 +1,4 @@
import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core'; import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { finalize } from "rxjs/operators"; import { finalize } from "rxjs/operators";
import { ScanningMetrics, Triggers } from '../../config/config'; import { ScanningMetrics, Triggers } from '../../config/config';
import { ErrorHandler } from '../../../../shared/units/error-handler'; import { ErrorHandler } from '../../../../shared/units/error-handler';
@ -9,9 +9,11 @@ import { CronScheduleComponent } from "../../../../shared/components/cron-schedu
import { DATABASE_UPDATED_PROPERTY, VULNERABILITY_SCAN_STATUS } from "../../../../shared/units/utils"; import { DATABASE_UPDATED_PROPERTY, VULNERABILITY_SCAN_STATUS } from "../../../../shared/units/utils";
import { DatePipe } from "@angular/common"; import { DatePipe } from "@angular/common";
import { errorHandler } from "../../../../shared/units/shared.utils"; import { errorHandler } from "../../../../shared/units/shared.utils";
import { ScanAllService } from "../../../../../../ng-swagger-gen/services/scan-all.service";
const SCHEDULE_TYPE_NONE = "None"; const SCHEDULE_TYPE_NONE = "None";
const TIMEOUT: number = 5000; const TIMEOUT: number = 5000;
@Component({ @Component({
selector: 'vulnerability-config', selector: 'vulnerability-config',
templateUrl: './vulnerability-config.component.html', templateUrl: './vulnerability-config.component.html',
@ -42,18 +44,23 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
onGettingUpdatedTimeStr: boolean; onGettingUpdatedTimeStr: boolean;
hasDefaultScanner: boolean = false; hasDefaultScanner: boolean = false;
private _timeout: any; private _timeout: any;
constructor( constructor(
private scanningService: ScanAllRepoService, private scanningService: ScanAllRepoService,
private newScanAllService: ScanAllService,
private errorHandlerEntity: ErrorHandler, private errorHandlerEntity: ErrorHandler,
private translate: TranslateService, private translate: TranslateService,
) { } ) {
}
get scanningMetrics(): ScanningMetrics { get scanningMetrics(): ScanningMetrics {
return this._scanningMetrics; return this._scanningMetrics;
} }
set scanningMetrics(metrics: ScanningMetrics) { set scanningMetrics(metrics: ScanningMetrics) {
this._scanningMetrics = metrics; this._scanningMetrics = metrics;
} }
get scanAvailable(): boolean { get scanAvailable(): boolean {
return !this.onSubmitting return !this.onSubmitting
&& !this.gettingMetrics && !this.gettingMetrics
@ -66,6 +73,7 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
this.getLabelCurrent = res; this.getLabelCurrent = res;
}); });
} }
getSchedule() { getSchedule() {
this.onGoing = true; this.onGoing = true;
this.scanningService.getSchedule() this.scanningService.getSchedule()
@ -78,6 +86,7 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
this.errorHandlerEntity.error(error); this.errorHandlerEntity.error(error);
}); });
} }
getScanners() { getScanners() {
this.onGettingUpdatedTimeStr = true; this.onGettingUpdatedTimeStr = true;
this.scanningService.getScanners() this.scanningService.getScanners()
@ -108,6 +117,7 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
this.onGettingUpdatedTimeStr = false; this.onGettingUpdatedTimeStr = false;
}); });
} }
getScannerMetadata(uid: string) { getScannerMetadata(uid: string) {
this.scanningService.getScannerMetadata(uid) this.scanningService.getScannerMetadata(uid)
.pipe(finalize(() => this.onGettingUpdatedTimeStr = false)) .pipe(finalize(() => this.onGettingUpdatedTimeStr = false))
@ -133,6 +143,7 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
}; };
} }
} }
ngOnInit(): void { ngOnInit(): void {
this.getScanText(); this.getScanText();
this.getScanners(); this.getScanners();
@ -144,10 +155,12 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
this._timeout = null; this._timeout = null;
} }
} }
isOnScanning(): boolean { isOnScanning(): boolean {
return this.scanningMetrics return this.scanningMetrics
&& this.scanningMetrics.ongoing; && this.scanningMetrics.ongoing;
} }
getMetrics() { getMetrics() {
this.gettingMetrics = true; this.gettingMetrics = true;
this.scanningService.getMetrics() this.scanningService.getMetrics()
@ -169,12 +182,14 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
} }
}); });
} }
getI18nKey(str: string): string { getI18nKey(str: string): string {
if (str && this.i18nKeyMap[str]) { if (str && this.i18nKeyMap[str]) {
return this.i18nKeyMap[str]; return this.i18nKeyMap[str];
} }
return str; return str;
} }
errorWidth() { errorWidth() {
if (this.scanningMetrics if (this.scanningMetrics
&& this.scanningMetrics.metrics && this.scanningMetrics.metrics
@ -185,6 +200,7 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
} }
return '0'; return '0';
} }
finishedWidth() { finishedWidth() {
if (this.scanningMetrics if (this.scanningMetrics
&& this.scanningMetrics.metrics && this.scanningMetrics.metrics
@ -195,6 +211,7 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
} }
return '0'; return '0';
} }
runningWidth() { runningWidth() {
if (this.scanningMetrics if (this.scanningMetrics
&& this.scanningMetrics.metrics && this.scanningMetrics.metrics
@ -205,6 +222,7 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
} }
return '0'; return '0';
} }
abortWidth() { abortWidth() {
if (this.scanningMetrics if (this.scanningMetrics
&& this.scanningMetrics.metrics && this.scanningMetrics.metrics
@ -215,6 +233,7 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
} }
return '0'; return '0';
} }
scanNow(): void { scanNow(): void {
if (this.onSubmitting) { if (this.onSubmitting) {
return; // Aoid duplicated submitting return; // Aoid duplicated submitting
@ -288,22 +307,47 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
); );
} }
} }
isError(status: string): boolean { isError(status: string): boolean {
return status === VULNERABILITY_SCAN_STATUS.ERROR; return status === VULNERABILITY_SCAN_STATUS.ERROR;
} }
isFinished(status: string): boolean { isFinished(status: string): boolean {
return status === VULNERABILITY_SCAN_STATUS.SUCCESS; return status === VULNERABILITY_SCAN_STATUS.SUCCESS;
} }
isInProgress(status: string): boolean { isInProgress(status: string): boolean {
return status === VULNERABILITY_SCAN_STATUS.RUNNING; return status === VULNERABILITY_SCAN_STATUS.RUNNING;
} }
isAborted(status: string): boolean { isAborted(status: string): boolean {
return status === VULNERABILITY_SCAN_STATUS.STOPPED; return status === VULNERABILITY_SCAN_STATUS.STOPPED;
} }
isManual() { isManual() {
return this.scanningMetrics && this.scanningMetrics.trigger === Triggers.MANUAL; return this.scanningMetrics && this.scanningMetrics.trigger === Triggers.MANUAL;
} }
isSchedule() { isSchedule() {
return this.scanningMetrics && this.scanningMetrics.trigger === Triggers.SCHEDULE; return this.scanningMetrics && this.scanningMetrics.trigger === Triggers.SCHEDULE;
} }
scanOrStop() {
if (this.isOnScanning()) {
this.stopNow();
} else {
this.scanNow();
}
}
stopNow() {
this.onSubmitting = true;
this.newScanAllService.stopScanAll()
.pipe(finalize(() => this.onSubmitting = false))
.subscribe(res => {
this.errorHandlerEntity.info('CONFIG.SCANNING.STOP_SCAN_ALL_SUCCESS');
}, error => {
this.errorHandlerEntity.error(error);
});
}
} }

View File

@ -92,6 +92,11 @@ export const INITIAL_ACCESSES: FrontAccess[] = [
"resource": "scan", "resource": "scan",
"action": "create", "action": "create",
"checked": true "checked": true
},
{
"resource": "scan",
"action": "stop",
"checked": true
} }
]; ];
@ -108,7 +113,8 @@ export const ACTION_RESOURCE_I18N_MAP = {
'tag': 'REPLICATION.TAG', 'tag': 'REPLICATION.TAG',
'artifact-label': 'SYSTEM_ROBOT.ARTIFACT_LABEL', 'artifact-label': 'SYSTEM_ROBOT.ARTIFACT_LABEL',
'scan': 'SYSTEM_ROBOT.SCAN', 'scan': 'SYSTEM_ROBOT.SCAN',
'scanner-pull': 'SYSTEM_ROBOT.SCANNER_PULL' 'scanner-pull': 'SYSTEM_ROBOT.SCANNER_PULL',
'stop': 'SYSTEM_ROBOT.STOP'
}; };
export enum ExpirationType { export enum ExpirationType {

View File

@ -11,14 +11,16 @@
<clr-dg-action-bar> <clr-dg-action-bar>
<div class="clr-row center"> <div class="clr-row center">
<div class="ml-05"> <div class="ml-05">
<button id="scan-btn" (click)="scanNow()" type="button" class="btn btn-secondary" [clrLoading]="scanBtnState" [disabled]="!(hasEnabledScanner && hasScanningPermission && !onSendingScanCommand)"> <button id="scan-btn" (click)="scanOrStop()" type="button" class="btn btn-secondary" [clrLoading]="scanBtnState" [disabled]="!canScan()">
<clr-icon shape="shield-check" size="16"></clr-icon>&nbsp; <clr-icon shape="shield-check" size="16" *ngIf="!isRunningState()"></clr-icon>&nbsp;
<span>{{'VULNERABILITY.SCAN_NOW' | translate}}</span> <clr-icon shape="stop" size="16" *ngIf="isRunningState()"></clr-icon>&nbsp;
<span *ngIf="!isRunningState()">{{'VULNERABILITY.SCAN_NOW' | translate}}</span>
<span *ngIf="isRunningState()">{{'VULNERABILITY.STOP_NOW' | translate}}</span>
</button> </button>
</div> </div>
<div class="ml-1"> <div class="ml-1">
<div [hidden]="!shouldShowBar()"> <div [hidden]="!shouldShowBar()">
<hbr-vulnerability-bar [summary]="handleScanOverview(artifact?.scan_overview)" [inputScanner]="scanner" <hbr-vulnerability-bar (submitStopFinish)="submitStopFinish($event)" [summary]="handleScanOverview(artifact?.scan_overview)" [inputScanner]="scanner"
(submitFinish)="submitFinish($event)" [projectName]="projectName" [repoName]="repoName" (submitFinish)="submitFinish($event)" [projectName]="projectName" [repoName]="repoName"
[artifactDigest]="digest"> [artifactDigest]="digest">
</hbr-vulnerability-bar> </hbr-vulnerability-bar>

View File

@ -15,7 +15,6 @@ import {
} from "../../../../../../shared/services"; } from "../../../../../../shared/services";
import { AdditionLink } from "../../../../../../../../ng-swagger-gen/models/addition-link"; import { AdditionLink } from "../../../../../../../../ng-swagger-gen/models/addition-link";
import { ErrorHandler } from "../../../../../../shared/units/error-handler"; import { ErrorHandler } from "../../../../../../shared/units/error-handler";
import { ChannelService } from "../../../../../../shared/services/channel.service";
import { SessionService } from "../../../../../../shared/services/session.service"; import { SessionService } from "../../../../../../shared/services/session.service";
import { SessionUser } from "../../../../../../shared/entities/session-user"; import { SessionUser } from "../../../../../../shared/entities/session-user";
import { delay } from "rxjs/operators"; import { delay } from "rxjs/operators";
@ -66,13 +65,6 @@ describe('ArtifactVulnerabilitiesComponent', () => {
return of(true); return of(true);
} }
}; };
const fakedChannelService = {
ArtifactDetail$: {
subscribe() {
return null;
}
}
};
const mockedUser: SessionUser = { const mockedUser: SessionUser = {
user_id: 1, user_id: 1,
username: 'admin', username: 'admin',
@ -120,7 +112,6 @@ describe('ArtifactVulnerabilitiesComponent', () => {
{provide: AdditionsService, useValue: fakedAdditionsService}, {provide: AdditionsService, useValue: fakedAdditionsService},
{provide: UserPermissionService, useValue: fakedUserPermissionService}, {provide: UserPermissionService, useValue: fakedUserPermissionService},
{provide: ScanningResultService, useValue: fakedScanningResultService}, {provide: ScanningResultService, useValue: fakedScanningResultService},
{provide: ChannelService, useValue: fakedChannelService},
{provide: SessionService, useValue: fakedSessionService}, {provide: SessionService, useValue: fakedSessionService},
{provide: ProjectService, useValue: fakedProjectService}, {provide: ProjectService, useValue: fakedProjectService},
], ],
@ -160,4 +151,10 @@ describe('ArtifactVulnerabilitiesComponent', () => {
const cells = firstRow.querySelectorAll("clr-dg-cell"); const cells = firstRow.querySelectorAll("clr-dg-cell");
expect(cells[cells.length - 1].innerText).toEqual("TAG_RETENTION.YES"); expect(cells[cells.length - 1].innerText).toEqual("TAG_RETENTION.YES");
}); });
it("scan button should show the right text", async () => {
fixture.autoDetectChanges(true);
const scanBtn: HTMLButtonElement = fixture.nativeElement.querySelector("#scan-btn");
expect(scanBtn.innerText).toContain("VULNERABILITY.SCAN_NOW");
});
}); });

View File

@ -14,11 +14,11 @@ import {
} from "../../../../../../shared/services"; } from "../../../../../../shared/services";
import { ErrorHandler } from "../../../../../../shared/units/error-handler"; import { ErrorHandler } from "../../../../../../shared/units/error-handler";
import { SEVERITY_LEVEL_MAP, VULNERABILITY_SEVERITY } from "../../../../../../shared/units/utils"; import { SEVERITY_LEVEL_MAP, VULNERABILITY_SEVERITY } from "../../../../../../shared/units/utils";
import { ChannelService } from "../../../../../../shared/services/channel.service";
import { ResultBarChartComponent } from "../../vulnerability-scanning/result-bar-chart.component"; import { ResultBarChartComponent } from "../../vulnerability-scanning/result-bar-chart.component";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { Artifact } from "../../../../../../../../ng-swagger-gen/models/artifact"; import { Artifact } from "../../../../../../../../ng-swagger-gen/models/artifact";
import { SessionService } from "../../../../../../shared/services/session.service"; import { SessionService } from "../../../../../../shared/services/session.service";
import { EventService, HarborEvent } from "../../../../../../services/event-service/event.service";
@Component({ @Component({
selector: 'hbr-artifact-vulnerabilities', selector: 'hbr-artifact-vulnerabilities',
@ -49,6 +49,7 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
cvssSort: ClrDatagridComparatorInterface<VulnerabilityItem>; cvssSort: ClrDatagridComparatorInterface<VulnerabilityItem>;
hasScanningPermission: boolean = false; hasScanningPermission: boolean = false;
onSendingScanCommand: boolean = false; onSendingScanCommand: boolean = false;
onSendingStopCommand: boolean = false;
hasShowLoading: boolean = false; hasShowLoading: boolean = false;
@ViewChild(ResultBarChartComponent) @ViewChild(ResultBarChartComponent)
resultBarChartComponent: ResultBarChartComponent; resultBarChartComponent: ResultBarChartComponent;
@ -60,7 +61,7 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
private additionsService: AdditionsService, private additionsService: AdditionsService,
private userPermissionService: UserPermissionService, private userPermissionService: UserPermissionService,
private scanningService: ScanningResultService, private scanningService: ScanningResultService,
private channel: ChannelService, private eventService: EventService,
private session: SessionService, private session: SessionService,
private projectService: ProjectService, private projectService: ProjectService,
private systemInfoService: SystemInfoService, private systemInfoService: SystemInfoService,
@ -86,8 +87,10 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
this.getScanningPermission(); this.getScanningPermission();
this.getProjectScanner(); this.getProjectScanner();
if (!this.sub) { if (!this.sub) {
this.sub = this.channel.ArtifactDetail$.subscribe(tag => { this.sub = this.eventService.subscribe(HarborEvent.UPDATE_VULNERABILITY_INFO, (artifact: Artifact) => {
if (artifact?.digest === this.artifact?.digest) {
this.getVulnerabilities(); this.getVulnerabilities();
}
}); });
} }
setTimeout(() => { setTimeout(() => {
@ -190,20 +193,27 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
} }
scanNow() { scanNow() {
this.onSendingScanCommand = true; this.onSendingScanCommand = true;
this.channel.publishScanEvent(this.repoName + "/" + this.digest); this.eventService.publish(HarborEvent.START_SCAN_ARTIFACT, this.repoName + "/" + this.digest);
} }
submitFinish(e: boolean) { submitFinish(e: boolean) {
this.onSendingScanCommand = e; this.onSendingScanCommand = e;
} }
submitStopFinish(e: boolean) {
this.onSendingStopCommand = e;
}
shouldShowBar(): boolean { shouldShowBar(): boolean {
return this.hasViewInitWithDelay && this.resultBarChartComponent return this.hasViewInitWithDelay && this.resultBarChartComponent
&& (this.resultBarChartComponent.queued || this.resultBarChartComponent.scanning || this.resultBarChartComponent.error); && (this.resultBarChartComponent.queued
|| this.resultBarChartComponent.scanning
|| this.resultBarChartComponent.error
|| this.resultBarChartComponent.stopped);
} }
hasScanned(): boolean { hasScanned(): boolean {
return this.hasViewInitWithDelay && this.resultBarChartComponent return this.hasViewInitWithDelay && this.resultBarChartComponent
&& !(this.resultBarChartComponent.completed && !(this.resultBarChartComponent.completed
|| this.resultBarChartComponent.error || this.resultBarChartComponent.error
|| this.resultBarChartComponent.queued || this.resultBarChartComponent.queued
|| this.resultBarChartComponent.stopped
|| this.resultBarChartComponent.scanning); || this.resultBarChartComponent.scanning);
} }
handleScanOverview(scanOverview: any): any { handleScanOverview(scanOverview: any): any {
@ -256,4 +266,23 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
} }
return ""; return "";
} }
isRunningState(): boolean {
return this.hasViewInitWithDelay && this.resultBarChartComponent
&& (this.resultBarChartComponent.queued || this.resultBarChartComponent.scanning);
}
scanOrStop() {
if (this.isRunningState()) {
this.stopNow();
} else {
this.scanNow();
}
}
stopNow() {
this.onSendingStopCommand = true;
this.eventService.publish(HarborEvent.STOP_SCAN_ARTIFACT, this.repoName + "/" + this.digest);
}
canScan(): boolean {
return this.hasEnabledScanner && this.hasScanningPermission && !this.onSendingScanCommand;
}
} }

View File

@ -96,6 +96,12 @@
<span>{{'VULNERABILITY.SCAN_NOW' | translate}}</span> <span>{{'VULNERABILITY.SCAN_NOW' | translate}}</span>
</button> </button>
<button id="stop-scan" [clrLoading]="stopBtnState" type="button" class="btn btn-secondary scan-btn"
[disabled]="!(canStopScan() && hasScanImagePermission )" (click)="stopNow()">
<clr-icon shape="stop" size="16"></clr-icon>&nbsp;
<span>{{'VULNERABILITY.STOP_NOW' | translate}}</span>
</button>
<clr-dropdown class="btn btn-link" *ngIf="!depth"> <clr-dropdown class="btn btn-link" *ngIf="!depth">
<span clrDropdownTrigger id="artifact-list-action" class="btn pl-0"> <span clrDropdownTrigger id="artifact-list-action" class="btn pl-0">
{{'BUTTON.ACTIONS' | translate}} {{'BUTTON.ACTIONS' | translate}}
@ -257,7 +263,7 @@
<span *ngIf="!hasVul(artifact)"> <span *ngIf="!hasVul(artifact)">
{{'ARTIFACT.SCAN_UNSUPPORTED' | translate}} {{'ARTIFACT.SCAN_UNSUPPORTED' | translate}}
</span> </span>
<hbr-vulnerability-bar (scanFinished)="scanFinished($event)" *ngIf="hasVul(artifact)" [inputScanner]="handleScanOverview(artifact.scan_overview)?.scanner" <hbr-vulnerability-bar (submitStopFinish)="submitStopFinish($event)" (scanFinished)="scanFinished($event)" *ngIf="hasVul(artifact)" [inputScanner]="handleScanOverview(artifact.scan_overview)?.scanner"
(submitFinish)="submitFinish($event)" [projectName]="projectName" [repoName]="repoName" (submitFinish)="submitFinish($event)" [projectName]="projectName" [repoName]="repoName"
[artifactDigest]="artifact.digest" [summary]="handleScanOverview(artifact.scan_overview)"> [artifactDigest]="artifact.digest" [summary]="handleScanOverview(artifact.scan_overview)">
</hbr-vulnerability-bar> </hbr-vulnerability-bar>

View File

@ -22,7 +22,6 @@ import { ConfirmationDialogComponent } from "../../../../../../../shared/compone
import { ImageNameInputComponent } from "../../../../../../../shared/components/image-name-input/image-name-input.component"; import { ImageNameInputComponent } from "../../../../../../../shared/components/image-name-input/image-name-input.component";
import { CopyInputComponent } from "../../../../../../../shared/components/push-image/copy-input.component"; import { CopyInputComponent } from "../../../../../../../shared/components/push-image/copy-input.component";
import { ErrorHandler } from "../../../../../../../shared/units/error-handler"; import { ErrorHandler } from "../../../../../../../shared/units/error-handler";
import { ChannelService } from "../../../../../../../shared/services/channel.service";
import { OperationService } from "../../../../../../../shared/components/operation/operation.service"; import { OperationService } from "../../../../../../../shared/components/operation/operation.service";
import { ArtifactService as NewArtifactService } from "../../../../../../../../../ng-swagger-gen/services/artifact.service"; import { ArtifactService as NewArtifactService } from "../../../../../../../../../ng-swagger-gen/services/artifact.service";
import { Tag } from "../../../../../../../../../ng-swagger-gen/models/tag"; import { Tag } from "../../../../../../../../../ng-swagger-gen/models/tag";
@ -307,8 +306,6 @@ describe("ArtifactListTabComponent (inline template)", () => {
CopyInputComponent CopyInputComponent
], ],
providers: [ providers: [
ErrorHandler,
ChannelService,
ArtifactDefaultService, ArtifactDefaultService,
{ provide: Router, useValue: mockRouter }, { provide: Router, useValue: mockRouter },
{ provide: ArtifactService, useValue: mockNewArtifactService }, { provide: ArtifactService, useValue: mockNewArtifactService },

View File

@ -36,7 +36,6 @@ import { CopyInputComponent } from "../../../../../../../shared/components/push-
import { ErrorHandler } from "../../../../../../../shared/units/error-handler"; import { ErrorHandler } from "../../../../../../../shared/units/error-handler";
import { ArtifactService } from "../../../artifact.service"; import { ArtifactService } from "../../../artifact.service";
import { OperationService } from "../../../../../../../shared/components/operation/operation.service"; import { OperationService } from "../../../../../../../shared/components/operation/operation.service";
import { ChannelService } from "../../../../../../../shared/services/channel.service";
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../../../../../../../shared/entities/shared.const"; import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../../../../../../../shared/entities/shared.const";
import { operateChanges, OperateInfo, OperationState } from "../../../../../../../shared/components/operation/operate"; import { operateChanges, OperateInfo, OperationState } from "../../../../../../../shared/components/operation/operate";
import { artifactDefault, ArtifactFront as Artifact, ArtifactFront, artifactPullCommands, mutipleFilter } from '../../../artifact'; import { artifactDefault, ArtifactFront as Artifact, ArtifactFront, artifactPullCommands, mutipleFilter } from '../../../artifact';
@ -52,6 +51,7 @@ import { ConfirmationAcknowledgement } from "../../../../../../global-confirmati
import { UN_LOGGED_PARAM } from "../../../../../../../account/sign-in/sign-in.service"; import { UN_LOGGED_PARAM } from "../../../../../../../account/sign-in/sign-in.service";
import { Label } from "../../../../../../../../../ng-swagger-gen/models/label"; import { Label } from "../../../../../../../../../ng-swagger-gen/models/label";
import { LabelService } from "../../../../../../../../../ng-swagger-gen/services/label.service"; import { LabelService } from "../../../../../../../../../ng-swagger-gen/services/label.service";
import { EventService, HarborEvent } from "../../../../../../../services/event-service/event.service";
export interface LabelState { export interface LabelState {
iconsShow: boolean; iconsShow: boolean;
@ -142,6 +142,9 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
hasEnabledScanner: boolean; hasEnabledScanner: boolean;
scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT; scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
onSendingScanCommand: boolean; onSendingScanCommand: boolean;
onSendingStopScanCommand: boolean = false;
onStopScanArtifactsLength: number = 0;
scanStoppedArtifactLength: number = 0;
artifactDigest: string; artifactDigest: string;
depth: string; depth: string;
@ -157,6 +160,8 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
scanFinishedArtifactLength: number = 0; scanFinishedArtifactLength: number = 0;
onScanArtifactsLength: number = 0; onScanArtifactsLength: number = 0;
stopBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
updateArtifactSub: Subscription;
constructor( constructor(
private errorHandlerService: ErrorHandler, private errorHandlerService: ErrorHandler,
private userPermissionService: UserPermissionService, private userPermissionService: UserPermissionService,
@ -165,7 +170,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
private newArtifactService: NewArtifactService, private newArtifactService: NewArtifactService,
private translateService: TranslateService, private translateService: TranslateService,
private operationService: OperationService, private operationService: OperationService,
private channel: ChannelService, private eventService: EventService,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private scanningService: ScanningResultService, private scanningService: ScanningResultService,
private router: Router, private router: Router,
@ -186,6 +191,17 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
} }
this.init(); this.init();
}); });
if (!this.updateArtifactSub) {
this.updateArtifactSub = this.eventService.subscribe(HarborEvent.UPDATE_VULNERABILITY_INFO, (artifact: Artifact) => {
if (this.artifactList && this.artifactList.length) {
this.artifactList.forEach(item => {
if (item.digest === artifact.digest) {
item.scan_overview = artifact.scan_overview;
}
});
}
});
}
} }
ngOnDestroy() { ngOnDestroy() {
if (this.triggerSub) { if (this.triggerSub) {
@ -200,6 +216,10 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
this.stickLabelNameFilterSub.unsubscribe(); this.stickLabelNameFilterSub.unsubscribe();
this.stickLabelNameFilterSub = null; this.stickLabelNameFilterSub = null;
} }
if (this.updateArtifactSub) {
this.updateArtifactSub.unsubscribe();
this.updateArtifactSub = null;
}
} }
init() { init() {
this.hasInit = true; this.hasInit = true;
@ -921,12 +941,21 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
} }
return VULNERABILITY_SCAN_STATUS.NOT_SCANNED; return VULNERABILITY_SCAN_STATUS.NOT_SCANNED;
} }
// Whether show the 'scan now' menu // if has running job, return false
canScanNow(): boolean { canScanNow(): boolean {
if (!this.hasScanImagePermission) { return false; } if (!this.hasScanImagePermission) { return false; }
if (this.onSendingScanCommand) { return false; } if (this.onSendingScanCommand) { return false; }
let st: string = this.scanStatus(this.selectedRow[0]); if (this.selectedRow && this.selectedRow.length) {
return st !== VULNERABILITY_SCAN_STATUS.RUNNING; let flag: boolean = true;
this.selectedRow.forEach(item => {
const st: string = this.scanStatus(item);
if (this.isRunningState(st)) {
flag = false;
}
});
return flag;
}
return false;
} }
getImagePermissionRule(projectId: number): void { getImagePermissionRule(projectId: number): void {
const permissions = [ const permissions = [
@ -958,7 +987,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
this.onSendingScanCommand = true; this.onSendingScanCommand = true;
this.selectedRow.forEach((data: any) => { this.selectedRow.forEach((data: any) => {
let digest = data.digest; let digest = data.digest;
this.channel.publishScanEvent(this.repoName + "/" + digest); this.eventService.publish(HarborEvent.START_SCAN_ARTIFACT, this.repoName + "/" + digest);
}); });
} }
selectedRowHasVul(): boolean { selectedRowHasVul(): boolean {
@ -972,11 +1001,18 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
} }
submitFinish(e: boolean) { submitFinish(e: boolean) {
this.scanFinishedArtifactLength += 1; this.scanFinishedArtifactLength += 1;
// all selected scan action has start // all selected scan action has started
if (this.scanFinishedArtifactLength === this.onScanArtifactsLength) { if (this.scanFinishedArtifactLength === this.onScanArtifactsLength) {
this.onSendingScanCommand = e; this.onSendingScanCommand = e;
} }
} }
submitStopFinish(e: boolean) {
this.scanStoppedArtifactLength += 1;
// all selected scan action has stopped
if (this.scanStoppedArtifactLength === this.onStopScanArtifactsLength) {
this.onSendingScanCommand = e;
}
}
// pull command // pull command
onCpError($event: any): void { onCpError($event: any): void {
this.copyInput.setPullCommendShow(); this.copyInput.setPullCommendShow();
@ -1108,7 +1144,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
if (res.headers) { if (res.headers) {
let xHeader: string = res.headers.get("x-total-count"); let xHeader: string = res.headers.get("x-total-count");
if (xHeader) { if (xHeader) {
item.tagNumber = Number.parseInt(xHeader); item.tagNumber = Number.parseInt(xHeader, 10);
} }
} }
item.tags = res.body; item.tags = res.body;
@ -1117,4 +1153,35 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
}); });
} }
} }
// return true if all selected rows are in "running" state
canStopScan(): boolean {
if (this.onSendingStopScanCommand) { return false; }
if (this.selectedRow && this.selectedRow.length) {
let flag: boolean = true;
this.selectedRow.forEach(item => {
const st: string = this.scanStatus(item);
if (!this.isRunningState(st)) {
flag = false;
}
});
return flag;
}
return false;
}
isRunningState(state: string): boolean {
return state === VULNERABILITY_SCAN_STATUS.RUNNING ||
state === VULNERABILITY_SCAN_STATUS.PENDING ||
state === VULNERABILITY_SCAN_STATUS.SCHEDULED;
}
stopNow() {
if (this.selectedRow && this.selectedRow.length) {
this.scanStoppedArtifactLength = 0;
this.onStopScanArtifactsLength = this.selectedRow.length;
this.onSendingStopScanCommand = true;
this.selectedRow.forEach((data: any) => {
let digest = data.digest;
this.eventService.publish(HarborEvent.STOP_SCAN_ARTIFACT, this.repoName + "/" + digest);
});
}
}
} }

View File

@ -6,7 +6,6 @@ import { delay } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { SystemInfo, SystemInfoDefaultService, SystemInfoService, } from "../../../../../../shared/services"; import { SystemInfo, SystemInfoDefaultService, SystemInfoService, } from "../../../../../../shared/services";
import { ArtifactDefaultService, ArtifactService } from "../../artifact.service"; import { ArtifactDefaultService, ArtifactService } from "../../artifact.service";
import { ChannelService } from "../../../../../../shared/services/channel.service";
import { ErrorHandler } from "../../../../../../shared/units/error-handler"; import { ErrorHandler } from "../../../../../../shared/units/error-handler";
import { RepositoryService as NewRepositoryService } from "../../../../../../../../ng-swagger-gen/services/repository.service"; import { RepositoryService as NewRepositoryService } from "../../../../../../../../ng-swagger-gen/services/repository.service";
import { SharedTestingModule } from "../../../../../../shared/shared.module"; import { SharedTestingModule } from "../../../../../../shared/shared.module";
@ -35,9 +34,6 @@ describe('ArtifactListComponent (inline template)', () => {
}, },
snapshot: { data: null } snapshot: { data: null }
}; };
let mockChannelService = {
scanCommand$: of(1)
};
let mockSystemInfo: SystemInfo = { let mockSystemInfo: SystemInfo = {
'with_notary': true, 'with_notary': true,
'with_admiral': false, 'with_admiral': false,
@ -72,7 +68,6 @@ describe('ArtifactListComponent (inline template)', () => {
], ],
providers: [ providers: [
{ provide: ErrorHandler, useValue: fakedErrorHandler }, { provide: ErrorHandler, useValue: fakedErrorHandler },
{ provide: ChannelService, useValue: mockChannelService },
{ provide: SystemInfoService, useClass: SystemInfoDefaultService }, { provide: SystemInfoService, useClass: SystemInfoDefaultService },
{ provide: ArtifactService, useClass: ArtifactDefaultService }, { provide: ArtifactService, useClass: ArtifactDefaultService },
{ provide: ActivatedRoute, useValue: mockActivatedRoute }, { provide: ActivatedRoute, useValue: mockActivatedRoute },

View File

@ -15,6 +15,9 @@
<div *ngIf="completed" class="bar-state bar-state-chart"> <div *ngIf="completed" class="bar-state bar-state-chart">
<hbr-result-tip-histogram [scanner]="getScanner()" [vulnerabilitySummary]="summary"></hbr-result-tip-histogram> <hbr-result-tip-histogram [scanner]="getScanner()" [vulnerabilitySummary]="summary"></hbr-result-tip-histogram>
</div> </div>
<div *ngIf="stopped" class="bar-state">
<span class="label stopped">{{'VULNERABILITY.STATE.STOPPED' | translate}}</span>
</div>
<div *ngIf="otherStatus" class="bar-state"> <div *ngIf="otherStatus" class="bar-state">
<span class="label not-scan">{{'VULNERABILITY.STATE.OTHER_STATUS' | translate}}</span> <span class="label not-scan">{{'VULNERABILITY.STATE.OTHER_STATUS' | translate}}</span>
</div> </div>

View File

@ -6,7 +6,6 @@ import { HistogramChartComponent } from "./histogram-chart/histogram-chart.compo
import { JobLogDefaultService, JobLogService, ScanningResultDefaultService, ScanningResultService, } from "../../../../../shared/services"; import { JobLogDefaultService, JobLogService, ScanningResultDefaultService, ScanningResultService, } from "../../../../../shared/services";
import { VULNERABILITY_SCAN_STATUS } from "../../../../../shared/units/utils"; import { VULNERABILITY_SCAN_STATUS } from "../../../../../shared/units/utils";
import { ErrorHandler } from "../../../../../shared/units/error-handler"; import { ErrorHandler } from "../../../../../shared/units/error-handler";
import { ChannelService } from "../../../../../shared/services/channel.service";
import { ArtifactDefaultService, ArtifactService } from "../artifact.service"; import { ArtifactDefaultService, ArtifactService } from "../artifact.service";
import { NativeReportSummary } from "../../../../../../../ng-swagger-gen/models/native-report-summary"; import { NativeReportSummary } from "../../../../../../../ng-swagger-gen/models/native-report-summary";
import { SharedTestingModule } from "../../../../../shared/shared.module"; import { SharedTestingModule } from "../../../../../shared/shared.module";
@ -40,7 +39,6 @@ describe('ResultBarChartComponent (inline template)', () => {
HistogramChartComponent], HistogramChartComponent],
providers: [ providers: [
ErrorHandler, ErrorHandler,
ChannelService,
ArtifactDefaultService, ArtifactDefaultService,
{ provide: ArtifactService, useValue: ArtifactDefaultService }, { provide: ArtifactService, useValue: ArtifactDefaultService },
{ provide: ScanningResultService, useValue: ScanningResultDefaultService }, { provide: ScanningResultService, useValue: ScanningResultDefaultService },
@ -61,15 +59,14 @@ describe('ResultBarChartComponent (inline template)', () => {
it('should be created', () => { it('should be created', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should show "scan stopped" if status is STOPPED', () => {
it('should show "not scanned" if status is STOPPED', () => {
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.STOPPED; component.summary.scan_status = VULNERABILITY_SCAN_STATUS.STOPPED;
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { fixture.whenStable().then(() => {
fixture.detectChanges(); fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('span'); let el: HTMLElement = fixture.nativeElement.querySelector('span');
expect(el).toBeTruthy(); expect(el).toBeTruthy();
expect(el.textContent).toEqual('VULNERABILITY.STATE.OTHER_STATUS'); expect(el.textContent).toEqual('VULNERABILITY.STATE.STOPPED');
}); });
}); });

View File

@ -1,13 +1,14 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, } from '@angular/core'; import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, } from '@angular/core';
import { Subscription, timer } from "rxjs"; import { Subscription, timer } from "rxjs";
import { finalize } from "rxjs/operators"; import { finalize } from "rxjs/operators";
import { ScannerVo, ScanningResultService } from "../../../../../shared/services"; import { ScannerVo } from "../../../../../shared/services";
import { ErrorHandler } from "../../../../../shared/units/error-handler"; import { ErrorHandler } from "../../../../../shared/units/error-handler";
import { ChannelService } from "../../../../../shared/services/channel.service";
import { clone, CURRENT_BASE_HREF, dbEncodeURIComponent, VULNERABILITY_SCAN_STATUS } from "../../../../../shared/units/utils"; import { clone, CURRENT_BASE_HREF, dbEncodeURIComponent, VULNERABILITY_SCAN_STATUS } from "../../../../../shared/units/utils";
import { ArtifactService } from "../../../../../../../ng-swagger-gen/services/artifact.service"; import { ArtifactService } from "../../../../../../../ng-swagger-gen/services/artifact.service";
import { Artifact } from "../../../../../../../ng-swagger-gen/models/artifact"; import { Artifact } from "../../../../../../../ng-swagger-gen/models/artifact";
import { NativeReportSummary } from "../../../../../../../ng-swagger-gen/models/native-report-summary"; import { NativeReportSummary } from "../../../../../../../ng-swagger-gen/models/native-report-summary";
import { EventService, HarborEvent } from "../../../../../services/event-service/event.service";
import { ScanService } from "../../../../../../../ng-swagger-gen/services/scan.service";
const STATE_CHECK_INTERVAL: number = 3000; // 3s const STATE_CHECK_INTERVAL: number = 3000; // 3s
@ -25,20 +26,25 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
@Input() artifactDigest: string = ""; @Input() artifactDigest: string = "";
@Input() summary: NativeReportSummary; @Input() summary: NativeReportSummary;
onSubmitting: boolean = false; onSubmitting: boolean = false;
onStopping: boolean = false;
retryCounter: number = 0; retryCounter: number = 0;
stateCheckTimer: Subscription; stateCheckTimer: Subscription;
scanSubscription: Subscription; scanSubscription: Subscription;
stopSubscription: Subscription;
timerHandler: any; timerHandler: any;
@Output() @Output()
submitFinish: EventEmitter<boolean> = new EventEmitter<boolean>(); submitFinish: EventEmitter<boolean> = new EventEmitter<boolean>();
// if sending stop scan request is finished, emit to farther component
@Output()
submitStopFinish: EventEmitter<boolean> = new EventEmitter<boolean>();
@Output() @Output()
scanFinished: EventEmitter<Artifact> = new EventEmitter<Artifact>(); scanFinished: EventEmitter<Artifact> = new EventEmitter<Artifact>();
constructor( constructor(
private artifactService: ArtifactService, private artifactService: ArtifactService,
private scanningService: ScanningResultService, private scanService: ScanService,
private errorHandler: ErrorHandler, private errorHandler: ErrorHandler,
private channel: ChannelService, private eventService: EventService,
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
@ -50,13 +56,23 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
this.getSummary(); this.getSummary();
}); });
} }
this.scanSubscription = this.channel.scanCommand$.subscribe((artifactDigest: string) => { if (!this.scanSubscription) {
this.scanSubscription = this.eventService.subscribe( HarborEvent.START_SCAN_ARTIFACT, (artifactDigest: string) => {
let myFullTag: string = this.repoName + "/" + this.artifactDigest; let myFullTag: string = this.repoName + "/" + this.artifactDigest;
if (myFullTag === artifactDigest) { if (myFullTag === artifactDigest) {
this.scanNow(); this.scanNow();
} }
}); });
} }
if (!this.stopSubscription) {
this.stopSubscription = this.eventService.subscribe( HarborEvent.STOP_SCAN_ARTIFACT, (artifactDigest: string) => {
let myFullTag: string = this.repoName + "/" + this.artifactDigest;
if (myFullTag === artifactDigest) {
this.stopScan();
}
});
}
}
ngOnDestroy(): void { ngOnDestroy(): void {
if (this.stateCheckTimer) { if (this.stateCheckTimer) {
@ -65,6 +81,11 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
} }
if (this.scanSubscription) { if (this.scanSubscription) {
this.scanSubscription.unsubscribe(); this.scanSubscription.unsubscribe();
this.scanSubscription = null;
}
if (!this.stopSubscription) {
this.stopSubscription.unsubscribe();
this.stopSubscription = null;
} }
} }
@ -91,8 +112,11 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
public get scanning(): boolean { public get scanning(): boolean {
return this.status === VULNERABILITY_SCAN_STATUS.RUNNING; return this.status === VULNERABILITY_SCAN_STATUS.RUNNING;
} }
public get stopped(): boolean {
return this.status === VULNERABILITY_SCAN_STATUS.STOPPED;
}
public get otherStatus(): boolean { public get otherStatus(): boolean {
return !(this.completed || this.error || this.queued || this.scanning); return !(this.completed || this.error || this.queued || this.scanning || this.stopped);
} }
scanNow(): void { scanNow(): void {
@ -109,8 +133,11 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
this.onSubmitting = true; this.onSubmitting = true;
this.scanningService.startVulnerabilityScanning(this.projectName, dbEncodeURIComponent(this.repoName), this.artifactDigest) this.scanService.scanArtifact({
.pipe(finalize(() => this.submitFinish.emit(false))) projectName: this.projectName,
reference: this.artifactDigest,
repositoryName: dbEncodeURIComponent(this.repoName)
}).pipe(finalize(() => this.submitFinish.emit(false)))
.subscribe(() => { .subscribe(() => {
this.onSubmitting = false; this.onSubmitting = false;
// Forcely change status to queued after successful submitting // Forcely change status to queued after successful submitting
@ -157,7 +184,7 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
} }
this.scanFinished.emit(artifact); this.scanFinished.emit(artifact);
} }
this.channel.ArtifactDetail$.next(artifact); this.eventService.publish(HarborEvent.UPDATE_VULNERABILITY_INFO, artifact);
}, error => { }, error => {
this.errorHandler.error(error); this.errorHandler.error(error);
this.retryCounter++; this.retryCounter++;
@ -186,4 +213,38 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
} }
return this.inputScanner; return this.inputScanner;
} }
stopScan() {
if (this.onStopping) {
// Avoid duplicated stopping command
console.log("duplicated stopping command");
return;
}
if (!this.repoName || !this.artifactDigest) {
console.log("bad repository or artifact");
return;
}
this.onStopping = true;
this.scanService.stopScanArtifact({
projectName: this.projectName,
reference: this.artifactDigest,
repositoryName: dbEncodeURIComponent(this.repoName)
}).pipe(
finalize(() => {
this.submitStopFinish.emit(false);
this.onStopping = false;
}))
.subscribe(() => {
// Start check status util the job is done
if (!this.stateCheckTimer) {
// Avoid duplicated subscribing
this.stateCheckTimer = timer(STATE_CHECK_INTERVAL, STATE_CHECK_INTERVAL).subscribe(() => {
this.getSummary();
});
}
this.errorHandler.info('VULNERABILITY.TRIGGER_STOP_SUCCESS');
}, error => {
this.errorHandler.error(error);
});
}
} }

View File

@ -169,3 +169,6 @@ hbr-vulnerability-bar {
width: 90%; width: 90%;
} }
} }
.stopped {
border-color: #cccc15;
}

View File

@ -76,5 +76,8 @@ export class EventService {
export enum HarborEvent { export enum HarborEvent {
SCROLL = 'scroll', SCROLL = 'scroll',
SCROLL_TO_POSITION = 'scrollToPosition', SCROLL_TO_POSITION = 'scrollToPosition',
REFRESH_PROJECT_INFO = 'refreshProjectInfo' REFRESH_PROJECT_INFO = 'refreshProjectInfo',
START_SCAN_ARTIFACT = 'startScanArtifact',
STOP_SCAN_ARTIFACT = 'stopScanArtifact',
UPDATE_VULNERABILITY_INFO = 'UpdateVulnerabilityInfo'
} }

View File

@ -1,12 +1,9 @@
import { waitForAsync, ComponentFixture, TestBed } from "@angular/core/testing"; import { waitForAsync, ComponentFixture, TestBed } from "@angular/core/testing";
import { ImageNameInputComponent } from "./image-name-input.component"; import { ImageNameInputComponent } from "./image-name-input.component";
import { ErrorHandler } from "../../units/error-handler/error-handler";
import { ProjectDefaultService, ProjectService } from "../../services"; import { ProjectDefaultService, ProjectService } from "../../services";
import { Project } from "../../../base/project/project-config/project-policy-config/project"; import { Project } from "../../../base/project/project-config/project-policy-config/project";
import { of } from "rxjs"; import { of } from "rxjs";
import { HttpResponse } from "@angular/common/http"; import { HttpResponse } from "@angular/common/http";
import { ChannelService } from "../../services/channel.service";
import { CURRENT_BASE_HREF } from "../../units/utils";
import { SharedTestingModule } from "../../shared.module"; import { SharedTestingModule } from "../../shared.module";
describe("ImageNameInputComponent (inline template)", () => { describe("ImageNameInputComponent (inline template)", () => {
@ -35,8 +32,6 @@ describe("ImageNameInputComponent (inline template)", () => {
ImageNameInputComponent ImageNameInputComponent
], ],
providers: [ providers: [
ErrorHandler,
ChannelService,
{ provide: ProjectService, useClass: ProjectDefaultService } { provide: ProjectService, useClass: ProjectDefaultService }
] ]
}); });

View File

@ -1,31 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Subject } from "rxjs";
import { ArtifactFront as Artifact } from "../../base/project/repository/artifact/artifact";
@Injectable({
providedIn: 'root',
})
export class ChannelService {
// Declare for publishing scan event
scanCommandSource = new Subject<string>();
scanCommand$ = this.scanCommandSource.asObservable();
publishScanEvent(tagId: string): void {
this.scanCommandSource.next(tagId);
}
ArtifactDetail$ = new Subject<Artifact>();
}

View File

@ -965,6 +965,7 @@
"OIDC_ADMIN_GROUP_INFO": "Spezifiziere den Namen einer OIDC Administratorengruppe. Alle Mitglieder dieser Gruppe haben in Harbor administrative Berechtigungen. Falls dies nicht gewünscht ist, kann das Feld leer gelassen werden." "OIDC_ADMIN_GROUP_INFO": "Spezifiziere den Namen einer OIDC Administratorengruppe. Alle Mitglieder dieser Gruppe haben in Harbor administrative Berechtigungen. Falls dies nicht gewünscht ist, kann das Feld leer gelassen werden."
}, },
"SCANNING": { "SCANNING": {
"STOP_SCAN_ALL_SUCCESS": "Trigger stopping scan all successfully!",
"TRIGGER_SCAN_ALL_SUCCESS": "Scan erfolgreich gestartet!", "TRIGGER_SCAN_ALL_SUCCESS": "Scan erfolgreich gestartet!",
"TRIGGER_SCAN_ALL_FAIL": "Fehler beim Start des Scans: {{error}}", "TRIGGER_SCAN_ALL_FAIL": "Fehler beim Start des Scans: {{error}}",
"TITLE": "Schwachstellen-Scanning", "TITLE": "Schwachstellen-Scanning",
@ -1035,7 +1036,8 @@
"OTHER_STATUS": "Nicht gescannt", "OTHER_STATUS": "Nicht gescannt",
"QUEUED": "In Warteschlange", "QUEUED": "In Warteschlange",
"ERROR": "Log anzeigen", "ERROR": "Log anzeigen",
"SCANNING": "Scanning" "SCANNING": "Scanning",
"STOPPED": "Scan stopped"
}, },
"GRID": { "GRID": {
"PLACEHOLDER": "Es wurden keine Scan-Ergebnisse gefunden!", "PLACEHOLDER": "Es wurden keine Scan-Ergebnisse gefunden!",
@ -1078,7 +1080,9 @@
"SCAN_NOW": "Scan", "SCAN_NOW": "Scan",
"SCAN_BY": "SCAN BY {{scanner}}", "SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}", "REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER" "NO_SCANNER": "NO SCANNER",
"TRIGGER_STOP_SUCCESS": "Trigger stopping scan successfully",
"STOP_NOW": "Stop Scan"
}, },
"PUSH_IMAGE": { "PUSH_IMAGE": {
"TITLE": "Push Befehl", "TITLE": "Push Befehl",
@ -1697,6 +1701,7 @@
"FINAL_PROJECT_NAME_TIP": "Der zusammengesetzte Robot-Account-Name besteht aus dem Prefix, dem Projektnamen, einem plus und dem Inhalt des Eingabefeldes.", "FINAL_PROJECT_NAME_TIP": "Der zusammengesetzte Robot-Account-Name besteht aus dem Prefix, dem Projektnamen, einem plus und dem Inhalt des Eingabefeldes.",
"FINAL_SYSTEM_NAME_TIP": "Der zusammengesetzte systemweite Robot-Account-Name besteht aus dem Prefix und dem Inhalt des Eingabefeldes.", "FINAL_SYSTEM_NAME_TIP": "Der zusammengesetzte systemweite Robot-Account-Name besteht aus dem Prefix und dem Inhalt des Eingabefeldes.",
"PUSH_AND_PULL": "Push", "PUSH_AND_PULL": "Push",
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission" "PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission",
"STOP": "Stop"
} }
} }

View File

@ -965,6 +965,7 @@
"OIDC_ADMIN_GROUP_INFO": "Specify an OIDC admin group name. All OIDC users in this group will have harbor admin privilege. Keep it blank if you do not want to." "OIDC_ADMIN_GROUP_INFO": "Specify an OIDC admin group name. All OIDC users in this group will have harbor admin privilege. Keep it blank if you do not want to."
}, },
"SCANNING": { "SCANNING": {
"STOP_SCAN_ALL_SUCCESS": "Trigger stopping scan all successfully!",
"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",
@ -1035,7 +1036,8 @@
"OTHER_STATUS": "Not Scanned", "OTHER_STATUS": "Not Scanned",
"QUEUED": "Queued", "QUEUED": "Queued",
"ERROR": "View Log", "ERROR": "View Log",
"SCANNING": "Scanning" "SCANNING": "Scanning",
"STOPPED": "Scan stopped"
}, },
"GRID": { "GRID": {
"PLACEHOLDER": "We couldn't find any scanning results!", "PLACEHOLDER": "We couldn't find any scanning results!",
@ -1078,7 +1080,9 @@
"SCAN_NOW": "Scan", "SCAN_NOW": "Scan",
"SCAN_BY": "SCAN BY {{scanner}}", "SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}", "REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER" "NO_SCANNER": "NO SCANNER",
"TRIGGER_STOP_SUCCESS": "Trigger stopping scan successfully",
"STOP_NOW": "Stop Scan"
}, },
"PUSH_IMAGE": { "PUSH_IMAGE": {
"TITLE": "Push Command", "TITLE": "Push Command",
@ -1697,6 +1701,7 @@
"FINAL_PROJECT_NAME_TIP": "The final project robot name consists of the prefix,the project name,a plus mark and the current input value", "FINAL_PROJECT_NAME_TIP": "The final project robot name consists of the prefix,the project name,a plus mark and the current input value",
"FINAL_SYSTEM_NAME_TIP": "The final system robot name consists of the prefix and the current input value", "FINAL_SYSTEM_NAME_TIP": "The final system robot name consists of the prefix and the current input value",
"PUSH_AND_PULL": "Push", "PUSH_AND_PULL": "Push",
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission" "PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission",
"STOP": "Stop"
} }
} }

View File

@ -966,6 +966,7 @@
"OIDC_ADMIN_GROUP_INFO": "Specify an OIDC admin group name. All OIDC users in this group will have harbor admin privilege. Keep it blank if you do not want to." "OIDC_ADMIN_GROUP_INFO": "Specify an OIDC admin group name. All OIDC users in this group will have harbor admin privilege. Keep it blank if you do not want to."
}, },
"SCANNING": { "SCANNING": {
"STOP_SCAN_ALL_SUCCESS": "Trigger stopping scan all successfully!",
"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",
@ -1036,7 +1037,8 @@
"OTHER_STATUS": "Not Scanned", "OTHER_STATUS": "Not Scanned",
"QUEUED": "Queued", "QUEUED": "Queued",
"ERROR": "View Log", "ERROR": "View Log",
"SCANNING": "Scanning" "SCANNING": "Scanning",
"STOPPED": "Scan stopped"
}, },
"GRID": { "GRID": {
"PLACEHOLDER": "We couldn't find any scanning results!", "PLACEHOLDER": "We couldn't find any scanning results!",
@ -1079,7 +1081,9 @@
"SCAN_NOW": "Scan", "SCAN_NOW": "Scan",
"SCAN_BY": "SCAN BY {{scanner}}", "SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}", "REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER" "NO_SCANNER": "NO SCANNER",
"TRIGGER_STOP_SUCCESS": "Trigger stopping scan successfully",
"STOP_NOW": "Stop Scan"
}, },
"PUSH_IMAGE": { "PUSH_IMAGE": {
"TITLE": "Push Command", "TITLE": "Push Command",
@ -1696,6 +1700,7 @@
"FINAL_PROJECT_NAME_TIP": "The final project robot name consists of the prefix,the project name,a plus mark and the current input value", "FINAL_PROJECT_NAME_TIP": "The final project robot name consists of the prefix,the project name,a plus mark and the current input value",
"FINAL_SYSTEM_NAME_TIP": "The final system robot name consists of the prefix and the current input value", "FINAL_SYSTEM_NAME_TIP": "The final system robot name consists of the prefix and the current input value",
"PUSH_AND_PULL": "Push", "PUSH_AND_PULL": "Push",
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission" "PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission",
"STOP": "Stop"
} }
} }

View File

@ -938,6 +938,7 @@
"OIDC_ADMIN_GROUP_INFO": "Specify an OIDC admin group name. All OIDC users in this group will have harbor admin privilege. Keep it blank if you do not want to." "OIDC_ADMIN_GROUP_INFO": "Specify an OIDC admin group name. All OIDC users in this group will have harbor admin privilege. Keep it blank if you do not want to."
}, },
"SCANNING": { "SCANNING": {
"STOP_SCAN_ALL_SUCCESS": "Trigger stopping scan all successfully!",
"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é",
@ -1008,7 +1009,8 @@
"OTHER_STATUS": "Non Analysé", "OTHER_STATUS": "Non Analysé",
"QUEUED": "En fil d'attente", "QUEUED": "En fil d'attente",
"ERROR": "Voir le Log", "ERROR": "Voir le Log",
"SCANNING": "En cours d'analyse" "SCANNING": "En cours d'analyse",
"STOPPED": "Scan stopped"
}, },
"GRID": { "GRID": {
"PLACEHOLDER": "Nous n'avons pas trouvé de résultats d'analyse !", "PLACEHOLDER": "Nous n'avons pas trouvé de résultats d'analyse !",
@ -1051,7 +1053,9 @@
"SCAN_NOW": "Analyser", "SCAN_NOW": "Analyser",
"SCAN_BY": "SCAN BY {{scanner}}", "SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}", "REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER" "NO_SCANNER": "NO SCANNER",
"TRIGGER_STOP_SUCCESS": "Trigger stopping scan successfully",
"STOP_NOW": "Stop Scan"
}, },
"PUSH_IMAGE": { "PUSH_IMAGE": {
"TITLE": "Push Command", "TITLE": "Push Command",
@ -1665,6 +1669,7 @@
"FINAL_PROJECT_NAME_TIP": "The final project robot name consists of the prefix,the project name,a plus mark and the current input value", "FINAL_PROJECT_NAME_TIP": "The final project robot name consists of the prefix,the project name,a plus mark and the current input value",
"FINAL_SYSTEM_NAME_TIP": "The final system robot name consists of the prefix and the current input value", "FINAL_SYSTEM_NAME_TIP": "The final system robot name consists of the prefix and the current input value",
"PUSH_AND_PULL": "Push", "PUSH_AND_PULL": "Push",
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission" "PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission",
"STOP": "Stop"
} }
} }

View File

@ -961,6 +961,7 @@
"OIDC_ADMIN_GROUP_INFO": "Informe o nome do grupo OIDC que será considerado administrativo. Todos os usuários deste grupo obterão privilégios de adiministração no Harbor. Deixe vazio para ser ignorado." "OIDC_ADMIN_GROUP_INFO": "Informe o nome do grupo OIDC que será considerado administrativo. Todos os usuários deste grupo obterão privilégios de adiministração no Harbor. Deixe vazio para ser ignorado."
}, },
"SCANNING": { "SCANNING": {
"STOP_SCAN_ALL_SUCCESS": "Trigger stopping scan all successfully!",
"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",
@ -1031,7 +1032,8 @@
"OTHER_STATUS": "Não analisado", "OTHER_STATUS": "Não analisado",
"QUEUED": "Solicitado", "QUEUED": "Solicitado",
"ERROR": "Ver erros...", "ERROR": "Ver erros...",
"SCANNING": "Analisando" "SCANNING": "Analisando",
"STOPPED": "Scan stopped"
}, },
"GRID": { "GRID": {
"PLACEHOLDER": "Não foi possível encontrar nenhum resultado de análise!", "PLACEHOLDER": "Não foi possível encontrar nenhum resultado de análise!",
@ -1074,7 +1076,9 @@
"SCAN_NOW": "Analisar", "SCAN_NOW": "Analisar",
"SCAN_BY": "SCAN BY {{scanner}}", "SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Relatado por {{scanner}}", "REPORTED_BY": "Relatado por {{scanner}}",
"NO_SCANNER": "NO SCANNER" "NO_SCANNER": "NO SCANNER",
"TRIGGER_STOP_SUCCESS": "Trigger stopping scan successfully",
"STOP_NOW": "Stop Scan"
}, },
"PUSH_IMAGE": { "PUSH_IMAGE": {
"TITLE": "Comando Push", "TITLE": "Comando Push",
@ -1693,7 +1697,8 @@
"FINAL_PROJECT_NAME_TIP": "Para evitar conflitos entre projetos, o nome completo será composto pelo nome do projeto concatenado a um marcador e o valor aqui informado.", "FINAL_PROJECT_NAME_TIP": "Para evitar conflitos entre projetos, o nome completo será composto pelo nome do projeto concatenado a um marcador e o valor aqui informado.",
"FINAL_SYSTEM_NAME_TIP": "Este valor será concatenado ao prefixo do projeto.", "FINAL_SYSTEM_NAME_TIP": "Este valor será concatenado ao prefixo do projeto.",
"PUSH_AND_PULL": "Push", "PUSH_AND_PULL": "Push",
"PUSH_PERMISSION_TOOLTIP": "Permissões de envio (push) presume também a permissão e recebimento (pull)." "PUSH_PERMISSION_TOOLTIP": "Permissões de envio (push) presume também a permissão e recebimento (pull).",
"STOP": "Stop"
} }
} }

View File

@ -965,6 +965,7 @@
"OIDC_ADMIN_GROUP_INFO": "Specify an OIDC admin group name. All OIDC users in this group will have harbor admin privilege. Keep it blank if you do not want to." "OIDC_ADMIN_GROUP_INFO": "Specify an OIDC admin group name. All OIDC users in this group will have harbor admin privilege. Keep it blank if you do not want to."
}, },
"SCANNING": { "SCANNING": {
"STOP_SCAN_ALL_SUCCESS": "Trigger stopping scan all successfully!",
"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ı",
@ -1035,7 +1036,8 @@
"OTHER_STATUS": "Taranmadı", "OTHER_STATUS": "Taranmadı",
"QUEUED": "Sıraya alındı", "QUEUED": "Sıraya alındı",
"ERROR": "Günlüğü Görüntüle", "ERROR": "Günlüğü Görüntüle",
"SCANNING": "Taranıyor" "SCANNING": "Taranıyor",
"STOPPED": "Scan stopped"
}, },
"GRID": { "GRID": {
"PLACEHOLDER": "Herhangi bir tarama sonucu bulamadık!", "PLACEHOLDER": "Herhangi bir tarama sonucu bulamadık!",
@ -1078,7 +1080,9 @@
"SCAN_NOW": "Tara", "SCAN_NOW": "Tara",
"SCAN_BY": "SCAN BY {{scanner}}", "SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}", "REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER" "NO_SCANNER": "NO SCANNER",
"TRIGGER_STOP_SUCCESS": "Trigger stopping scan successfully",
"STOP_NOW": "Stop Scan"
}, },
"PUSH_IMAGE": { "PUSH_IMAGE": {
"TITLE": "Push Command", "TITLE": "Push Command",
@ -1697,6 +1701,7 @@
"FINAL_PROJECT_NAME_TIP": "The final project robot name consists of the prefix,the project name,a plus mark and the current input value", "FINAL_PROJECT_NAME_TIP": "The final project robot name consists of the prefix,the project name,a plus mark and the current input value",
"FINAL_SYSTEM_NAME_TIP": "The final system robot name consists of the prefix and the current input value", "FINAL_SYSTEM_NAME_TIP": "The final system robot name consists of the prefix and the current input value",
"PUSH_AND_PULL": "Push", "PUSH_AND_PULL": "Push",
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission" "PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission",
"STOP": "Stop"
} }
} }

View File

@ -966,6 +966,7 @@
"OIDC_ADMIN_GROUP_INFO": "OIDC管理员组名称。所有该组内用户都会有管理员权限此属性可以为空。" "OIDC_ADMIN_GROUP_INFO": "OIDC管理员组名称。所有该组内用户都会有管理员权限此属性可以为空。"
}, },
"SCANNING": { "SCANNING": {
"STOP_SCAN_ALL_SUCCESS": "停止扫描所有镜像任务成功!",
"TRIGGER_SCAN_ALL_SUCCESS": "启动扫描所有镜像任务成功!", "TRIGGER_SCAN_ALL_SUCCESS": "启动扫描所有镜像任务成功!",
"TRIGGER_SCAN_ALL_FAIL": "启动扫描所有镜像任务失败:{{error}}", "TRIGGER_SCAN_ALL_FAIL": "启动扫描所有镜像任务失败:{{error}}",
"TITLE": "缺陷扫描", "TITLE": "缺陷扫描",
@ -1036,7 +1037,8 @@
"OTHER_STATUS": "未扫描", "OTHER_STATUS": "未扫描",
"QUEUED": "已入队列", "QUEUED": "已入队列",
"ERROR": "查看日志", "ERROR": "查看日志",
"SCANNING": "扫描中" "SCANNING": "扫描中",
"STOPPED": "扫描中止"
}, },
"GRID": { "GRID": {
"PLACEHOLDER": "没有扫描结果!", "PLACEHOLDER": "没有扫描结果!",
@ -1079,7 +1081,9 @@
"SCAN_NOW": "扫描", "SCAN_NOW": "扫描",
"SCAN_BY": "使用 {{scanner}} 进行扫描", "SCAN_BY": "使用 {{scanner}} 进行扫描",
"REPORTED_BY": "结果由 {{scanner}} 提供", "REPORTED_BY": "结果由 {{scanner}} 提供",
"NO_SCANNER": "无扫描器" "NO_SCANNER": "无扫描器",
"TRIGGER_STOP_SUCCESS": "停止扫描成功",
"STOP_NOW": "停止扫描"
}, },
"PUSH_IMAGE": { "PUSH_IMAGE": {
"TITLE": "推送命令", "TITLE": "推送命令",
@ -1695,6 +1699,7 @@
"FINAL_PROJECT_NAME_TIP": "项目级机器人的最终名称由前缀,项目名称,一个加号以及当前输入值组成", "FINAL_PROJECT_NAME_TIP": "项目级机器人的最终名称由前缀,项目名称,一个加号以及当前输入值组成",
"FINAL_SYSTEM_NAME_TIP": "系统级机器人的最终名称由前缀和当前输入值组成", "FINAL_SYSTEM_NAME_TIP": "系统级机器人的最终名称由前缀和当前输入值组成",
"PUSH_AND_PULL": "推送", "PUSH_AND_PULL": "推送",
"PUSH_PERMISSION_TOOLTIP": "推送权限无法单独工作,请在选择推送权限的时,确保已经勾选了拉取权限" "PUSH_PERMISSION_TOOLTIP": "推送权限无法单独工作,请在选择推送权限的时,确保已经勾选了拉取权限",
"STOP": "停止"
} }
} }

View File

@ -961,6 +961,7 @@
"OIDC_ADMIN_GROUP_INFO": "Specify an OIDC admin group name. All OIDC users in this group will have harbor admin privilege. Keep it blank if you do not want to." "OIDC_ADMIN_GROUP_INFO": "Specify an OIDC admin group name. All OIDC users in this group will have harbor admin privilege. Keep it blank if you do not want to."
}, },
"SCANNING":{ "SCANNING":{
"STOP_SCAN_ALL_SUCCESS": "Trigger stopping scan all successfully!",
"TRIGGER_SCAN_ALL_SUCCESS": "啟動掃描所有鏡像任務成功!", "TRIGGER_SCAN_ALL_SUCCESS": "啟動掃描所有鏡像任務成功!",
"TRIGGER_SCAN_ALL_FAIL": "啟動掃描所有鏡像任務失敗:{{error}}", "TRIGGER_SCAN_ALL_FAIL": "啟動掃描所有鏡像任務失敗:{{error}}",
"TITLE": "缺陷掃描", "TITLE": "缺陷掃描",
@ -1031,7 +1032,8 @@
"OTHER_STATUS": "未掃描", "OTHER_STATUS": "未掃描",
"QUEUED": "已入隊列", "QUEUED": "已入隊列",
"ERROR": "查看日誌", "ERROR": "查看日誌",
"SCANNING": "掃描中" "SCANNING": "掃描中",
"STOPPED": "Scan stopped"
}, },
"GRID":{ "GRID":{
"PLACEHOLDER": "沒有掃描結果!", "PLACEHOLDER": "沒有掃描結果!",
@ -1074,7 +1076,9 @@
"SCAN_NOW":"掃描", "SCAN_NOW":"掃描",
"SCAN_BY": "SCAN BY {{scanner}}", "SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}", "REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER" "NO_SCANNER": "NO SCANNER",
"TRIGGER_STOP_SUCCESS": "Trigger stopping scan successfully",
"STOP_NOW": "Stop Scan"
}, },
"PUSH_IMAGE":{ "PUSH_IMAGE":{
"TITLE": "推送鏡像的Docker命令", "TITLE": "推送鏡像的Docker命令",
@ -1682,6 +1686,7 @@
"FINAL_PROJECT_NAME_TIP": "The final project robot name consists of the prefix,the project name,a plus mark and the current input value", "FINAL_PROJECT_NAME_TIP": "The final project robot name consists of the prefix,the project name,a plus mark and the current input value",
"FINAL_SYSTEM_NAME_TIP": "The final system robot name consists of the prefix and the current input value", "FINAL_SYSTEM_NAME_TIP": "The final system robot name consists of the prefix and the current input value",
"PUSH_AND_PULL": "Push", "PUSH_AND_PULL": "Push",
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission" "PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission",
"STOP": "Stop"
} }
} }