Provide 'Scan Now' menu in the tag list (#2819)

This commit is contained in:
Steven Zou 2017-07-20 09:28:00 +08:00 committed by Yan
parent 5c8be3502c
commit aa681eb018
14 changed files with 272 additions and 164 deletions

View File

@ -0,0 +1,28 @@
// 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/Subject';
import { Observable } from "rxjs/Observable";
@Injectable()
export class ChannelService {
//Declare for publishing scan event
scanCommandSource = new Subject<string>();
scanCommand$ = this.scanCommandSource.asObservable();
publishScanEvent(tagId: string): void {
this.scanCommandSource.next(tagId);
}
}

View File

@ -0,0 +1 @@
export * from './channel.service';

View File

@ -52,6 +52,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { TranslateServiceInitializer } from './i18n/index';
import { DEFAULT_LANG_COOKIE_KEY, DEFAULT_SUPPORTING_LANGS, DEFAULT_LANG } from './utils';
import { ChannelService } from './channel/index';
/**
* Declare default service configuration; all the endpoints will be defined in
@ -203,7 +204,8 @@ export class HarborLibraryModule {
useFactory: initConfig,
deps: [TranslateServiceInitializer, SERVICE_CONFIG],
multi: true
}
},
ChannelService
]
};
}
@ -221,7 +223,8 @@ export class HarborLibraryModule {
config.repositoryService || { provide: RepositoryService, useClass: RepositoryDefaultService },
config.tagService || { provide: TagService, useClass: TagDefaultService },
config.scanningService || { provide: ScanningResultService, useClass: ScanningResultDefaultService },
config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService }
config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService },
ChannelService
]
};
}

View File

@ -17,3 +17,4 @@ export * from './push-image/index';
export * from './third-party/index';
export * from './config/index';
export * from './job-log-viewer/index';
export * from './channel/index';

View File

@ -27,8 +27,9 @@ export const TAG_TEMPLATE = `
<clr-dg-placeholder>{{'TGA.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-action-overflow>
<button class="action-item" *ngIf="canScanNow(t)" (click)="scanNow(t.name)">{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button class="action-item" *ngIf="hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell style="width: 80px;" [ngSwitch]="withClair">
<a *ngSwitchCase="true" href="javascript:void(0)" (click)="onTagClick(t)">{{t.name}}</a>
@ -36,7 +37,7 @@ export const TAG_TEMPLATE = `
</clr-dg-cell>
<clr-dg-cell style="min-width: 180px;" class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
<clr-dg-cell style="width: 160px;" *ngIf="withClair">
<hbr-vulnerability-bar [tagId]="t.name" [summary]="t.scan_overview" (startScanning)="scanTag($event)"></hbr-vulnerability-bar>
<hbr-vulnerability-bar [repoName]="repoName" [tagId]="t.name" [summary]="t.scan_overview"></hbr-vulnerability-bar>
</clr-dg-cell>
<clr-dg-cell style="width: 80px;" *ngIf="withNotary" [ngSwitch]="t.signature !== null">
<clr-icon shape="check" *ngSwitchCase="true" style="color: #1D5100;"></clr-icon>

View File

@ -15,6 +15,7 @@ import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index';
import { FILTER_DIRECTIVES } from '../filter/index'
import { Observable, Subscription } from 'rxjs/Rx';
import { ChannelService } from '../channel/index';
describe('TagComponent (inline template)', () => {
@ -52,6 +53,7 @@ describe('TagComponent (inline template)', () => {
],
providers: [
ErrorHandler,
ChannelService,
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: TagService, useClass: TagDefaultService },
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }

View File

@ -20,14 +20,17 @@ import {
EventEmitter,
ChangeDetectionStrategy,
ChangeDetectorRef,
ElementRef,
OnDestroy
ElementRef
} from '@angular/core';
import { TagService } from '../service/tag.service';
import { ErrorHandler } from '../error-handler/error-handler';
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from '../shared/shared.const';
import { ChannelService } from '../channel/index';
import {
ConfirmationTargets,
ConfirmationState,
ConfirmationButtons
} from '../shared/shared.const';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
@ -38,25 +41,23 @@ import { Tag, TagClickEvent } from '../service/interface';
import { TAG_TEMPLATE } from './tag.component.html';
import { TAG_STYLE } from './tag.component.css';
import { toPromise, CustomComparator, VULNERABILITY_SCAN_STATUS } from '../utils';
import {
toPromise,
CustomComparator,
VULNERABILITY_SCAN_STATUS
} from '../utils';
import { TranslateService } from '@ngx-translate/core';
import { State, Comparator } from 'clarity-angular';
import { ScanningResultService } from '../service/index';
import { Observable, Subscription } from 'rxjs/Rx';
const STATE_CHECK_INTERVAL: number = 2000;//2s
@Component({
selector: 'hbr-tag',
template: TAG_TEMPLATE,
styles: [TAG_STYLE],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TagComponent implements OnInit, OnDestroy {
export class TagComponent implements OnInit {
@Input() projectId: number;
@Input() repoName: string;
@ -83,11 +84,6 @@ export class TagComponent implements OnInit, OnDestroy {
createdComparator: Comparator<Tag> = new CustomComparator<Tag>('created', 'date');
loading: boolean = false;
stateCheckTimer: Subscription;
tagsInScanning: { [key: string]: any } = {};
scanningTagCount: number = 0;
copyFailed: boolean = false;
@ViewChild('confirmationDialog')
@ -99,8 +95,9 @@ export class TagComponent implements OnInit, OnDestroy {
private errorHandler: ErrorHandler,
private tagService: TagService,
private translateService: TranslateService,
private scanningService: ScanningResultService,
private ref: ChangeDetectorRef) { }
private ref: ChangeDetectorRef,
private channel: ChannelService
) { }
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
@ -135,18 +132,6 @@ export class TagComponent implements OnInit, OnDestroy {
}
this.retrieve();
this.stateCheckTimer = Observable.timer(STATE_CHECK_INTERVAL, STATE_CHECK_INTERVAL).subscribe(() => {
if (this.scanningTagCount > 0) {
this.updateScanningStates();
}
});
}
ngOnDestroy(): void {
if (this.stateCheckTimer) {
this.stateCheckTimer.unsubscribe();
}
}
retrieve() {
@ -215,49 +200,6 @@ export class TagComponent implements OnInit, OnDestroy {
}
}
scanTag(tagId: string): void {
//Double check
if (this.tagsInScanning[tagId]) {
return;
}
toPromise<any>(this.scanningService.startVulnerabilityScanning(this.repoName, tagId))
.then(() => {
//Add to scanning map
this.tagsInScanning[tagId] = tagId;
//Counting
this.scanningTagCount += 1;
})
.catch(error => this.errorHandler.error(error));
}
updateScanningStates(): void {
toPromise<Tag[]>(this.tagService
.getTags(this.repoName))
.then(items => {
console.debug("updateScanningStates called!");
//Reset the scanning states
this.tagsInScanning = {};
this.scanningTagCount = 0;
items.forEach(item => {
if (item.scan_overview) {
if (item.scan_overview.scan_status === VULNERABILITY_SCAN_STATUS.pending ||
item.scan_overview.scan_status === VULNERABILITY_SCAN_STATUS.running) {
this.tagsInScanning[item.name] = item.name;
this.scanningTagCount += 1;
}
}
});
this.tags = items;
})
.catch(error => {
this.errorHandler.error(error);
});
let hnd = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 1000);
}
onSuccess($event: any): void {
this.copyFailed = false;
//Directly close dialog
@ -268,8 +210,33 @@ export class TagComponent implements OnInit, OnDestroy {
//Show error
this.copyFailed = true;
//Select all text
if(this.textInput){
if (this.textInput) {
this.textInput.nativeElement.select();
}
}
//Get vulnerability scanning status
scanStatus(t: Tag): string {
if (t && t.scan_overview && t.scan_overview.scan_status) {
return t.scan_overview.scan_status;
}
return VULNERABILITY_SCAN_STATUS.unknown;
}
//Whether show the 'scan now' menu
canScanNow(t: Tag): boolean {
if (!this.withClair) { return false; }
let st: string = this.scanStatus(t);
return st !== VULNERABILITY_SCAN_STATUS.pending &&
st !== VULNERABILITY_SCAN_STATUS.running;
}
//Trigger scan
scanNow(tagId: string): void {
if (tagId) {
this.channel.publishScanEvent(tagId);
}
}
}

View File

@ -1,17 +1,20 @@
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { DebugElement } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { VulnerabilitySummary } from '../service/index';
import { ResultBarChartComponent, ScanState } from './result-bar-chart.component';
import { ResultBarChartComponent } from './result-bar-chart.component';
import { ResultTipComponent } from './result-tip.component';
import { ScanningResultService, ScanningResultDefaultService } from '../service/scanning.service';
import {
ScanningResultService,
ScanningResultDefaultService,
TagService,
TagDefaultService
} from '../service/index';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { ErrorHandler } from '../error-handler/index';
import { SharedModule } from '../shared/shared.module';
import { VULNERABILITY_SCAN_STATUS } from '../utils';
import { ChannelService } from '../channel/index';
describe('ResultBarChartComponent (inline template)', () => {
let component: ResultBarChartComponent;
@ -52,7 +55,10 @@ describe('ResultBarChartComponent (inline template)', () => {
ResultTipComponent],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: testConfig }
ChannelService,
{ provide: SERVICE_CONFIG, useValue: testConfig },
{ provide: TagService, useValue: TagDefaultService },
{ provide: ScanningResultService, useValue: ScanningResultDefaultService }
]
});
@ -62,7 +68,7 @@ describe('ResultBarChartComponent (inline template)', () => {
fixture = TestBed.createComponent(ResultBarChartComponent);
component = fixture.componentInstance;
component.tagId = "mockTag";
component.state = ScanState.UNKNOWN;
component.summary = mockData;
serviceConfig = TestBed.get(SERVICE_CONFIG);
@ -71,30 +77,28 @@ describe('ResultBarChartComponent (inline template)', () => {
it('should be created', () => {
expect(component).toBeTruthy();
});
it('should inject the SERVICE_CONFIG', () => {
expect(serviceConfig).toBeTruthy();
expect(serviceConfig.vulnerabilityScanningBaseEndpoint).toEqual("/api/vulnerability/testing");
});
it('should show a button if status is PENDING', async(() => {
component.state = ScanState.PENDING;
it('should show "not scanned" if status is STOPPED', async(() => {
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.stopped;
fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getRecentLogs
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('.scanning-button');
let el: HTMLElement = fixture.nativeElement.querySelector('span');
expect(el).toBeTruthy();
expect(el.textContent).toEqual('VULNERABILITY.STATE.STOPPED');
});
}));
it('should show progress if status is SCANNING', async(() => {
component.state = ScanState.SCANNING;
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.running;
fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getRecentLogs
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('.progress');
@ -103,10 +107,10 @@ describe('ResultBarChartComponent (inline template)', () => {
}));
it('should show QUEUED if status is QUEUED', async(() => {
component.state = ScanState.QUEUED;
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.pending;
fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getRecentLogs
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-state');
@ -119,11 +123,10 @@ describe('ResultBarChartComponent (inline template)', () => {
}));
it('should show summary bar chart if status is COMPLETED', async(() => {
component.state = ScanState.COMPLETED;
component.summary = mockData;
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.finished;
fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getRecentLogs
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none');

View File

@ -1,35 +1,40 @@
import {
Component,
Input,
Output,
EventEmitter,
OnInit
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef
} from '@angular/core';
import { VulnerabilitySummary } from '../service/index';
import { SCANNING_STYLES } from './scanning.css';
import { BAR_CHART_COMPONENT_HTML } from './scanning.html';
import { VULNERABILITY_SCAN_STATUS } from '../utils';
import { VulnerabilitySeverity } from '../service/index';
import {
VulnerabilitySummary,
VulnerabilitySeverity,
TagService,
ScanningResultService,
Tag
} from '../service/index';
import { ErrorHandler } from '../error-handler/index';
import { toPromise } from '../utils';
import { Observable, Subscription } from 'rxjs/Rx';
import { ChannelService } from '../channel/index';
export enum ScanState {
COMPLETED, //Scanning work successfully completed
ERROR, //Error occurred when scanning
QUEUED, //Scanning job is queued
SCANNING, //Scanning in progress
PENDING, //Scanning not start
UNKNOWN //Unknown status
}
const STATE_CHECK_INTERVAL: number = 2000;//2s
const RETRY_TIMES: number = 3;
@Component({
selector: 'hbr-vulnerability-bar',
styles: [SCANNING_STYLES],
template: BAR_CHART_COMPONENT_HTML
})
export class ResultBarChartComponent implements OnInit {
export class ResultBarChartComponent implements OnInit, OnDestroy {
@Input() repoName: string = "";
@Input() tagId: string = "";
@Input() state: ScanState = ScanState.PENDING;
@Input() summary: VulnerabilitySummary = {
scan_status: VULNERABILITY_SCAN_STATUS.unknown,
scan_status: VULNERABILITY_SCAN_STATUS.stopped,
severity: VulnerabilitySeverity.UNKNOWN,
update_time: new Date(),
components: {
@ -37,63 +42,157 @@ export class ResultBarChartComponent implements OnInit {
summary: []
}
};
@Output() startScanning: EventEmitter<string> = new EventEmitter<string>();
scanningInProgress: boolean = false;
onSubmitting: boolean = false;
retryCounter: number = 0;
stateCheckTimer: Subscription;
timerHandler: any;
constructor() { }
constructor(
private tagService: TagService,
private scanningService: ScanningResultService,
private errorHandler: ErrorHandler,
private channel: ChannelService,
private ref: ChangeDetectorRef
) { }
ngOnInit(): void {
this.channel.scanCommand$.subscribe((tagId: string) => {
if (this.tagId === tagId) {
this.scanNow();
}
});
}
ngOnDestroy(): void {
if (this.stateCheckTimer) {
this.stateCheckTimer.unsubscribe();
this.stateCheckTimer = null;
}
}
//Get vulnerability scanning status
public get status(): string {
if (this.summary && this.summary.scan_status) {
switch (this.summary.scan_status) {
case VULNERABILITY_SCAN_STATUS.unknown:
this.state = ScanState.UNKNOWN;
break;
case VULNERABILITY_SCAN_STATUS.error:
this.state = ScanState.ERROR;
break;
case VULNERABILITY_SCAN_STATUS.pending:
this.state = ScanState.QUEUED;
break;
case VULNERABILITY_SCAN_STATUS.stopped:
this.state = ScanState.PENDING;
break;
case VULNERABILITY_SCAN_STATUS.finished:
this.state = ScanState.COMPLETED;
break;
default:
break;
}
return this.summary.scan_status;
}
return VULNERABILITY_SCAN_STATUS.unknown;
}
public get completed(): boolean {
return this.state === ScanState.COMPLETED;
return this.status === VULNERABILITY_SCAN_STATUS.finished;
}
public get error(): boolean {
return this.state === ScanState.ERROR;
return this.status === VULNERABILITY_SCAN_STATUS.error;
}
public get queued(): boolean {
return this.state === ScanState.QUEUED;
return this.status === VULNERABILITY_SCAN_STATUS.pending;
}
public get scanning(): boolean {
return this.state === ScanState.SCANNING;
return this.status === VULNERABILITY_SCAN_STATUS.running;
}
public get pending(): boolean {
return this.state === ScanState.PENDING;
public get stopped(): boolean {
return this.status === VULNERABILITY_SCAN_STATUS.stopped;
}
public get unknown(): boolean {
return this.state === ScanState.UNKNOWN;
return this.status === VULNERABILITY_SCAN_STATUS.unknown;
}
scanNow(): void {
if (this.tagId && this.tagId !== '') {
this.scanningInProgress = true;
this.startScanning.emit(this.tagId);
if (this.onSubmitting) {
//Avoid duplicated submitting
return;
}
if (!this.repoName || !this.tagId) {
return;
}
this.onSubmitting = true;
toPromise<any>(this.scanningService.startVulnerabilityScanning(this.repoName, this.tagId))
.then(() => {
this.onSubmitting = false;
//Forcely change status to queued after successful submitting
this.summary.scan_status = VULNERABILITY_SCAN_STATUS.pending;
//Forcely refresh view
this.forceRefreshView(1000);
//Start check status util the job is done
if (!this.stateCheckTimer) {
//Avoid duplicated subscribing
this.stateCheckTimer = Observable.timer(STATE_CHECK_INTERVAL, STATE_CHECK_INTERVAL).subscribe(() => {
this.getSummary();
});
}
})
.catch(error => {
this.onSubmitting = false;
this.errorHandler.error(error);
});
}
getSummary(): void {
if (!this.repoName || !this.tagId) {
return
}
toPromise<Tag>(this.tagService.getTag(this.repoName, this.tagId))
.then((t: Tag) => {
//To keep the same summary reference, use value copy.
this.copyValue(t.scan_overview);
//Forcely refresh view
this.forceRefreshView(1000);
if (!this.queued && !this.scanning) {
//Scanning should be done
if (this.stateCheckTimer) {
this.stateCheckTimer.unsubscribe();
this.stateCheckTimer = null;
}
}
})
.catch(error => {
this.errorHandler.error(error);
this.retryCounter++;
if (this.retryCounter >= RETRY_TIMES) {
//Stop timer
if (this.stateCheckTimer) {
this.stateCheckTimer.unsubscribe();
this.stateCheckTimer = null;
}
this.retryCounter = 0;
}
});
}
copyValue(newVal: VulnerabilitySummary): void {
if (!newVal || !newVal.scan_status) { return; }
this.summary.scan_status = newVal.scan_status;
this.summary.severity = newVal.severity;
this.summary.components = newVal.components;
this.summary.update_time = newVal.update_time;
}
forceRefreshView(duration: number): void {
//Reset timer
if (this.timerHandler) {
clearInterval(this.timerHandler);
}
this.timerHandler = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => {
if (this.timerHandler) {
clearInterval(this.timerHandler);
this.timerHandler = null;
}
}, duration);
}
}

View File

@ -34,7 +34,7 @@ export const TIP_COMPONENT_HTML: string = `
</div>
<div>
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
<span>{{completeTimestamp | date}}</span>
<span>{{completeTimestamp | date:'MM/dd/y HH:mm:ss'}}</span>
</div>
</clr-tooltip-content>
</clr-tooltip>
@ -89,11 +89,11 @@ export const GRID_COMPONENT_HTML: string = `
export const BAR_CHART_COMPONENT_HTML: string = `
<div class="bar-wrapper">
<div *ngIf="pending" class="bar-state">
<button class="btn btn-link scanning-button" (click)="scanNow()" [disabled]="scanningInProgress">{{'VULNERABILITY.STATE.PENDING' | translate}}</button>
<div *ngIf="stopped" class="bar-state">
<span class="label">{{'VULNERABILITY.STATE.STOPPED' | translate}}</span>
</div>
<div *ngIf="queued" class="bar-state">
<span>{{'VULNERABILITY.STATE.QUEUED' | translate}}</span>
<span class="label label-orange">{{'VULNERABILITY.STATE.QUEUED' | translate}}</span>
</div>
<div *ngIf="error" class="bar-state bar-state-error">
<clr-icon shape="info-circle" class="is-error" size="24"></clr-icon>
@ -101,9 +101,9 @@ export const BAR_CHART_COMPONENT_HTML: string = `
</div>
<div *ngIf="scanning" class="bar-state bar-state-chart">
<div>{{'VULNERABILITY.STATE.SCANNING' | translate}}</div>
<div class="progress loop" style="height:2px;min-height:2px;"><progress></progress></div>
<div class="progress loop" style="height:2px;"><progress></progress></div>
</div>
<div *ngIf="completed" class="bar-state bar-state-chart" style="z-index:1020;">
<div *ngIf="completed" class="bar-state bar-state-chart">
<hbr-vulnerability-summary-chart [summary]="summary"></hbr-vulnerability-summary-chart>
</div>
<div *ngIf="unknown" class="bar-state">

View File

@ -31,7 +31,7 @@
"clarity-icons": "^0.9.8",
"clarity-ui": "^0.9.8",
"core-js": "^2.4.1",
"harbor-ui": "0.3.2",
"harbor-ui": "0.3.18",
"intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0",

View File

@ -462,7 +462,7 @@
},
"VULNERABILITY": {
"STATE": {
"PENDING": "SCAN NOW",
"STOPPED": "Not Scanned",
"QUEUED": "Queued",
"ERROR": "Error",
"SCANNING": "Scanning",
@ -495,7 +495,8 @@
"PLURAL": "Vulnerabilities",
"PLACEHOLDER": "Filter Vulnerabilities",
"PACKAGE": "Package with",
"PACKAGES": "Packages with"
"PACKAGES": "Packages with",
"SCAN_NOW": "Scan"
},
"PUSH_IMAGE": {
"TITLE": "Push Image",

View File

@ -461,7 +461,7 @@
},
"VULNERABILITY": {
"STATE": {
"PENDING": "SCAN NOW",
"STOPPED": "Not Scanned",
"QUEUED": "Queued",
"ERROR": "Error",
"SCANNING": "Scanning",
@ -494,7 +494,8 @@
"PLURAL": "Vulnerabilities",
"PLACEHOLDER": "Filter Vulnerabilities",
"PACKAGE": "Package with",
"PACKAGES": "Packages with"
"PACKAGES": "Packages with",
"SCAN_NOW": "Scan"
},
"PUSH_IMAGE": {
"TITLE": "Push Image",

View File

@ -466,7 +466,7 @@
},
"VULNERABILITY": {
"STATE": {
"PENDING": "开始扫描",
"STOPPED": "未扫描",
"QUEUED": "已入队列",
"ERROR": "错误",
"SCANNING": "扫描中",
@ -499,7 +499,8 @@
"PLURAL": "缺陷",
"PLACEHOLDER": "过滤缺陷",
"PACKAGE": "个组件有",
"PACKAGES": "个组件有"
"PACKAGES": "个组件有",
"SCAN_NOW": "扫描"
},
"PUSH_IMAGE": {
"TITLE": "推送镜像",