Improve scanner UI

Signed-off-by: sshijun <sshijun@vmware.com>
This commit is contained in:
sshijun 2019-10-28 11:33:26 +08:00
parent e8554b9d66
commit 0c41a01f83
52 changed files with 400 additions and 374 deletions

View File

@ -153,8 +153,6 @@ watchRepoClickEvent(repo: RepositoryItem): void {
**hasProjectAdminRole** is a user session related property to determined whether the current user has project administrator role. Some action menus might be disabled based on this property.
**withClair** is Clair installed
**withNotary** is Notary installed
**tagClickEvent** is an @output event emitter for you to catch the tag click events.
@ -162,7 +160,7 @@ watchRepoClickEvent(repo: RepositoryItem): void {
**goBackClickEvent** is an @output event emitter for you to catch the go back events.
```
<hbr-repository [projectId]="" [repoName]="" [hasSignedIn]="" [hasProjectAdminRole]="" [withClair]="" [withNotary]=""
<hbr-repository [projectId]="" [repoName]="" [hasSignedIn]="" [hasProjectAdminRole]="" [withNotary]=""
(tagClickEvent)="watchTagClickEvt($event)" (backEvt)="watchGoBackEvt($event)" ></hbr-repository>
watchTagClickEvt(tagEvt: TagClickEvent): void {
@ -213,7 +211,7 @@ watchAddInfoEvent(repo: RepositoryItem): void {
**goBackClickEvent** is an @output event emitter for you to catch the go back events.
```
<hbr-repository [projectId]="" [repoName]="" [hasSignedIn]="" [hasProjectAdminRole]="" [withClair]="" [withNotary]=""
<hbr-repository [projectId]="" [repoName]="" [hasSignedIn]="" [hasProjectAdminRole]="" [withNotary]=""
(tagClickEvent)="watchTagClickEvt($event)" (backEvt)="watchGoBackEvt($event)" ></hbr-repository>
watchTagClickEvt(tagEvt: TagClickEvent): void {

View File

@ -48,11 +48,6 @@ export class RegistryConfigComponent implements OnInit {
get hasCAFile(): boolean {
return this.systemInfo && this.systemInfo.has_ca_root;
}
get withClair(): boolean {
return this.systemInfo && this.systemInfo.with_clair;
}
get withAdmiral(): boolean {
return this.systemInfo && this.systemInfo.with_admiral;
}

View File

@ -266,11 +266,6 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
.subscribe(systemInfo => this.systemInfo = systemInfo
, error => this.errorHandler.error(error));
}
get withClair(): boolean {
return this.systemInfo ? this.systemInfo.with_clair : false;
}
getSystemWhitelist() {
this.onGoing = true;
this.systemInfoService.getSystemWhitelist()

View File

@ -1,28 +1,5 @@
<form #systemConfigFrom="ngForm" class="compact">
<section class="form-block">
<label class="section-title" *ngIf="showSubTitle">{{ 'CONFIG.SCANNING.TITLE' | translate }}</label>
<div>
<label class="update-time">{{ 'CONFIG.SCANNING.DB_REFRESH_TIME' | translate }}</label>
<clr-tooltip *ngIf="!isClairDBFullyReady">
<clr-icon shape="warning" class="is-warning" size="22"></clr-icon>
<clr-tooltip-content [clrPosition]="'top-right'" [clrSize]="'md'" *clrIfOpen>
<span>{{'CONFIG.SCANNING.DB_NOT_READY' | translate }}</span>
</clr-tooltip-content>
</clr-tooltip>
<clr-dropdown *ngIf="isClairDBFullyReady && showScanningNamespaces" class="clr-dropdown-override">
<button class="btn btn-link btn-font" clrDropdownToggle>
{{ updatedTimestamp | date:'short' }}
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu [clrPosition]="'bottom-right'" class="dropdown-namespace">
<div *ngFor="let nt of namespaceTimestamps" class="namespace">
<span class="label label-info">{{nt.namespace}}</span>
<span>{{ convertToLocalTime(nt.last_update) | date:'short'}} </span>
</div>
</clr-dropdown-menu>
</clr-dropdown>
<span *ngIf="isClairDBFullyReady && !showScanningNamespaces">{{ updatedTimestamp | date:'short' }} </span>
</div>
<div class="button-group">
<cron-selection #CronScheduleComponent [labelCurrent]="getLabelCurrent" [labelEdit]='getLabelCurrent' [originCron]='originCron' (inputvalue)="scanAll($event)"></cron-selection>
<div class="btn-scan-right btn-scan">
@ -30,4 +7,4 @@
</div>
</div>
</section>
</form>
</form>

View File

@ -62,28 +62,6 @@ export class VulnerabilityConfigComponent implements OnInit {
this.getLabelCurrent = res;
});
}
get updatedTimestamp(): Date {
if (this.systemInfo &&
this.systemInfo.clair_vulnerability_status &&
this.systemInfo.clair_vulnerability_status.overall_last_update > 0) {
return this.convertToLocalTime(this.systemInfo.clair_vulnerability_status.overall_last_update);
}
return null;
}
get namespaceTimestamps(): ClairDetail[] {
if (this.systemInfo &&
this.systemInfo.clair_vulnerability_status &&
this.systemInfo.clair_vulnerability_status.details &&
this.systemInfo.clair_vulnerability_status.details.length > 0) {
return this.systemInfo.clair_vulnerability_status.details;
}
return [];
}
getSchedule() {
this.onGoing = true;
this.scanningService.getSchedule()
@ -115,12 +93,6 @@ export class VulnerabilityConfigComponent implements OnInit {
return this.systemSettingsForm && this.systemSettingsForm.valid;
}
get isClairDBFullyReady(): boolean {
return this.systemInfo &&
this.systemInfo.clair_vulnerability_status &&
this.systemInfo.clair_vulnerability_status.overall_last_update > 0;
}
ngOnInit(): void {
this.getSystemInfo();
this.getScanText();

View File

@ -56,7 +56,7 @@
<clr-control-helper class="config-subtext"> {{ 'PROJECT_CONFIG.AUTOSCAN_POLICY' | translate }}
</clr-control-helper>
</clr-checkbox-container>
<div class="clr-form-control" [class.clr-form-control-disabled]="!hasChangeConfigRole">
<div *ngIf="systemInfo" class="clr-form-control" [class.clr-form-control-disabled]="!hasChangeConfigRole">
<label for="systemWhitelist" class="clr-control-label">{{'CVE_WHITELIST.CVE_WHITELIST'|translate}}</label>
<div class="w-100 clr-control-container">
<div class="config-subtext">
@ -134,10 +134,10 @@
</li>
</ul>
</div>
<div class="clr-col padding-top-8 ">
<div class="clr-col padding-top-16 pl-2">
<div class="clr-row expire-data">
<label for="expires"
class="bottom-line clr-col-4">{{'CVE_WHITELIST.EXPIRES_AT'|translate}}</label>
class="bottom-line clr-col-3">{{'CVE_WHITELIST.EXPIRES_AT'|translate}}</label>
<div class="underline">
<input #dateSystemInput readonly type="date" [(clrDate)]="systemExpiresDate">
<input [disabled]="!hasChangeConfigRole" *ngIf="!isUseSystemWhitelist()" #dateInput
@ -149,7 +149,7 @@
</div>
</div>
<div class="clr-row">
<label for="expires" class="clr-col-4"></label>
<label for="expires" class="clr-col-3"></label>
<clr-checkbox-wrapper>
<input [disabled]="isUseSystemWhitelist() || !hasChangeConfigRole"
[checked]="neverExpires" [(ngModel)]="neverExpires" type="checkbox" clrCheckbox

View File

@ -42,8 +42,8 @@
color: #0079bb;
}
.padding-top-8 {
padding-top: 8px;
.padding-top-16 {
padding-top: 16px;
}
.position-relative {
position: relative;

View File

@ -106,11 +106,9 @@ export class ProjectPolicyConfigComponent implements OnInit {
this.systemInfoService.getSystemInfo()
.subscribe(systemInfo => {
this.systemInfo = systemInfo;
if (this.withClair) {
setTimeout(() => {
this.dateSystemInput.nativeElement.parentNode.setAttribute("hidden", "hidden");
}, 100);
}
setTimeout(() => {
this.dateSystemInput.nativeElement.parentNode.setAttribute("hidden", "hidden");
}, 100);
} , error => this.errorHandler.error(error));
// retrive project level policy data
this.retrieve();
@ -146,11 +144,6 @@ export class ProjectPolicyConfigComponent implements OnInit {
public get withNotary(): boolean {
return this.systemInfo ? this.systemInfo.with_notary : false;
}
public get withClair(): boolean {
return this.systemInfo ? this.systemInfo.with_clair : false;
}
retrieve(state?: State): any {
this.projectService.getProject(this.projectId)
.subscribe(

View File

@ -39,10 +39,6 @@
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="showDBStatusWarning" class="db-status-warning">
<clr-icon shape="warning" class="is-warning" size="24"></clr-icon>
{{'CONFIG.SCANNING.DB_NOT_READY' | translate }}
</span>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}</span>
{{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}
<clr-dg-pagination #pagination [(clrDgPage)]="currentPage" [clrDgPageSize]="pageSize" [clrDgTotalItems]="totalCount"></clr-dg-pagination>
@ -105,4 +101,4 @@
</ng-template>
</hbr-gridview>
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
</div>
</div>

View File

@ -104,11 +104,6 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit {
public get registryUrl(): string {
return this.systemInfo ? this.systemInfo.registry_url : "";
}
public get withClair(): boolean {
return this.systemInfo ? this.systemInfo.with_clair : false;
}
public get isClairDBReady(): boolean {
return (
this.systemInfo &&
@ -120,11 +115,6 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit {
public get withAdmiral(): boolean {
return this.mode === "admiral";
}
public get showDBStatusWarning(): boolean {
return this.withClair && !this.isClairDBReady;
}
get canDownloadCert(): boolean {
return this.systemInfo && this.systemInfo.has_ca_root;
}

View File

@ -74,11 +74,6 @@ export class RepositoryComponent implements OnInit {
public get withNotary(): boolean {
return this.systemInfo ? this.systemInfo.with_notary : false;
}
public get withClair(): boolean {
return this.systemInfo ? this.systemInfo.with_clair : false;
}
public get withAdmiral(): boolean {
return this.systemInfo ? this.systemInfo.with_admiral : false;
}

View File

@ -157,5 +157,12 @@ export const USERSTATICPERMISSION = {
"READ": "read",
}
},
"SCANNER": {
"KEY": "scanner",
"VALUE": {
"READ": "read",
"CREATE": "create"
}
}
};

View File

@ -34,8 +34,6 @@ export class TagDetailComponent implements OnInit {
repositoryId: string;
@Input()
withAdmiral: boolean;
@Input()
withClair: boolean;
tagDetails: Tag = {
name: "--",
size: "--",
@ -156,7 +154,8 @@ export class TagDetailComponent implements OnInit {
}
get hasCve(): boolean {
return this.vulnerabilitySummary
&& this.vulnerabilitySummary.scan_status === VULNERABILITY_SCAN_STATUS.SUCCESS;
&& this.vulnerabilitySummary.scan_status === VULNERABILITY_SCAN_STATUS.SUCCESS
&& this.vulnerabilitySummary.severity !== VULNERABILITY_SEVERITY.NONE;
}
public get scanCompletedDatetime(): Date {
return this.tagDetails && this.tagDetails.scan_overview

View File

@ -92,44 +92,70 @@
<clr-dg-placeholder>{{'TAG.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-cell class="truncated flex-max-width">
<a href="javascript:void(0)" (click)="onTagClick(t)" title="{{t.name}}">{{t.name}}</a>
<span *ngIf="t.immutable" class="label label-info ml-1">{{'REPOSITORY.IMMUTABLE' | translate}}</span>
</clr-dg-cell>
<clr-dg-cell>{{sizeTransform(t.size)}}</clr-dg-cell>
<clr-dg-cell class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">
<hbr-copy-input #copyInput (onCopyError)="onCpError($event)" iconMode="true" defaultValue="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}"></hbr-copy-input>
<div class="cell">
<a href="javascript:void(0)" (click)="onTagClick(t)" title="{{t.name}}">{{t.name}}</a>
<span *ngIf="t.immutable" class="label label-info ml-1">{{'REPOSITORY.IMMUTABLE' | translate}}</span>
</div>
</clr-dg-cell>
<clr-dg-cell>
<hbr-vulnerability-bar [repoName]="repoName" [tagId]="t.name" [summary]="handleScanOverview(t.scan_overview)"></hbr-vulnerability-bar>
<div class="cell">
{{sizeTransform(t.size)}}
</div>
</clr-dg-cell>
<clr-dg-cell class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">
<div class="cell">
<hbr-copy-input class="margin-top-m4" #copyInput (onCopyError)="onCpError($event)" iconMode="true" defaultValue="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}"></hbr-copy-input>
</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">
<hbr-vulnerability-bar [repoName]="repoName" [tagId]="t.name" [summary]="handleScanOverview(t.scan_overview)"></hbr-vulnerability-bar>
</div>
</clr-dg-cell>
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signature !== null">
<clr-icon shape="check-circle" *ngSwitchCase="true" size="20" class="color-green"></clr-icon>
<clr-icon shape="times-circle" *ngSwitchCase="false" size="16" class="color-red"></clr-icon>
<a href="javascript:void(0)" *ngSwitchDefault role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="help" class="color-gray" size="16"></clr-icon>
<span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span>
</a>
<div class="cell">
<clr-icon shape="check-circle" *ngSwitchCase="true" size="20" class="color-green"></clr-icon>
<clr-icon shape="times-circle" *ngSwitchCase="false" size="16" class="color-red"></clr-icon>
<a href="javascript:void(0)" *ngSwitchDefault role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="help" class="color-gray" size="16"></clr-icon>
<span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span>
</a>
</div>
</clr-dg-cell>
<clr-dg-cell class="truncated" title="{{t.author}}">
<div class="cell">
{{t.author}}
</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">{{t.created | date: 'short'}}</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">{{t.docker_version}}</div>
</clr-dg-cell>
<clr-dg-cell class="truncated" title="{{t.author}}">{{t.author}}</clr-dg-cell>
<clr-dg-cell>{{t.created | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{t.docker_version}}</clr-dg-cell>
<clr-dg-cell *ngIf="!withAdmiral">
<hbr-label-piece *ngIf="t.labels?.length" [label]="t.labels[0]" [labelWidth]="90"> </hbr-label-piece>
<div class="signpost-item" [hidden]="t.labels?.length<=1">
<div class="trigger-item">
<clr-signpost>
<button class="btn btn-link" clrSignpostTrigger>...</button>
<clr-signpost-content [clrPosition]="'left-top'" *clrIfOpen>
<div>
<hbr-label-piece *ngFor="let label of t.labels" [label]="label"></hbr-label-piece>
</div>
</clr-signpost-content>
</clr-signpost>
<div class="cell">
<hbr-label-piece *ngIf="t.labels?.length" [label]="t.labels[0]" [labelWidth]="90"> </hbr-label-piece>
<div class="signpost-item" [hidden]="t.labels?.length<=1">
<div class="trigger-item">
<clr-signpost>
<button class="btn btn-link" clrSignpostTrigger>...</button>
<clr-signpost-content [clrPosition]="'left-top'" *clrIfOpen>
<div>
<hbr-label-piece *ngFor="let label of t.labels" [label]="label"></hbr-label-piece>
</div>
</clr-signpost-content>
</clr-signpost>
</div>
</div>
</div>
</clr-dg-cell>
<clr-dg-cell>{{t.push_time | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{t.pull_time | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>
<div class="cell">{{t.push_time | date: 'short'}}</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">{{t.pull_time | date: 'short'}}</div>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}</span> {{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -249,3 +249,13 @@ clr-datagrid {
::ng-deep .clr-form-control {
margin-top: 0;
}
.cell {
display: flex;
align-items: center;
width: 100%;
height: 100%;
}
.margin-top-m4{
margin-top: -4px;
}

View File

@ -25,7 +25,7 @@ import {
import { forkJoin, Observable, Subject, throwError as observableThrowError } from "rxjs";
import { catchError, debounceTime, distinctUntilChanged, finalize, map } from 'rxjs/operators';
import { TranslateService } from "@ngx-translate/core";
import { Comparator, Label, State, Tag, TagClickEvent } from "../service/interface";
import { Comparator, Label, State, Tag, TagClickEvent, VulnerabilitySummary } from "../service/interface";
import {
RequestQueryParams,
@ -84,7 +84,6 @@ export class TagComponent implements OnInit, AfterViewInit {
@Input() isGuest: boolean;
@Input() registryUrl: string;
@Input() withNotary: boolean;
@Input() withClair: boolean;
@Input() withAdmiral: boolean;
@Output() refreshRepo = new EventEmitter<boolean>();
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
@ -766,8 +765,9 @@ export class TagComponent implements OnInit, AfterViewInit {
this.scanningService.getProjectScanner(this.projectId)
.subscribe(response => {
if (response && "{}" !== JSON.stringify(response) && !response.disabled
&& response.uuid) {
this.getScannerMetadata(response.uuid);
&& response.health === "healthy") {
this.scanBtnState = ClrLoadingState.SUCCESS;
this.hasEnabledScanner = true;
} else {
this.scanBtnState = ClrLoadingState.ERROR;
}
@ -775,16 +775,8 @@ export class TagComponent implements OnInit, AfterViewInit {
this.scanBtnState = ClrLoadingState.ERROR;
});
}
getScannerMetadata(uuid: string) {
this.scanningService.getScannerMetadata(uuid)
.subscribe(response => {
this.hasEnabledScanner = true;
this.scanBtnState = ClrLoadingState.SUCCESS;
}, error => {
this.scanBtnState = ClrLoadingState.ERROR;
});
}
handleScanOverview(scanOverview: any) {
handleScanOverview(scanOverview: any): VulnerabilitySummary {
if (scanOverview) {
return scanOverview[DEFAULT_SUPPORTED_MIME_TYPE];
}

View File

@ -253,7 +253,8 @@ export const VULNERABILITY_SEVERITY = {
LOW: "Low",
MEDIUM: "Medium",
HIGH: "High",
CRITICAL: "Critical"
CRITICAL: "Critical",
NONE: "None"
};
/**

View File

@ -12,7 +12,7 @@
<div>{{'VULNERABILITY.STATE.SCANNING' | translate}}</div>
<div class="progress loop loop-height"><progress></progress></div>
</div>
<div *ngIf="completed" class="bar-state bar-state-chart">
<div *ngIf="completed" class="bar-state bar-state-chart margin-top-m15">
<hbr-result-tip-histogram [vulnerabilitySummary]="summary"></hbr-result-tip-histogram>
</div>
<div *ngIf="otherStatus" class="bar-state">

View File

@ -43,9 +43,6 @@ export class ResultGridComponent implements OnInit {
this.channel.tagDetail$.subscribe(tag => {
this.loadResults(this.repositoryId, this.tagId);
});
if (this.projectId) {
this.getProjectScanner();
}
}
getProjectScanner(): void {
this.hasEnabledScanner = false;
@ -53,8 +50,9 @@ export class ResultGridComponent implements OnInit {
this.scanningService.getProjectScanner(this.projectId)
.subscribe(response => {
if (response && "{}" !== JSON.stringify(response) && !response.disabled
&& response.uuid) {
this.getScannerMetadata(response.uuid);
&& response.health === "healthy") {
this.scanBtnState = ClrLoadingState.SUCCESS;
this.hasEnabledScanner = true;
} else {
this.scanBtnState = ClrLoadingState.ERROR;
}
@ -62,15 +60,6 @@ export class ResultGridComponent implements OnInit {
this.scanBtnState = ClrLoadingState.ERROR;
});
}
getScannerMetadata(uuid: string) {
this.scanningService.getScannerMetadata(uuid)
.subscribe(response => {
this.hasEnabledScanner = true;
this.scanBtnState = ClrLoadingState.SUCCESS;
}, error => {
this.scanBtnState = ClrLoadingState.ERROR;
});
}
loadResults(repositoryId: string, tagId: string): void {
// only show loading for one time
@ -138,6 +127,9 @@ export class ResultGridComponent implements OnInit {
USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE);
forkJoin(hasScanImagePermission).subscribe(permissions => {
this.hasScanImagePermission = permissions[0] as boolean;
if (this.projectId && this.hasScanImagePermission) {
this.getProjectScanner();
}
}, error => {
this.errorHandler.error(error);
});

View File

@ -1,14 +1,30 @@
<div class="tip-wrapper tip-position width-210">
<div class="tip-wrapper width-210">
<clr-tooltip *ngIf="!isNone">
<div clrTooltipTrigger class="level-border">
<div [className]="getClass()">
<div class="inner">
{{vulnerabilitySummary?.severity | slice:0:1}}
</div>
</div>
</div>
<clr-tooltip-content [clrPosition]="'left'" [clrSize]="'md'" *clrIfOpen>
<span class="font-weight-600">{{'SCANNER.VULNERABILITY_SEVERITY' | translate }}</span>
<span class="font-weight-600 margin-left-5">{{tipTitle}}</span>
</clr-tooltip-content>
</clr-tooltip>
<clr-tooltip>
<div clrTooltipTrigger class="tip-block">
<div *ngIf="!isNone" class="circle-block">
<div class="level-border" [className]="getClass()">{{vulnerabilitySummary?.severity | slice:0:1}}</div>
<div class="black-point margin-left-5"></div>
<span class="margin-left-5">{{total}}</span>
<div class="black-point-container margin-left-10">
<div class="black-point"></div>
</div>
<span class="margin-left-10 font-weight-800">{{total}}</span>
<span class="margin-left-5">{{'SCANNER.TOTAL' | translate}}</span>
<div class="black-point margin-left-10"></div>
<span class="margin-left-5">{{fixableCount}}</span>
<span class="margin-left-5">{{'SCANNER.FIXABLE' | translate}}</span>
<div class="black-point-container margin-left-10">
<div class="black-point "></div>
</div>
<span class="margin-left-10 font-weight-800 color-green">{{fixableCount}}</span>
<span class="margin-left-5 color-green">{{'SCANNER.FIXABLE' | translate}}</span>
</div>
<div *ngIf="isNone" class="margin-left-5 tip-wrapper bar-block-none shadow-none width-150">{{'VULNERABILITY.NO_VULNERABILITY' | translate }}</div>
</div>

View File

@ -1,3 +1,6 @@
$thirty-pixel: 30px;
$twenty-two-pixel: 22px;
.bar-wrapper {
width: 144px;
height: 12px;
@ -40,21 +43,13 @@
.tip-wrapper {
display: inline-block;
height: 15px;
color: #fff;
text-align: center;
font-size: 10px;
height: 15px;
line-height: 15px;
}
.tip-position {
margin-left: -4px;
}
.tip-block {
margin-left: -3px;
}
.bar-block-critical {
background-color: red;
}
@ -102,7 +97,7 @@
margin-bottom: 3px;
span {
:nth-child(1) {
width: 30px;
width: $thirty-pixel;
text-align: center;
display: inline-block;
}
@ -213,7 +208,7 @@ hr {
}
.width-30 {
width: 30px;
width: $thirty-pixel;
}
.width-210 {
@ -225,51 +220,74 @@ hr {
}
.circle-block {
color: #575757;
display: flex;
align-items: center;
div:first-child {
display: inline-block;
border-radius: 50%;
height: 20px;
width: 20px;
line-height: 20px;
font-size: 14px;
text-align: center;
}
}
.level-border {
border:1px solid #f8b5b4;
.level-border>div{
display: inline-flex;
align-items: center;
justify-items: center;
border-radius: 50%;
height: $thirty-pixel;
width: $thirty-pixel;
line-height: $thirty-pixel;
}
.inner {
margin: auto;
height: $twenty-two-pixel;
width: $twenty-two-pixel;
line-height: $twenty-two-pixel;
font-size: 14px;
font-weight: 600;
border-radius: 50%;
text-align: center;
background-color: #fff;
}
.level-critical {
background:red;
color:#621501;
color:red;
}
.level-high {
background:#e64524;
color:#621501;
color:#e64524;
}
.level-medium {
background-color: orange;
color:#621501;
color:orange;
}
.level-low {
background: #007CBB;
color:#cab6b1;
color:#007CBB;
}
.level-negligible {
background-color: green;
color:#bad7ba;
color:green;
}
.level-unknown {
background-color: grey;
color:#bad7ba;
color:grey;
}
.black-point-container {
display: inline-block;
position: relative;
height: 6px;
}
.black-point {
display: inline-block;
width: 4px;background-color: #000;
position: absolute;
width: 4px;
background-color: #000;
height: 4px;
border-radius: 50%;
}
.font-weight-800 {
font-weight: 800;
}
.color-green {
color: green;
}
.tip-block {
height: $thirty-pixel;
line-height: $thirty-pixel;
position: relative;
}

View File

@ -29,7 +29,7 @@ export class ResultTipHistogramComponent implements OnInit {
constructor(private translate: TranslateService) { }
ngOnInit(): void {
let key = "VULNERABILITY.SEVERITY.UNKNOWN";
let key = "VULNERABILITY.SEVERITY.NONE";
switch (this.vulnerabilitySummary.severity) {
case VULNERABILITY_SEVERITY.CRITICAL:
key = "VULNERABILITY.SEVERITY.CRITICAL";
@ -46,6 +46,9 @@ export class ResultTipHistogramComponent implements OnInit {
case VULNERABILITY_SEVERITY.NEGLIGIBLE:
key = "VULNERABILITY.SEVERITY.NEGLIGIBLE";
break;
case VULNERABILITY_SEVERITY.UNKNOWN:
key = "VULNERABILITY.SEVERITY.UNKNOWN";
break;
default:
break;
}

View File

@ -41,12 +41,6 @@
height: 10px;
max-width: 120px;
}
.tip-position {
margin-left: -4px;
}
.tip-block {
margin-left: -3px;
}
.bar-block-high {
background-color: #e64524;
}
@ -144,31 +138,29 @@ hr{
.label-critical {
background:red;
color:#621501;
border:1px solid #f8b5b4;
}
.label-danger {
background:#e64524!important;
color:#621501!important;
border:1px solid #f8b5b4!important;
}
.label-medium {
background-color: orange;
color:#621501;
border:1px solid #f8b5b4;
}
.label-low {
background: #007CBB;
color:#cab6b1;
border:1px solid #f8b5b4;
}
.label-negligible {
background-color: green;
color:#bad7ba;
border:1px solid #f8b5b4;
}
.label-unknown {
background-color: grey;
color:#bad7ba;
border:1px solid #f8b5b4;
}
.margin-top-m15{
margin-top: -15px;
}

View File

@ -111,11 +111,6 @@ export class HarborShellComponent implements OnInit, OnDestroy {
let account = this.session.getCurrentUser();
return account != null;
}
public get withClair(): boolean {
return this.appConfigService.getConfig().with_clair;
}
public get hasAdminRole(): boolean {
return this.session.getCurrentUser() &&
this.session.getCurrentUser().has_admin_role;

View File

@ -61,15 +61,15 @@
<clr-dg-row *clrDgItems="let scanner of scanners" [clrDgItem]="scanner">
<clr-dg-cell>
<span>{{scanner.name}}</span>
<span *ngIf="scanner.is_default" class="label label-info ml-1">{{'SCANNER.DEFAULT' | translate}}</span>
<span *ngIf="scanner.is_default" class="label label-info ml-1 label-in-cell">{{'SCANNER.DEFAULT' | translate}}</span>
</clr-dg-cell>
<clr-dg-cell>{{scanner.url}}</clr-dg-cell>
<clr-dg-cell>
<span *ngIf="scanner.loadingMetadata;else elseBlockLoading" class="spinner spinner-inline ml-2"></span>
<ng-template #elseBlockLoading>
<span *ngIf="scanner.metadata;else elseBlock" class="label label-success">{{'SCANNER.HEALTHY' | translate}}</span>
<span *ngIf="scanner.metadata;else elseBlock" class="label label-success label-in-cell">{{'SCANNER.HEALTHY' | translate}}</span>
<ng-template #elseBlock>
<span class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</span>
<span class="label label-danger label-in-cell">{{'SCANNER.UNHEALTHY' | translate}}</span>
</ng-template>
</ng-template>
</clr-dg-cell>

View File

@ -31,3 +31,7 @@
.margin-left-10 {
margin-left: 10px;
}
.label-in-cell {
position: absolute;
margin-top: -3px;
}

View File

@ -133,6 +133,7 @@ export class ConfigurationScannerComponent implements OnInit, OnDestroy {
resetValue['description'] = this.selectedRow.description;
resetValue['url'] = this.selectedRow.url;
resetValue['skipCertVerify'] = this.selectedRow.skip_certVerify;
resetValue['useInner'] = this.selectedRow.use_internal_addr;
if (this.selectedRow.auth === 'Basic') {
resetValue['auth'] = 'Basic';
let username: string = this.selectedRow.access_credential.split(":")[0];

View File

@ -67,4 +67,9 @@ export class ConfigScannerService {
return this.http.patch(`/api/scanners/${uid}`, {is_default: true} )
.pipe(catchError(error => observableThrowError(error)));
}
getProjectScanners(projectId: number) {
return this.http.get(`/api/projects/${projectId}/scanner/candidates`)
.pipe(map(response => response as Scanner[]))
.pipe(catchError(error => observableThrowError(error)));
}
}

View File

@ -105,19 +105,32 @@
</div>
</ng-container>
<div class="clr-form-control">
<label class="clr-control-label">{{"SCANNER.SKIP" | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
{{'SCANNER.SKIP_CERT_VERIFY' | translate}}
</clr-tooltip-content>
</clr-tooltip>
</label>
<label class="clr-control-label">{{"SCANNER.OPTIONS" | translate}}</label>
<div class="clr-control-container padding-top-3">
<div class="clr-checkbox-wrapper">
<input clrCheckbox formControlName="skipCertVerify"
<clr-checkbox-wrapper>
<input name="scanner-skipCertVerify" clrCheckbox formControlName="skipCertVerify"
type="checkbox" id="scanner-skipCertVerify">
</div>
<label class="width-10rem" for="scanner-skipCertVerify">{{"SCANNER.SKIP" | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content class="width-14rem" clrPosition="top-left" clrSize="lg" *clrIfOpen>
{{'SCANNER.SKIP_CERT_VERIFY' | translate}}
</clr-tooltip-content>
</clr-tooltip>
</label>
</clr-checkbox-wrapper>
<clr-checkbox-wrapper>
<input name="scanner-use-inner" clrCheckbox formControlName="useInner"
type="checkbox" id="scanner-use-inner">
<label class="width-10rem" for="scanner-use-inner">{{"SCANNER.USE_INNER" | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content class="width-14rem" clrPosition="top-left" clrSize="lg" *clrIfOpen>
{{"SCANNER.USE_INNER_TIP" | translate}}
</clr-tooltip-content>
</clr-tooltip>
</label>
</clr-checkbox-wrapper>
</div>
</div>
</form>

View File

@ -4,6 +4,10 @@
.padding-top-3 {
padding-top: 3px;
}
.clr-control-label {
width: 9rem !important;
.width-10rem {
width: 10rem;
}
.width-14rem {
width: 14rem;
}

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing';
import { async, ComponentFixture, ComponentFixtureAutoDetect, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { NewScannerFormComponent } from "./new-scanner-form.component";
import { FormBuilder } from "@angular/forms";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
@ -7,6 +7,7 @@ import { SharedModule } from "../../../shared/shared.module";
import { ConfigScannerService } from "../config-scanner.service";
import { of } from "rxjs";
import { TranslateService } from "@ngx-translate/core";
import { delay } from "rxjs/operators";
describe('NewScannerFormComponent', () => {
let mockScanner1 = {
@ -19,7 +20,10 @@ describe('NewScannerFormComponent', () => {
let fixture: ComponentFixture<NewScannerFormComponent>;
let fakedConfigScannerService = {
getScannersByName() {
return of([mockScanner1]);
return of([mockScanner1]).pipe(delay(500));
},
getScannersByEndpointUrl() {
return of([mockScanner1]).pipe(delay(500));
}
};
beforeEach(async(() => {
@ -57,32 +61,48 @@ describe('NewScannerFormComponent', () => {
let el = fixture.nativeElement.querySelector('clr-control-error');
expect(el).toBeTruthy();
});
it('name should be valid', () => {
it('name should be existed', fakeAsync(() => {
let nameInput = fixture.nativeElement.querySelector('#scanner-name');
nameInput.value = "test1";
nameInput.dispatchEvent(new Event('input'));
nameInput.blur();
nameInput.dispatchEvent(new Event('blur'));
let el = null;
setTimeout(() => {
el = fixture.nativeElement.querySelector('#name-error');
expect(el).toBeTruthy();
}, 20000);
tick(20000);
}));
it('name should be valid', fakeAsync(() => {
let nameInput = fixture.nativeElement.querySelector('#scanner-name');
nameInput.value = "test2";
nameInput.dispatchEvent(new Event('input'));
nameInput.blur();
nameInput.dispatchEvent(new Event('blur'));
let el = null;
setTimeout(() => {
let el = fixture.nativeElement.querySelector('#name-error');
el = fixture.nativeElement.querySelector('#name-error');
expect(el).toBeFalsy();
}, 11000);
});
}, 20000);
tick(20000);
}));
it('endpoint url should be valid', () => {
it('endpoint url should be valid', fakeAsync(() => {
let nameInput = fixture.nativeElement.querySelector('#scanner-name');
nameInput.value = "test2";
let urlInput = fixture.nativeElement.querySelector('#scanner-endpoint');
urlInput.value = "http://168.0.0.1";
urlInput.value = "http://168.0.0.2";
urlInput.dispatchEvent(new Event('input'));
urlInput.blur();
urlInput.dispatchEvent(new Event('blur'));
let el = null;
setTimeout(() => {
let el = fixture.nativeElement.querySelector('#endpoint-error');
expect(el).toBeFalsy();
}, 11000);
});
el = fixture.nativeElement.querySelector('#endpoint-error');
}, 20000);
tick(20000);
expect(el).toBeFalsy();
}));
it('auth should be valid', () => {
let authInput = fixture.nativeElement.querySelector('#scanner-authorization');

View File

@ -33,7 +33,8 @@ export class NewScannerFormComponent implements OnInit, AfterViewInit, OnDestro
token: this.fb.control("", Validators.required),
apiKey: this.fb.control("", Validators.required)
}),
skipCertVerify: this.fb.control(false)
skipCertVerify: this.fb.control(false),
useInner: this.fb.control(false)
});
checkNameSubscribe: any;
checkEndpointUrlSubscribe: any;

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing';
import { async, ComponentFixture, ComponentFixtureAutoDetect, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { ClrLoadingState } from "@clr/angular";
import { ConfigScannerService } from "../config-scanner.service";
import { NewScannerModalComponent } from "./new-scanner-modal.component";
@ -9,12 +9,13 @@ import { of, Subscription } from "rxjs";
import { delay } from "rxjs/operators";
import { SharedModule } from "@harbor/ui";
import { SharedModule as AppSharedModule } from "../../../shared/shared.module";
import { Scanner } from "../scanner";
describe('NewScannerModalComponent', () => {
let component: NewScannerModalComponent;
let fixture: ComponentFixture<NewScannerModalComponent>;
let mockScanner1 = {
let mockScanner1: Scanner = {
name: 'test1',
description: 'just a sample',
url: 'http://168.0.0.1',
@ -71,7 +72,7 @@ describe('NewScannerModalComponent', () => {
let el = fixture.nativeElement.querySelector('#button-add');
expect(el).toBeTruthy();
});
it('should be edit mode', () => {
it('should be edit mode', fakeAsync(() => {
component.isEdit = true;
fixture.detectChanges();
let el = fixture.nativeElement.querySelector('#button-save');
@ -97,10 +98,11 @@ describe('NewScannerModalComponent', () => {
el.click();
el.dispatchEvent(new Event('click'));
setTimeout(() => {
expect(component.opened).toBeFalsy();
}, 300);
});
it('test connection button should not be disabled', () => {
expect(component.opened).toBeFalsy();
}, 10000);
tick(10000);
}));
it('test connection button should not be disabled', fakeAsync(() => {
let nameInput = fixture.nativeElement.querySelector('#scanner-name');
nameInput.value = "test2";
nameInput.dispatchEvent(new Event('input'));
@ -114,9 +116,10 @@ describe('NewScannerModalComponent', () => {
expect(component.checkBtnState).toBe(ClrLoadingState.LOADING);
setTimeout(() => {
expect(component.checkBtnState).toBe(ClrLoadingState.SUCCESS);
}, 300);
});
it('add button should not be disabled', () => {
}, 10000);
tick(10000);
}));
it('add button should not be disabled', fakeAsync(() => {
fixture.nativeElement.querySelector('#scanner-name').value = "test2";
fixture.nativeElement.querySelector('#scanner-endpoint').value = "http://168.0.0.1";
let authInput = fixture.nativeElement.querySelector('#scanner-authorization');
@ -136,8 +139,9 @@ describe('NewScannerModalComponent', () => {
el.dispatchEvent(new Event('click'));
setTimeout(() => {
expect(component.opened).toBeFalsy();
}, 300);
});
}, 10000);
tick(10000);
}));
});

View File

@ -63,11 +63,12 @@ export class NewScannerModalComponent {
scanner.access_credential = value.accessCredential.token;
}
scanner.skip_certVerify = !!value.skipCertVerify;
scanner.use_internal_addr = !!value.useInner;
this.configScannerService.addScanner(scanner)
.pipe(finalize(() => this.onSaving = false))
.subscribe(response => {
this.close();
this.msgHandler.showSuccess("ADD_SUCCESS");
this.msgHandler.showSuccess("SCANNER.ADD_SUCCESS");
this.notify.emit();
this.saveBtnState = ClrLoadingState.SUCCESS;
}, error => {
@ -133,6 +134,9 @@ export class NewScannerModalComponent {
if (this.originValue.skipCertVerify !== this.newScannerFormComponent.newScannerForm.get('skipCertVerify').value) {
return true;
}
if (this.originValue.useInner !== this.newScannerFormComponent.newScannerForm.get('useInner').value) {
return true;
}
if (this.originValue.auth === "Basic") {
if (this.originValue.accessCredential.username !==
this.newScannerFormComponent.newScannerForm.get('accessCredential').get('username').value) {
@ -178,17 +182,18 @@ export class NewScannerModalComponent {
scanner.access_credential = value.accessCredential.token;
}
scanner.skip_certVerify = !!value.skipCertVerify;
scanner.use_internal_addr = !!value.useInner;
this.configScannerService.testEndpointUrl(scanner)
.pipe(finalize(() => this.onTesting = false))
.subscribe(response => {
this.inlineAlert.showInlineSuccess({
message: "TEST_PASS"
message: "SCANNER.TEST_PASS"
});
this.checkBtnState = ClrLoadingState.SUCCESS;
this.testMap[this.newScannerFormComponent.newScannerForm.get('url').value] = true;
}, error => {
this.inlineAlert.showInlineError({
message: "TEST_FAILED"
message: "SCANNER.TEST_FAILED"
});
this.checkBtnState = ClrLoadingState.ERROR;
});
@ -213,12 +218,13 @@ export class NewScannerModalComponent {
this.editScanner.access_credential = value.accessCredential.token;
}
this.editScanner.skip_certVerify = !!value.skipCertVerify;
this.editScanner.use_internal_addr = !!value.useInner;
this.editScanner.uuid = this.uid;
this.configScannerService.updateScanner(this.editScanner)
.pipe(finalize(() => this.onSaving = false))
.subscribe(response => {
this.close();
this.msgHandler.showSuccess("UPDATE_SUCCESS");
this.msgHandler.showSuccess("SCANNER.UPDATE_SUCCESS");
this.notify.emit();
this.saveBtnState = ClrLoadingState.SUCCESS;
}, error => {

View File

@ -7,16 +7,18 @@ export class Scanner {
url?: string;
auth?: string;
access_credential?: string;
scanner?: string;
adapter?: string;
disabled?: boolean;
is_default?: boolean;
skip_certVerify?: boolean;
use_internal_addr?: boolean;
create_time?: any;
update_time?: any;
vendor?: string;
version?: string;
metadata?: ScannerMetadata;
loadingMetadata?: boolean;
health?: string;
constructor() {
}
}

View File

@ -302,8 +302,8 @@ const harborRoutes: Routes = [
path: 'scanner',
data: {
permissionParam: {
resource: USERSTATICPERMISSION.CONFIGURATION.KEY,
action: USERSTATICPERMISSION.CONFIGURATION.VALUE.READ
resource: USERSTATICPERMISSION.SCANNER.KEY,
action: USERSTATICPERMISSION.SCANNER.VALUE.READ
}
},
component: ScannerComponent

View File

@ -34,7 +34,7 @@
<li class="nav-item" *ngIf="hasWebhookListPermission">
<a class="nav-link" routerLink="webhook" routerLinkActive="active">{{'PROJECT_DETAIL.WEBHOOKS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSessionValid && (hasConfigurationListPermission)">
<li class="nav-item" *ngIf="hasScannerReadPermission">
<a class="nav-link" routerLink="scanner" routerLinkActive="active">{{'SCANNER.SCANNER' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSessionValid && (hasConfigurationListPermission)">

View File

@ -45,6 +45,7 @@ export class ProjectDetailComponent implements OnInit {
hasRobotListPermission: boolean;
hasTagRetentionPermission: boolean;
hasWebhookListPermission: boolean;
hasScannerReadPermission: boolean;
constructor(
private route: ActivatedRoute,
private router: Router,
@ -89,11 +90,14 @@ export class ProjectDetailComponent implements OnInit {
USERSTATICPERMISSION.TAG_RETENTION.KEY, USERSTATICPERMISSION.TAG_RETENTION.VALUE.READ));
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.WEBHOOK.KEY, USERSTATICPERMISSION.WEBHOOK.VALUE.LIST));
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.SCANNER.KEY, USERSTATICPERMISSION.SCANNER.VALUE.READ));
forkJoin(...permissionsList).subscribe(Rules => {
[this.hasProjectReadPermission, this.hasLogListPermission, this.hasConfigurationListPermission, this.hasMemberListPermission
, this.hasLabelListPermission, this.hasRepositoryListPermission, this.hasHelmChartsListPermission, this.hasRobotListPermission
, this.hasLabelCreatePermission, this.hasTagRetentionPermission, this.hasWebhookListPermission] = Rules;
, this.hasLabelCreatePermission, this.hasTagRetentionPermission, this.hasWebhookListPermission,
this.hasScannerReadPermission] = Rules;
}, error => this.errorHandler.error(error));
}

View File

@ -5,8 +5,8 @@
<div class="clr-form-control">
<label class="clr-control-label name">{{'SCANNER.SCANNER' | translate}}</label>
<div class="clr-control-container">
<label *ngIf="!(scanners && scanners.length > 0)" class="name">{{'SCANNER.NOT_AVAILABLE' | translate}}</label>
<button *ngIf="(!scanner) && hasCreatePermission && scanners && scanners.length > 0" id="edit-scanner-copy" class="btn btn-primary btn-sm" (click)="open()">{{'SCANNER.EDIT' | translate}}</button>
<label *ngIf="(!scanner) && hasCreatePermission && !(scanners && scanners.length > 0)" class="name">{{'SCANNER.NOT_AVAILABLE' | translate}}</label>
</div>
</div>
<ng-container *ngIf="scanner">
@ -16,15 +16,10 @@
<div class="clr-input-wrapper">
<div class="clr-input-wrapper">
<span id="scanner-name" class="scanner-name">{{scanner?.name}}</span>
<button *ngIf="scanners && scanners.length > 0" id="edit-scanner" class="btn btn-primary btn-sm" (click)="open()">{{'SCANNER.EDIT' | translate}}</button>
<button *ngIf="hasCreatePermission && scanners && scanners.length > 0" id="edit-scanner" class="btn btn-primary btn-sm" (click)="open()">{{'SCANNER.EDIT' | translate}}</button>
<span *ngIf="scanner?.disabled" class="label label-warning ml-1">{{'SCANNER.DISABLED' | translate}}</span>
<span *ngIf="scanner?.loadingMetadata;else elseBlockLoading" class="spinner spinner-inline ml-2"></span>
<ng-template #elseBlockLoading>
<span *ngIf="scanner?.metadata;else elseBlock" class="label label-success ml-1">{{'SCANNER.HEALTHY' | translate}}</span>
<ng-template #elseBlock>
<span class="label label-danger ml-1">{{'SCANNER.UNHEALTHY' | translate}}</span>
</ng-template>
</ng-template>
<span *ngIf="scanner?.health === 'unhealthy'" class="label label-danger ml-1">{{'SCANNER.UNHEALTHY' | translate}}</span>
<span *ngIf="scanner?.health === 'healthy'" class="label label-success ml-1">{{'SCANNER.HEALTHY' | translate}}</span>
</div>
</div>
</div>
@ -38,29 +33,29 @@
</div>
</div>
</div>
<div class="clr-form-control" *ngIf="scanner?.metadata?.scanner?.name">
<div class="clr-form-control" *ngIf="scanner?.adapter">
<label class="clr-control-label">{{'SCANNER.ADAPTER' | translate}}</label>
<div class="clr-control-container">
<div class="clr-input-wrapper">
<input [ngModel]="scanner?.metadata?.scanner?.name" readonly class="clr-input width-240" type="text" id="scanner-scanner"
<input [ngModel]="scanner?.adapter" readonly class="clr-input width-240" type="text" id="scanner-scanner"
autocomplete="off">
</div>
</div>
</div>
<div class="clr-form-control" *ngIf="scanner?.metadata?.scanner?.vendor">
<div class="clr-form-control" *ngIf="scanner?.vendor">
<label class="clr-control-label">{{'SCANNER.VENDOR' | translate}}</label>
<div class="clr-control-container">
<div class="clr-input-wrapper">
<input [ngModel]="scanner?.metadata?.scanner?.vendor" readonly class="clr-input width-240" type="text" id="scanner-vendor"
<input [ngModel]="scanner?.vendor" readonly class="clr-input width-240" type="text" id="scanner-vendor"
autocomplete="off">
</div>
</div>
</div>
<div class="clr-form-control" *ngIf="scanner?.metadata?.scanner?.version">
<div class="clr-form-control" *ngIf="scanner?.version">
<label class="clr-control-label">{{'SCANNER.VERSION' | translate}}</label>
<div class="clr-control-container">
<div class="clr-input-wrapper">
<input [ngModel]="scanner?.metadata?.scanner?.version" readonly class="clr-input width-240" type="text" id="scanner-version"
<input [ngModel]="scanner?.version" readonly class="clr-input width-240" type="text" id="scanner-version"
autocomplete="off">
</div>
</div>
@ -74,26 +69,16 @@
<clr-datagrid [(clrDgSingleSelected)]="selectedScanner">
<clr-dg-column [clrDgField]="'name'">{{'SCANNER.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'url'">{{'SCANNER.ENDPOINT' | translate}}</clr-dg-column>
<clr-dg-column>{{'SCANNER.HEALTH' | translate}}</clr-dg-column>
<clr-dg-column>{{'SCANNER.DEFAULT' | translate}}</clr-dg-column>
<clr-dg-column>{{'SCANNER.AUTH' | translate}}</clr-dg-column>
<clr-dg-column>{{'SCANNER.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let scanner of scanners" [clrDgItem]="scanner">
<clr-dg-cell>{{scanner.name}}</clr-dg-cell>
<clr-dg-cell>{{scanner.url}}</clr-dg-cell>
<clr-dg-cell>
<span *ngIf="scanner.loadingMetadata;else elseBlockLoading" class="spinner spinner-inline ml-2">Loading...</span>
<ng-template #elseBlockLoading>
<span *ngIf="scanner.metadata;else elseBlock" class="label label-success">{{'SCANNER.HEALTHY' | translate}}</span>
<ng-template #elseBlock>
<span class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</span>
</ng-template>
</ng-template>
</clr-dg-cell>
<clr-dg-cell>
<span *ngIf="scanner.is_default" class="label label-info">{{scanner.is_default}}</span>
<span *ngIf="scanner.is_default" class="label label-info label-in-cell">{{scanner.is_default}}</span>
<span *ngIf="!scanner.is_default">{{scanner.is_default}}</span>
</clr-dg-cell>
<clr-dg-cell>{{scanner.auth?scanner.auth:('SCANNER.NONE'|translate)}}</clr-dg-cell>
<clr-dg-cell>{{scanner.description}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="scanners?.length > 0">1 - {{scanners?.length}} {{'WEBHOOK.OF' | translate}} </span> {{scanners?.length}} {{'WEBHOOK.ITEMS' | translate}}

View File

@ -21,3 +21,7 @@
.clr-form-control {
margin-top: 0.75rem !important;
}
.label-in-cell {
position: absolute;
margin-top: -3px;
}

View File

@ -15,7 +15,7 @@ import { Component, OnInit, ViewChild } from "@angular/core";
import { ConfigScannerService } from "../../config/scanner/config-scanner.service";
import { Scanner } from "../../config/scanner/scanner";
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
import { ErrorHandler } from "@harbor/ui";
import { ErrorHandler, UserPermissionService, USERSTATICPERMISSION } from "@harbor/ui";
import { ActivatedRoute } from "@angular/router";
import { ClrLoadingState } from "@clr/angular";
import { InlineAlertComponent } from "../../shared/inline-alert/inline-alert.component";
@ -36,72 +36,57 @@ export class ScannerComponent implements OnInit {
selectedScanner: Scanner;
saveBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
onSaving: boolean = false;
hasCreatePermission: boolean = false;
@ViewChild(InlineAlertComponent, { static: false }) inlineAlert: InlineAlertComponent;
constructor( private configScannerService: ConfigScannerService,
private msgHandler: MessageHandlerService,
private errorHandler: ErrorHandler,
private route: ActivatedRoute,
private userPermissionService: UserPermissionService,
) {
}
ngOnInit() {
this.projectId = +this.route.snapshot.parent.params['id'];
this.getPermission();
this.init();
}
getPermission() {
if (this.projectId) {
this.userPermissionService.getPermission(this.projectId,
USERSTATICPERMISSION.SCANNER.KEY, USERSTATICPERMISSION.SCANNER.VALUE.CREATE)
.subscribe( permission => {
this.hasCreatePermission = permission;
if (this.hasCreatePermission) {
this.getScanners();
}
});
}
}
init() {
this.getScanner();
this.getScanners();
}
getScanner() {
this.loading = true;
this.configScannerService.getProjectScanner(this.projectId)
.pipe(finalize(() => this.loading = false))
.subscribe(response => {
if (response && "{}" !== JSON.stringify(response)) {
this.scanner = response;
this.getScannerMetadata();
}
}, error => {
this.errorHandler.error(error);
});
}
getScannerMetadata() {
if (this.scanner && this.scanner.uuid) {
this.scanner.loadingMetadata = true;
this.configScannerService.getScannerMetadata(this.scanner.uuid)
.pipe(finalize(() => this.scanner.loadingMetadata = false))
.subscribe(response => {
this.scanner.metadata = response;
}, error => {
this.scanner.metadata = null;
});
}
}
getScanners() {
this.loading = true;
this.configScannerService.getScanners()
.pipe(finalize(() => this.loading = false))
.subscribe(response => {
if (response && response.length > 0) {
this.scanners = response.filter(scanner => {
return !scanner.disabled;
});
}
}, error => {
this.errorHandler.error(error);
});
}
getMetadataForAll() {
if (this.scanners && this.scanners.length > 0) {
this.scanners.forEach((scanner, index) => {
if (scanner.uuid ) {
this.scanners[index].loadingMetadata = true;
this.configScannerService.getScannerMetadata(scanner.uuid)
.pipe(finalize(() => this.scanners[index].loadingMetadata = false))
.subscribe(response => {
this.scanners[index].metadata = response;
}, error => {
this.scanners[index].metadata = null;
if (this.projectId) {
this.configScannerService.getProjectScanners(this.projectId)
.subscribe(response => {
if (response && response.length > 0) {
this.scanners = response.filter(scanner => {
return !scanner.disabled;
});
}
});
}
});
}
}
close() {
@ -116,7 +101,6 @@ export class ScannerComponent implements OnInit {
this.selectedScanner = s;
}
});
this.getMetadataForAll();
}
get valid(): boolean {
return this.selectedScanner

View File

@ -12,7 +12,7 @@
<div class="clr-row">
<div class="clr-col-2 flex-150">
<div class="dropdown" [ngClass]="{open:ruleIndex===i}">
<button (click)="openEditor(i)" class="dropdown-toggle btn btn-link btn-sm">
<button (click)="openEditor(i)" class="padding-left-0 dropdown-toggle btn btn-link btn-sm">
{{'TAG_RETENTION.ACTION' | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
@ -57,10 +57,10 @@
</ul>
<div class="v-center clr-row" [ngClass]="{'pt-1':retention?.rules?.length > 0}">
<div class="clr-col-2 flex-150"></div>
<div class="clr-col-2">
<div class="flex-8p">
<button [disabled]="retention?.rules?.length >= 15" class="btn btn-primary btn-sm" (click)="openAddRule()">{{'TAG_RETENTION.ADD_RULE' | translate}}</button>
</div>
<div class="clr-col-8 color-97 font-size-54">
<div class="clr-col-6 color-97 font-size-54">
{{'TAG_RETENTION.ADD_RULE_HELP_1' | translate}}
</div>
</div>

View File

@ -72,4 +72,12 @@
}
.flex-150 {
flex: 0 0 150px;
max-width: 150px;
}
.padding-left-0 {
padding-left: 0;
}
.flex-8p {
flex: 0 0 8.3%;
padding-left: 0.5rem;
}

View File

@ -44,10 +44,6 @@ export class TagDetailPageComponent implements OnInit {
return this.appConfigService.getConfig().with_admiral;
}
get withClair(): boolean {
return this.appConfigService.getConfig().with_clair;
}
goBack(tag: string): void {
this.router.navigate(["harbor", "projects", this.projectId, "repositories", tag]);
}

View File

@ -65,11 +65,6 @@ export class TagRepositoryComponent implements OnInit {
get withNotary(): boolean {
return this.appConfigService.getConfig().with_notary;
}
get withClair(): boolean {
return this.appConfigService.getConfig().with_clair;
}
get withAdmiral(): boolean {
return this.appConfigService.getConfig().with_admiral;
}

View File

@ -284,3 +284,7 @@
.text-xs-center {
text-align: center !important;
}
.float-lg-right {
float: right;
}

View File

@ -951,7 +951,7 @@
"SCANNING_TIME": "Scan completed time:",
"TOOLTIPS_TITLE": "{{totalVulnerability}} of {{totalPackages}} {{package}} have known {{vulnerability}}.",
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} of {{totalPackages}} {{package}} has known {{vulnerability}}.",
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability package found"
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
},
"SEVERITY": {
"CRITICAL": "Critical",
@ -1257,7 +1257,7 @@
"TOKEN": "Token",
"TOKEN_REQUIRED": "Token is required",
"API_KEY_REQUIRED": "APIKey is required",
"SKIP": "Skip Certificate Verification",
"SKIP": "Skip certificate verification",
"ADD_SCANNER": "Add Scanner",
"EDIT_SCANNER": "Edit Scanner",
"TEST_CONNECTION": "TEST CONNECTION",
@ -1295,6 +1295,10 @@
"DELETE_SUCCESS": "Successfully deleted",
"TOTAL": "Total",
"FIXABLE": "Fixable",
"DURATION": "Duration:"
"DURATION": "Duration:",
"OPTIONS": "Options",
"USE_INNER": "Use internal registry address",
"USE_INNER_TIP": "If the option is checked, the scanner will be forced to use the internal registry address to access the related contents.",
"VULNERABILITY_SEVERITY": "Vulnerability severity:"
}
}

View File

@ -950,7 +950,7 @@
"SCANNING_TIME": "Scan completed time:",
"TOOLTIPS_TITLE": "{{totalVulnerability}} of {{totalPackages}} {{package}} have known {{vulnerability}}.",
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} of {{totalPackages}} {{package}} has known {{vulnerability}}.",
"TOOLTIPS_TITLE_ZERO": "No se encontró ningún paquete de vulnerabilidad reconocible"
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
},
"SEVERITY": {
"CRITICAL": "Critical",
@ -1254,7 +1254,7 @@
"TOKEN": "Token",
"TOKEN_REQUIRED": "Token is required",
"API_KEY_REQUIRED": "APIKey is required",
"SKIP": "Skip Certificate Verification",
"SKIP": "Skip certificate verification",
"ADD_SCANNER": "Add Scanner",
"EDIT_SCANNER": "Edit Scanner",
"TEST_CONNECTION": "TEST CONNECTION",
@ -1292,6 +1292,10 @@
"DELETE_SUCCESS": "Successfully deleted",
"TOTAL": "Total",
"FIXABLE": "Fixable",
"DURATION": "Duration:"
"DURATION": "Duration:",
"OPTIONS": "Options",
"USE_INNER": "Use internal registry address",
"USE_INNER_TIP": "If the option is checked, the scanner will be forced to use the internal registry address to access the related contents.",
"VULNERABILITY_SEVERITY": "Vulnerability severity:"
}
}

View File

@ -924,7 +924,7 @@
"SCANNING_TIME": "Temps d'analyse complète :",
"TOOLTIPS_TITLE": "{{totalVulnerability}} de {{totalPackages}} {{package}} ont des {{vulnerability}} connues.",
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} de {{totalPackages}} {{package}} a des {{vulnerability}} connues.",
"TOOLTIPS_TITLE_ZERO": "Aucun paquet de vulnérabilité connue trouvé"
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
},
"SEVERITY": {
"CRITICAL": "Critique",
@ -1226,7 +1226,7 @@
"TOKEN": "Token",
"TOKEN_REQUIRED": "Token is required",
"API_KEY_REQUIRED": "APIKey is required",
"SKIP": "Skip Certificate Verification",
"SKIP": "Skip certificate verification",
"ADD_SCANNER": "Add Scanner",
"EDIT_SCANNER": "Edit Scanner",
"TEST_CONNECTION": "TEST CONNECTION",
@ -1264,6 +1264,10 @@
"DELETE_SUCCESS": "Successfully deleted",
"TOTAL": "Total",
"FIXABLE": "Fixable",
"DURATION": "Duration:"
"DURATION": "Duration:",
"OPTIONS": "Options",
"USE_INNER": "Use internal registry address",
"USE_INNER_TIP": "If the option is checked, the scanner will be forced to use the internal registry address to access the related contents.",
"VULNERABILITY_SEVERITY": "Vulnerability severity:"
}
}

View File

@ -945,7 +945,7 @@
"SCANNING_TIME": "Tempo de conclusão da análise:",
"TOOLTIPS_TITLE": "{{totalVulnerability}} de {{totalPackages}} {{package}} possuem {{vulnerability}}.",
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} de {{totalPackages}} {{package}} possuem {{vulnerability}}.",
"TOOLTIPS_TITLE_ZERO": "Nenhum pacote vulnerável reconhecido foi encontrado"
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
},
"SEVERITY": {
"CRITICAL": "Crítico",
@ -1251,7 +1251,7 @@
"TOKEN": "Token",
"TOKEN_REQUIRED": "Token is required",
"API_KEY_REQUIRED": "APIKey is required",
"SKIP": "Skip Certificate Verification",
"SKIP": "Skip certificate verification",
"ADD_SCANNER": "Add Scanner",
"EDIT_SCANNER": "Edit Scanner",
"TEST_CONNECTION": "TEST CONNECTION",
@ -1289,7 +1289,11 @@
"DELETE_SUCCESS": "Successfully deleted",
"TOTAL": "Total",
"FIXABLE": "Fixable",
"DURATION": "Duration:"
"DURATION": "Duration:",
"OPTIONS": "Options",
"USE_INNER": "Use internal registry address",
"USE_INNER_TIP": "If the option is checked, the scanner will be forced to use the internal registry address to access the related contents.",
"VULNERABILITY_SEVERITY": "Vulnerability severity:"
}
}

View File

@ -950,7 +950,7 @@
"SCANNING_TIME": "Tarama tamamlanma zamanı:",
"TOOLTIPS_TITLE": "{{totalVulnerability}} 'nın {{totalPackages}} {{package}} bilinen {{vulnerability}}.",
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} 'nın {{totalPackages}} {{package}} bilinen {{vulnerability}}.",
"TOOLTIPS_TITLE_ZERO": "Tanınabilir bir güvenlik açığı paketi bulunamadı"
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
},
"SEVERITY": {
"CRITICAL": "Kritik",
@ -1256,7 +1256,7 @@
"TOKEN": "Token",
"TOKEN_REQUIRED": "Token is required",
"API_KEY_REQUIRED": "APIKey is required",
"SKIP": "Skip Certificate Verification",
"SKIP": "Skip certificate verification",
"ADD_SCANNER": "Add Scanner",
"EDIT_SCANNER": "Edit Scanner",
"TEST_CONNECTION": "TEST CONNECTION",
@ -1294,6 +1294,10 @@
"DELETE_SUCCESS": "Successfully deleted",
"TOTAL": "Total",
"FIXABLE": "Fixable",
"DURATION": "Duration:"
"DURATION": "Duration:",
"OPTIONS": "Options",
"USE_INNER": "Use internal registry address",
"USE_INNER_TIP": "If the option is checked, the scanner will be forced to use the internal registry address to access the related contents.",
"VULNERABILITY_SEVERITY": "Vulnerability severity:"
}
}

View File

@ -950,7 +950,7 @@
"SCANNING_TIME": "扫描完成时间:",
"TOOLTIPS_TITLE": "{{totalPackages}}个{{package}}中的{{totalVulnerability}}个含有{{vulnerability}}。",
"TOOLTIPS_TITLE_SINGULAR": "{{totalPackages}}个{{package}}中的{{totalVulnerability}}个含有{{vulnerability}}。",
"TOOLTIPS_TITLE_ZERO": "没有发现可识别的漏洞"
"TOOLTIPS_TITLE_ZERO": "没有发现可识别的漏洞"
},
"SEVERITY": {
"CRITICAL": "危急",
@ -1241,7 +1241,7 @@
"ENDPOINT_EXISTS": "地址已存在",
"ENDPOINT_REQUIRED": "地址为必填项",
"ILLEGAL_ENDPOINT": "非法地址",
"AUTH": "Authorization",
"AUTH": "认证模型",
"NONE": "None",
"BASIC": "Basic",
"BEARER": "Bearer",
@ -1291,6 +1291,10 @@
"DELETE_SUCCESS": "删除成功",
"TOTAL": "总计",
"FIXABLE": "可修复",
"DURATION": "扫描用时:"
"DURATION": "扫描用时:",
"OPTIONS": "选项",
"USE_INNER": "使用仓库内部地址",
"USE_INNER_TIP": "选中此项,扫描器将使用仓库内部地址访问其相关内容",
"VULNERABILITY_SEVERITY": "漏洞严重度:"
}
}