mirror of
https://github.com/goharbor/harbor
synced 2025-04-22 17:59:30 +00:00
Add CVE data exporting UI (#16236)
Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
parent
130452111b
commit
aa3cdcbc6c
@ -0,0 +1,235 @@
|
|||||||
|
<clr-modal
|
||||||
|
clrModalSize="md"
|
||||||
|
[(clrModalOpen)]="opened"
|
||||||
|
[clrModalStaticBackdrop]="true"
|
||||||
|
[clrModalClosable]="true">
|
||||||
|
<h3 class="modal-title">{{ 'CVE_EXPORT.EXPORT_TITLE' | translate }}</h3>
|
||||||
|
<div class="modal-body">
|
||||||
|
<inline-alert class="modal-title"></inline-alert>
|
||||||
|
<p class="mt-0">{{ 'CVE_EXPORT.EXPORT_SUBTITLE' | translate }}</p>
|
||||||
|
<form #exportCVEForm="ngForm" class="clr-form clr-form-horizontal">
|
||||||
|
<section class="form-block">
|
||||||
|
<!-- projects -->
|
||||||
|
<div class="clr-form-control">
|
||||||
|
<label class="clr-control-label required">{{
|
||||||
|
'SIDE_NAV.PROJECTS' | translate
|
||||||
|
}}</label>
|
||||||
|
<div class="clr-control-container">
|
||||||
|
<div class="clr-input-wrapper flex">
|
||||||
|
<span #names class="names"
|
||||||
|
>{{ getProjectNames() | translate }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
*ngIf="
|
||||||
|
isOverflow() && !!selectedProjects?.length
|
||||||
|
"
|
||||||
|
>({{ selectedProjects?.length }})</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- filters-repo -->
|
||||||
|
<div class="clr-form-control">
|
||||||
|
<label for="repo" class="clr-control-label">{{
|
||||||
|
'P2P_PROVIDER.FILTERS' | translate
|
||||||
|
}}</label>
|
||||||
|
<div class="clr-control-container">
|
||||||
|
<div class="clr-input-wrapper">
|
||||||
|
<label class="sub-label">{{
|
||||||
|
'P2P_PROVIDER.REPOS' | translate
|
||||||
|
}}</label>
|
||||||
|
<input
|
||||||
|
placeholder="**"
|
||||||
|
[disabled]="loading"
|
||||||
|
autocomplete="off"
|
||||||
|
class="clr-input width-220"
|
||||||
|
type="text"
|
||||||
|
id="repo"
|
||||||
|
[(ngModel)]="repos"
|
||||||
|
size="30"
|
||||||
|
name="repo" />
|
||||||
|
<clr-icon
|
||||||
|
class="clr-validate-icon"
|
||||||
|
shape="exclamation-circle"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<clr-control-helper
|
||||||
|
class="margin-left-90px opacity-08"
|
||||||
|
>{{
|
||||||
|
'TAG_RETENTION.REP_SEPARATOR' | translate
|
||||||
|
}}</clr-control-helper
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- filters-tag -->
|
||||||
|
<div class="clr-form-control margin-top-06">
|
||||||
|
<label for="repo" class="clr-control-label"></label>
|
||||||
|
<div class="clr-control-container">
|
||||||
|
<div class="clr-input-wrapper">
|
||||||
|
<label class="sub-label">{{
|
||||||
|
'P2P_PROVIDER.TAGS' | translate
|
||||||
|
}}</label>
|
||||||
|
<input
|
||||||
|
placeholder="**"
|
||||||
|
[disabled]="loading"
|
||||||
|
autocomplete="off"
|
||||||
|
class="clr-input width-220"
|
||||||
|
type="text"
|
||||||
|
id="tag"
|
||||||
|
[(ngModel)]="tags"
|
||||||
|
size="30"
|
||||||
|
name="tag" />
|
||||||
|
<clr-icon
|
||||||
|
class="clr-validate-icon"
|
||||||
|
shape="exclamation-circle"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<clr-control-helper
|
||||||
|
class="margin-left-90px opacity-08"
|
||||||
|
>{{
|
||||||
|
'P2P_PROVIDER.TAG_SEPARATOR' | translate
|
||||||
|
}}</clr-control-helper
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- filters-label -->
|
||||||
|
<div class="clr-form-control margin-top-06">
|
||||||
|
<label for="repo" class="clr-control-label"></label>
|
||||||
|
<div class="clr-control-container">
|
||||||
|
<div class="clr-input-wrapper">
|
||||||
|
<label class="sub-label">{{
|
||||||
|
'P2P_PROVIDER.LABELS' | translate
|
||||||
|
}}</label>
|
||||||
|
<div class="dropdown clr-select-wrapper absolute">
|
||||||
|
<clr-dropdown class="width-tag-label">
|
||||||
|
<div class="label-text">
|
||||||
|
<div
|
||||||
|
class="dropdown-toggle"
|
||||||
|
clrDropdownTrigger>
|
||||||
|
<ng-container
|
||||||
|
*ngFor="
|
||||||
|
let l of selectedLabels;
|
||||||
|
let i = index
|
||||||
|
">
|
||||||
|
<hbr-label-piece
|
||||||
|
*ngIf="i <= 0"
|
||||||
|
[hasIcon]="false"
|
||||||
|
[label]="l"
|
||||||
|
[labelWidth]="
|
||||||
|
84
|
||||||
|
"></hbr-label-piece>
|
||||||
|
</ng-container>
|
||||||
|
<span
|
||||||
|
class="ellipsis color-white-dark"
|
||||||
|
*ngIf="
|
||||||
|
selectedLabels.length > 1
|
||||||
|
"
|
||||||
|
>···</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<clr-dropdown-menu
|
||||||
|
[ngStyle]="{ 'max-height.px': 230 }"
|
||||||
|
class="right-align"
|
||||||
|
clrPosition="bottom-left"
|
||||||
|
*clrIfOpen>
|
||||||
|
<clr-spinner
|
||||||
|
class="spinner"
|
||||||
|
*ngIf="loadingAllLabels"
|
||||||
|
[clrMedium]="true"></clr-spinner>
|
||||||
|
<ng-container *ngIf="!loadingAllLabels">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dropdown-item flex"
|
||||||
|
*ngFor="let label of allLabels"
|
||||||
|
(click)="
|
||||||
|
selectOrUnselect(label)
|
||||||
|
">
|
||||||
|
<clr-icon
|
||||||
|
shape="check"
|
||||||
|
[style.visibility]="
|
||||||
|
isSelected(label)
|
||||||
|
? 'visible'
|
||||||
|
: 'hidden'
|
||||||
|
"></clr-icon>
|
||||||
|
<hbr-label-piece
|
||||||
|
[label]="label"
|
||||||
|
[labelWidth]="
|
||||||
|
130
|
||||||
|
"></hbr-label-piece>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dropdown-item space-between no-labels"
|
||||||
|
*ngIf="!allLabels?.length">
|
||||||
|
<span class="alert-label">{{
|
||||||
|
'REPLICATION.NO_LABEL_INFO'
|
||||||
|
| translate
|
||||||
|
}}</span>
|
||||||
|
<span
|
||||||
|
class="alert-label go-link"
|
||||||
|
routerLink="/harbor/labels"
|
||||||
|
>{{
|
||||||
|
'CONFIG.LABEL'
|
||||||
|
| translate
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</clr-dropdown-menu>
|
||||||
|
</clr-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- filters-CVE-ids -->
|
||||||
|
<div class="clr-form-control margin-top-06">
|
||||||
|
<label for="ids" class="clr-control-label"></label>
|
||||||
|
<div class="clr-control-container">
|
||||||
|
<div class="clr-input-wrapper">
|
||||||
|
<label class="sub-label">{{
|
||||||
|
'CVE_EXPORT.CVE_IDS' | translate
|
||||||
|
}}</label>
|
||||||
|
<input
|
||||||
|
[disabled]="loading"
|
||||||
|
autocomplete="off"
|
||||||
|
class="clr-input width-220"
|
||||||
|
type="text"
|
||||||
|
id="ids"
|
||||||
|
[(ngModel)]="CVEIds"
|
||||||
|
size="30"
|
||||||
|
name="tag" />
|
||||||
|
<clr-icon
|
||||||
|
class="clr-validate-icon"
|
||||||
|
shape="exclamation-circle"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<clr-control-helper
|
||||||
|
class="margin-left-90px opacity-08"
|
||||||
|
>{{
|
||||||
|
'CVE_EXPORT.EXPORT_CVE_FILTER_HELP_TEXT'
|
||||||
|
| translate
|
||||||
|
}}</clr-control-helper
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button
|
||||||
|
[disabled]="loading"
|
||||||
|
(click)="cancel()"
|
||||||
|
id="system-robot-cancel"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline">
|
||||||
|
{{ 'BUTTON.CANCEL' | translate }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
[clrLoading]="saveBtnState"
|
||||||
|
[disabled]="loading || currentForm.invalid"
|
||||||
|
(click)="save()"
|
||||||
|
id="system-robot-save"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary">
|
||||||
|
{{ 'CVE_EXPORT.EXPORT_BUTTON' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</clr-modal>
|
@ -0,0 +1,80 @@
|
|||||||
|
@mixin cus-font {
|
||||||
|
font-size: .5417rem;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-label {
|
||||||
|
display: inline-block;
|
||||||
|
width: 90px;
|
||||||
|
@include cus-font;
|
||||||
|
}
|
||||||
|
|
||||||
|
.width-220 {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text {
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #000;
|
||||||
|
height: 1.2rem;
|
||||||
|
margin: 0 !important;
|
||||||
|
line-height: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 6px;
|
||||||
|
outline: none;
|
||||||
|
border-bottom: 1px solid rgb(154 154 154);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-toggle {
|
||||||
|
height: 24px;
|
||||||
|
width: 212px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-align {
|
||||||
|
min-width: 220px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-labels {
|
||||||
|
cursor: default;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
min-height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.absolute{
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clr-control-label {
|
||||||
|
width: 8rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.names {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 270px;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-between {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-width {
|
||||||
|
width: 310px;
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ExportCveComponent } from './export-cve.component';
|
||||||
|
import { SharedTestingModule } from '../../../../../shared/shared.module';
|
||||||
|
|
||||||
|
describe('ExportCveComponent', () => {
|
||||||
|
let component: ExportCveComponent;
|
||||||
|
let fixture: ComponentFixture<ExportCveComponent>;
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [SharedTestingModule],
|
||||||
|
declarations: [ExportCveComponent],
|
||||||
|
providers: [],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ExportCveComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,211 @@
|
|||||||
|
import { Component, ElementRef, ViewChild } from '@angular/core';
|
||||||
|
import { Label } from 'ng-swagger-gen/models/label';
|
||||||
|
import { LabelService } from 'ng-swagger-gen/services/label.service';
|
||||||
|
import { forkJoin, Observable } from 'rxjs';
|
||||||
|
import { finalize } from 'rxjs/operators';
|
||||||
|
import { Project } from 'src/app/base/project/project';
|
||||||
|
import { NgForm } from '@angular/forms';
|
||||||
|
import { ClrLoadingState } from '@clr/angular';
|
||||||
|
import { InlineAlertComponent } from '../../../../../shared/components/inline-alert/inline-alert.component';
|
||||||
|
import { ScanDataExportService } from '../../../../../../../ng-swagger-gen/services/scan-data-export.service';
|
||||||
|
import { MessageHandlerService } from '../../../../../shared/services/message-handler.service';
|
||||||
|
import {
|
||||||
|
EventService,
|
||||||
|
HarborEvent,
|
||||||
|
} from '../../../../../services/event-service/event.service';
|
||||||
|
|
||||||
|
const PAGE_SIZE: number = 100;
|
||||||
|
const SUPPORTED_MIME_TYPE: string =
|
||||||
|
'application/vnd.security.vulnerability.report; version=1.1';
|
||||||
|
@Component({
|
||||||
|
selector: 'export-cve',
|
||||||
|
templateUrl: './export-cve.component.html',
|
||||||
|
styleUrls: ['./export-cve.component.scss'],
|
||||||
|
})
|
||||||
|
export class ExportCveComponent {
|
||||||
|
selectedProjects: Project[] = [];
|
||||||
|
opened: boolean = false;
|
||||||
|
loading: boolean = false;
|
||||||
|
repos: string;
|
||||||
|
tags: string;
|
||||||
|
CVEIds: string;
|
||||||
|
selectedLabels: Label[] = [];
|
||||||
|
loadingAllLabels: boolean = false;
|
||||||
|
allLabels: Label[] = [];
|
||||||
|
@ViewChild('names', { static: true })
|
||||||
|
namesSpan: ElementRef;
|
||||||
|
@ViewChild('exportCVEForm', { static: true }) currentForm: NgForm;
|
||||||
|
saveBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
@ViewChild(InlineAlertComponent)
|
||||||
|
inlineAlertComponent: InlineAlertComponent;
|
||||||
|
constructor(
|
||||||
|
private labelService: LabelService,
|
||||||
|
private scanDataExportService: ScanDataExportService,
|
||||||
|
private msgHandler: MessageHandlerService,
|
||||||
|
private event: EventService
|
||||||
|
) {}
|
||||||
|
reset() {
|
||||||
|
this.inlineAlertComponent?.close();
|
||||||
|
this.selectedProjects = [];
|
||||||
|
this.repos = null;
|
||||||
|
this.tags = null;
|
||||||
|
this.selectedLabels = [];
|
||||||
|
this.CVEIds = null;
|
||||||
|
this.currentForm?.reset();
|
||||||
|
this.allLabels = [];
|
||||||
|
}
|
||||||
|
open(projects: Project[]) {
|
||||||
|
this.reset();
|
||||||
|
this.opened = true;
|
||||||
|
this.selectedProjects = projects;
|
||||||
|
this.getAllLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.opened = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
this.loading = true;
|
||||||
|
this.saveBtnState = ClrLoadingState.LOADING;
|
||||||
|
const param: ScanDataExportService.ExportScanDataParams = {
|
||||||
|
criteria: {
|
||||||
|
projects: this.selectedProjects.map(item => item.project_id),
|
||||||
|
labels: this.selectedLabels.map(item => item.id),
|
||||||
|
repositories: this.handleBrace(this.repos),
|
||||||
|
tags: this.handleBrace(this.tags),
|
||||||
|
cveIds: this.handleBrace(this.CVEIds),
|
||||||
|
},
|
||||||
|
XScanDataType: SUPPORTED_MIME_TYPE,
|
||||||
|
};
|
||||||
|
this.scanDataExportService
|
||||||
|
.exportScanData(param)
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.loading = false;
|
||||||
|
this.saveBtnState = ClrLoadingState.DEFAULT;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
res => {
|
||||||
|
this.msgHandler.showSuccess(
|
||||||
|
'CVE_EXPORT.TRIGGER_EXPORT_SUCCESS'
|
||||||
|
);
|
||||||
|
this.event.publish(HarborEvent.REFRESH_EXPORT_JOBS);
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
this.inlineAlertComponent.showInlineError(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
inputName() {}
|
||||||
|
isSelected(l: Label): boolean {
|
||||||
|
let flag: boolean = false;
|
||||||
|
this.selectedLabels.forEach(item => {
|
||||||
|
if (item.name === l.name) {
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return flag;
|
||||||
|
}
|
||||||
|
selectOrUnselect(l: Label) {
|
||||||
|
if (this.isSelected(l)) {
|
||||||
|
this.selectedLabels = this.selectedLabels.filter(
|
||||||
|
item => item.name !== l.name
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.selectedLabels.push(l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getProjectNames(): string {
|
||||||
|
if (this.selectedProjects?.length) {
|
||||||
|
const names: string[] = [];
|
||||||
|
this.selectedProjects.forEach(item => {
|
||||||
|
names.push(item.name);
|
||||||
|
});
|
||||||
|
return names.join(', ');
|
||||||
|
}
|
||||||
|
return 'CVE_EXPORT.ALL_PROJECTS';
|
||||||
|
}
|
||||||
|
|
||||||
|
isOverflow(): boolean {
|
||||||
|
return !(
|
||||||
|
this.namesSpan?.nativeElement?.clientWidth >=
|
||||||
|
this.namesSpan?.nativeElement?.scrollWidth
|
||||||
|
);
|
||||||
|
}
|
||||||
|
getAllLabels(): void {
|
||||||
|
// get all global labels
|
||||||
|
this.loadingAllLabels = true;
|
||||||
|
this.labelService
|
||||||
|
.ListLabelsResponse({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
page: 1,
|
||||||
|
scope: 'g',
|
||||||
|
})
|
||||||
|
.pipe(finalize(() => (this.loadingAllLabels = false)))
|
||||||
|
.subscribe(res => {
|
||||||
|
if (res.headers) {
|
||||||
|
const xHeader: string = res.headers.get('X-Total-Count');
|
||||||
|
const totalCount = parseInt(xHeader, 0);
|
||||||
|
let arr = res.body || [];
|
||||||
|
if (totalCount <= 100) {
|
||||||
|
// already gotten all global labels
|
||||||
|
if (arr && arr.length) {
|
||||||
|
arr.forEach(data => {
|
||||||
|
this.allLabels.push(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// get all the global labels in specified times
|
||||||
|
const times: number = Math.ceil(totalCount / PAGE_SIZE);
|
||||||
|
const observableList: Observable<Label[]>[] = [];
|
||||||
|
for (let i = 2; i <= times; i++) {
|
||||||
|
observableList.push(
|
||||||
|
this.labelService.ListLabels({
|
||||||
|
page: i,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
scope: 'g',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.loadingAllLabels = true;
|
||||||
|
forkJoin(observableList)
|
||||||
|
.pipe(
|
||||||
|
finalize(() => (this.loadingAllLabels = false))
|
||||||
|
)
|
||||||
|
.subscribe(response => {
|
||||||
|
if (response && response.length) {
|
||||||
|
response.forEach(item => {
|
||||||
|
arr = arr.concat(item);
|
||||||
|
});
|
||||||
|
arr.forEach(data => {
|
||||||
|
this.allLabels.push(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleBrace(originStr: string): string {
|
||||||
|
if (originStr) {
|
||||||
|
if (
|
||||||
|
originStr.indexOf(',') !== -1 &&
|
||||||
|
originStr.indexOf('{') === -1 &&
|
||||||
|
originStr.indexOf('}') === -1
|
||||||
|
) {
|
||||||
|
return `{${originStr}}`;
|
||||||
|
} else {
|
||||||
|
return originStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -12,16 +12,36 @@
|
|||||||
'PROJECT.NEW_PROJECT' | translate
|
'PROJECT.NEW_PROJECT' | translate
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<clr-dropdown
|
||||||
id="delete-project"
|
[clrCloseMenuOnItemClick]="false"
|
||||||
type="button"
|
class="btn btn-link"
|
||||||
class="btn btn-secondary"
|
clrDropdownTrigger>
|
||||||
[disabled]="!canDelete"
|
<span
|
||||||
(click)="deleteProjects(selectedRow)">
|
>{{ 'MEMBER.ACTION' | translate
|
||||||
<clr-icon shape="times" size="16"></clr-icon> {{
|
}}<clr-icon
|
||||||
'PROJECT.DELETE' | translate
|
shape="caret
|
||||||
}}
|
down"></clr-icon
|
||||||
</button>
|
></span>
|
||||||
|
<clr-dropdown-menu *clrIfOpen>
|
||||||
|
<button clrDropdownItem (click)="exportCVE()">
|
||||||
|
<clr-icon shape="export" size="16"></clr-icon>
|
||||||
|
<span id="export-cve">{{
|
||||||
|
getExportButtonText()
|
||||||
|
| translate: { number: selectedRow?.length }
|
||||||
|
}}</span>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<button
|
||||||
|
id="delete-project"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
[disabled]="!canDelete"
|
||||||
|
(click)="deleteProjects(selectedRow)">
|
||||||
|
<clr-icon shape="times" size="16"></clr-icon>
|
||||||
|
<span>{{ 'PROJECT.DELETE' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
</clr-dropdown-menu>
|
||||||
|
</clr-dropdown>
|
||||||
</clr-dg-action-bar>
|
</clr-dg-action-bar>
|
||||||
<clr-dg-column [clrDgSortBy]="'name'">{{
|
<clr-dg-column [clrDgSortBy]="'name'">{{
|
||||||
'PROJECT.NAME' | translate
|
'PROJECT.NAME' | translate
|
||||||
@ -82,3 +102,4 @@
|
|||||||
</clr-dg-pagination>
|
</clr-dg-pagination>
|
||||||
</clr-dg-footer>
|
</clr-dg-footer>
|
||||||
</clr-datagrid>
|
</clr-datagrid>
|
||||||
|
<export-cve></export-cve>
|
||||||
|
@ -12,7 +12,13 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
import { Subscription, forkJoin, of } from 'rxjs';
|
import { Subscription, forkJoin, of } from 'rxjs';
|
||||||
import { Component, Output, OnDestroy, EventEmitter } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
Output,
|
||||||
|
OnDestroy,
|
||||||
|
EventEmitter,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { ProjectService, State } from '../../../../shared/services';
|
import { ProjectService, State } from '../../../../shared/services';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
@ -47,6 +53,8 @@ import {
|
|||||||
import { ConfirmationDialogService } from '../../../global-confirmation-dialog/confirmation-dialog.service';
|
import { ConfirmationDialogService } from '../../../global-confirmation-dialog/confirmation-dialog.service';
|
||||||
import { errorHandler } from '../../../../shared/units/shared.utils';
|
import { errorHandler } from '../../../../shared/units/shared.utils';
|
||||||
import { ConfirmationMessage } from '../../../global-confirmation-dialog/confirmation-message';
|
import { ConfirmationMessage } from '../../../global-confirmation-dialog/confirmation-message';
|
||||||
|
import { ExportCveComponent } from './export-cve/export-cve.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'list-project',
|
selector: 'list-project',
|
||||||
templateUrl: 'list-project.component.html',
|
templateUrl: 'list-project.component.html',
|
||||||
@ -73,6 +81,8 @@ export class ListProjectComponent implements OnDestroy {
|
|||||||
1: 'PROJECT.PROXY_CACHE',
|
1: 'PROJECT.PROXY_CACHE',
|
||||||
};
|
};
|
||||||
state: ClrDatagridStateInterface;
|
state: ClrDatagridStateInterface;
|
||||||
|
@ViewChild(ExportCveComponent)
|
||||||
|
exportCveComponent: ExportCveComponent;
|
||||||
constructor(
|
constructor(
|
||||||
private session: SessionService,
|
private session: SessionService,
|
||||||
private appConfigService: AppConfigService,
|
private appConfigService: AppConfigService,
|
||||||
@ -201,8 +211,26 @@ export class ListProjectComponent implements OnDestroy {
|
|||||||
this.totalCount = parseInt(xHeader, 0);
|
this.totalCount = parseInt(xHeader, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.projects = response.body as Project[];
|
this.projects = response.body as Project[];
|
||||||
|
// When the reference of the projects in "this.projects" is modified, should also modify the
|
||||||
|
// reference of the projects in "this.selectedRow"
|
||||||
|
this.projects?.forEach(item => {
|
||||||
|
if (this.selectedRow?.length) {
|
||||||
|
for (
|
||||||
|
let i = this.selectedRow?.length - 1;
|
||||||
|
i >= 0;
|
||||||
|
i--
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
this.selectedRow[i].project_id ===
|
||||||
|
item.project_id
|
||||||
|
) {
|
||||||
|
this.selectedRow.splice(i, 1);
|
||||||
|
this.selectedRow.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
this.msgHandler.handleError(error);
|
this.msgHandler.handleError(error);
|
||||||
@ -297,7 +325,7 @@ export class ListProjectComponent implements OnDestroy {
|
|||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
this.filteredType = 0;
|
this.filteredType = 0;
|
||||||
this.searchKeyword = '';
|
this.searchKeyword = '';
|
||||||
|
this.selectedRow = [];
|
||||||
this.reload();
|
this.reload();
|
||||||
this.statisticHandler.refresh();
|
this.statisticHandler.refresh();
|
||||||
}
|
}
|
||||||
@ -351,4 +379,14 @@ export class ListProjectComponent implements OnDestroy {
|
|||||||
|
|
||||||
return st;
|
return st;
|
||||||
}
|
}
|
||||||
|
exportCVE() {
|
||||||
|
this.exportCveComponent.open(this.selectedRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
getExportButtonText(): string {
|
||||||
|
if (this.selectedRow?.length) {
|
||||||
|
return `CVE_EXPORT.EXPORT_SOME_PROJECTS`;
|
||||||
|
}
|
||||||
|
return 'CVE_EXPORT.EXPORT_ALL_PROJECTS';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import { ListProjectComponent } from './list-project/list-project.component';
|
|||||||
import { CreateProjectComponent } from './create-project/create-project.component';
|
import { CreateProjectComponent } from './create-project/create-project.component';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { StatisticsPanelComponent } from './statictics/statistics-panel.component';
|
import { StatisticsPanelComponent } from './statictics/statistics-panel.component';
|
||||||
|
import { ExportCveComponent } from './list-project/export-cve/export-cve.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -33,6 +34,7 @@ const routes: Routes = [
|
|||||||
ListProjectComponent,
|
ListProjectComponent,
|
||||||
CreateProjectComponent,
|
CreateProjectComponent,
|
||||||
StatisticsPanelComponent,
|
StatisticsPanelComponent,
|
||||||
|
ExportCveComponent,
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
})
|
})
|
||||||
|
@ -78,4 +78,5 @@ export enum HarborEvent {
|
|||||||
START_SCAN_ARTIFACT = 'startScanArtifact',
|
START_SCAN_ARTIFACT = 'startScanArtifact',
|
||||||
STOP_SCAN_ARTIFACT = 'stopScanArtifact',
|
STOP_SCAN_ARTIFACT = 'stopScanArtifact',
|
||||||
UPDATE_VULNERABILITY_INFO = 'UpdateVulnerabilityInfo',
|
UPDATE_VULNERABILITY_INFO = 'UpdateVulnerabilityInfo',
|
||||||
|
REFRESH_EXPORT_JOBS = 'refreshExportJobs',
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export class OperateInfo {
|
export class OperateInfo {
|
||||||
name: string;
|
name: string;
|
||||||
state: string;
|
state: string;
|
||||||
data: { [key: string]: string | number };
|
data: { [key: string]: string | number | boolean };
|
||||||
timeStamp: number;
|
timeStamp: number;
|
||||||
timeDiff: string;
|
timeDiff: string;
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
border-left: 1px solid #e0e0e0;
|
border-left: 1px solid #e0e0e0;
|
||||||
}
|
}
|
||||||
|
/* stylelint-disable */
|
||||||
.eventInfo {display: flex; justify-content: flex-start; align-content: flex-start;
|
.eventInfo {display: flex; justify-content: flex-start; align-content: flex-start;
|
||||||
padding: 8px 5px 8px 10px; border-bottom: 1px solid #ccc;}
|
padding: 8px 5px 8px 10px; border-bottom: 1px solid #ccc;}
|
||||||
.iconsArea{ flex-shrink: 1;}
|
.iconsArea{ flex-shrink: 1;}
|
||||||
@ -42,6 +43,7 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.freshIcon{float: right; margin-right: 20px; margin-top: -10px;cursor: pointer;}
|
.freshIcon{float: right; margin-right: 20px; margin-top: -10px;cursor: pointer;}
|
||||||
|
|
||||||
:host::ng-deep#contentAll{
|
:host::ng-deep#contentAll{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 115px;
|
top: 115px;
|
||||||
@ -49,6 +51,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host::ng-deep#contentFailed{
|
:host::ng-deep#contentFailed{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 115px;
|
top: 115px;
|
||||||
@ -56,6 +59,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host::ng-deep#contentRun{
|
:host::ng-deep#contentRun{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 115px;
|
top: 115px;
|
||||||
@ -87,6 +91,12 @@
|
|||||||
.hidden-info {
|
.hidden-info {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.margin-left-5 {
|
.margin-left-5 {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
@ -27,6 +27,63 @@
|
|||||||
{{ 'OPERATION.ALL' | translate }}
|
{{ 'OPERATION.ALL' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<clr-tab-content id="contentAll" *clrIfActive="true">
|
<clr-tab-content id="contentAll" *clrIfActive="true">
|
||||||
|
<div
|
||||||
|
class="eventInfo"
|
||||||
|
*ngFor="let item of exportJobs">
|
||||||
|
<div class="iconsArea">
|
||||||
|
<i
|
||||||
|
class="spinner spinner-inline spinner-pos"
|
||||||
|
[hidden]="
|
||||||
|
item.state !== 'progressing'
|
||||||
|
"></i>
|
||||||
|
<clr-icon
|
||||||
|
[hidden]="item.state !== 'success'"
|
||||||
|
size="18"
|
||||||
|
shape="success-standard"
|
||||||
|
class="color-green"></clr-icon>
|
||||||
|
<clr-icon
|
||||||
|
[hidden]="item.state !== 'failure'"
|
||||||
|
size="18"
|
||||||
|
shape="error-standard"
|
||||||
|
class="color-red"></clr-icon>
|
||||||
|
<clr-icon
|
||||||
|
[hidden]="item.state !== 'interrupt'"
|
||||||
|
size="18"
|
||||||
|
shape="unlink"
|
||||||
|
class="color-orange"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="infoArea">
|
||||||
|
<label
|
||||||
|
class="eventName"
|
||||||
|
(click)="toggleTitle(spanErrorInfo)">
|
||||||
|
<span class="flex">
|
||||||
|
<span class="job-name">{{
|
||||||
|
item.name | translate
|
||||||
|
}}</span>
|
||||||
|
<clr-icon
|
||||||
|
(click)="download(item)"
|
||||||
|
*ngIf="
|
||||||
|
item.state === 'success' &&
|
||||||
|
item?.data?.hasFile
|
||||||
|
"
|
||||||
|
class="btn btn-link"
|
||||||
|
size="16"
|
||||||
|
shape="download"></clr-icon>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<span class="eventTarget">{{
|
||||||
|
item.data.name
|
||||||
|
}}</span
|
||||||
|
><span class="eventTime">{{
|
||||||
|
item.timeDiff | translate
|
||||||
|
}}</span>
|
||||||
|
<span
|
||||||
|
#spanErrorInfo
|
||||||
|
class="eventErrorInf hidden-info"
|
||||||
|
>{{ item.data.errorInf }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="eventInfo"
|
class="eventInfo"
|
||||||
*ngFor="let list of resultLists">
|
*ngFor="let list of resultLists">
|
||||||
@ -78,6 +135,60 @@
|
|||||||
{{ 'OPERATION.RUNNING' | translate }}
|
{{ 'OPERATION.RUNNING' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<clr-tab-content id="contentRun" *clrIfActive>
|
<clr-tab-content id="contentRun" *clrIfActive>
|
||||||
|
<ng-container *ngFor="let item of exportJobs">
|
||||||
|
<div
|
||||||
|
class="eventInfo"
|
||||||
|
*ngIf="item.state === 'progressing'">
|
||||||
|
<div class="iconsArea">
|
||||||
|
<i
|
||||||
|
class="spinner spinner-inline spinner-pos"
|
||||||
|
[hidden]="
|
||||||
|
item.state !== 'progressing'
|
||||||
|
"></i>
|
||||||
|
<clr-icon
|
||||||
|
[hidden]="item.state !== 'success'"
|
||||||
|
size="18"
|
||||||
|
shape="success-standard"
|
||||||
|
class="color-green"></clr-icon>
|
||||||
|
<clr-icon
|
||||||
|
[hidden]="item.state !== 'failure'"
|
||||||
|
size="18"
|
||||||
|
shape="error-standard"
|
||||||
|
class="color-red"></clr-icon>
|
||||||
|
<clr-icon
|
||||||
|
[hidden]="
|
||||||
|
item.state !== 'interrupt'
|
||||||
|
"
|
||||||
|
size="18"
|
||||||
|
shape="unlink"
|
||||||
|
class="color-orange"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="infoArea">
|
||||||
|
<label
|
||||||
|
class="eventName"
|
||||||
|
(click)="
|
||||||
|
toggleTitle(spanErrorInfo)
|
||||||
|
">
|
||||||
|
<span class="flex">
|
||||||
|
<span class="job-name">{{
|
||||||
|
item.name | translate
|
||||||
|
}}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<span class="eventTarget">{{
|
||||||
|
item.data.name
|
||||||
|
}}</span
|
||||||
|
><span class="eventTime">{{
|
||||||
|
item.timeDiff | translate
|
||||||
|
}}</span>
|
||||||
|
<span
|
||||||
|
#spanErrorInfo
|
||||||
|
class="eventErrorInf hidden-info"
|
||||||
|
>{{ item.data.errorInf }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
<div
|
<div
|
||||||
class="eventInfo"
|
class="eventInfo"
|
||||||
*ngFor="let list of runningLists">
|
*ngFor="let list of runningLists">
|
||||||
@ -124,6 +235,56 @@
|
|||||||
{{ 'OPERATION.FAILED' | translate }}
|
{{ 'OPERATION.FAILED' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<clr-tab-content id="contentFailed" *clrIfActive>
|
<clr-tab-content id="contentFailed" *clrIfActive>
|
||||||
|
<div
|
||||||
|
class="eventInfo"
|
||||||
|
*ngFor="let item of exportJobs">
|
||||||
|
<div
|
||||||
|
class="iconsArea"
|
||||||
|
*ngIf="item.state === 'failure'">
|
||||||
|
<i
|
||||||
|
class="spinner spinner-inline spinner-pos"
|
||||||
|
[hidden]="
|
||||||
|
item.state !== 'progressing'
|
||||||
|
"></i>
|
||||||
|
<clr-icon
|
||||||
|
[hidden]="item.state !== 'success'"
|
||||||
|
size="18"
|
||||||
|
shape="success-standard"
|
||||||
|
class="color-green"></clr-icon>
|
||||||
|
<clr-icon
|
||||||
|
[hidden]="item.state !== 'failure'"
|
||||||
|
size="18"
|
||||||
|
shape="error-standard"
|
||||||
|
class="color-red"></clr-icon>
|
||||||
|
<clr-icon
|
||||||
|
[hidden]="item.state !== 'interrupt'"
|
||||||
|
size="18"
|
||||||
|
shape="unlink"
|
||||||
|
class="color-orange"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="infoArea">
|
||||||
|
<label
|
||||||
|
class="eventName"
|
||||||
|
(click)="toggleTitle(spanErrorInfo)">
|
||||||
|
<span class="flex">
|
||||||
|
<span class="job-name">{{
|
||||||
|
item.name | translate
|
||||||
|
}}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<span class="eventTarget">{{
|
||||||
|
item.data.name
|
||||||
|
}}</span
|
||||||
|
><span class="eventTime">{{
|
||||||
|
item.timeDiff | translate
|
||||||
|
}}</span>
|
||||||
|
<span
|
||||||
|
#spanErrorInfo
|
||||||
|
class="eventErrorInf hidden-info"
|
||||||
|
>{{ item.data.errorInf }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="eventInfo"
|
class="eventInfo"
|
||||||
*ngFor="let list of failLists">
|
*ngFor="let list of failLists">
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
|
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
|
||||||
import { OperationService } from './operation.service';
|
import {
|
||||||
|
downloadCVEs,
|
||||||
|
EventState,
|
||||||
|
ExportJobStatus,
|
||||||
|
OperationService,
|
||||||
|
} from './operation.service';
|
||||||
import { forkJoin, Subscription } from 'rxjs';
|
import { forkJoin, Subscription } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
OperateInfo,
|
OperateInfo,
|
||||||
@ -9,11 +14,19 @@ import {
|
|||||||
import { SlideInOutAnimation } from '../../_animations/slide-in-out.animation';
|
import { SlideInOutAnimation } from '../../_animations/slide-in-out.animation';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { SessionService } from '../../services/session.service';
|
import { SessionService } from '../../services/session.service';
|
||||||
|
import { ScanDataExportService } from '../../../../../ng-swagger-gen/services/scan-data-export.service';
|
||||||
|
import {
|
||||||
|
EventService,
|
||||||
|
HarborEvent,
|
||||||
|
} from '../../../services/event-service/event.service';
|
||||||
|
import { MessageHandlerService } from '../../services/message-handler.service';
|
||||||
|
import { HarborDatetimePipe } from '../../pipes/harbor-datetime.pipe';
|
||||||
|
const STAY_TIME: number = 5000;
|
||||||
const OPERATION_KEY: string = 'operation';
|
const OPERATION_KEY: string = 'operation';
|
||||||
const MAX_NUMBER: number = 500;
|
const MAX_NUMBER: number = 500;
|
||||||
const MAX_SAVING_TIME: number = 1000 * 60 * 60 * 24 * 30; // 30 days
|
const MAX_SAVING_TIME: number = 1000 * 60 * 60 * 24 * 30; // 30 days
|
||||||
|
const TIMEOUT = 7000;
|
||||||
|
const FILE_NAME_PREFIX: string = 'csv_file_';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'hbr-operation-model',
|
selector: 'hbr-operation-model',
|
||||||
templateUrl: './operation.component.html',
|
templateUrl: './operation.component.html',
|
||||||
@ -21,8 +34,10 @@ const MAX_SAVING_TIME: number = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|||||||
animations: [SlideInOutAnimation],
|
animations: [SlideInOutAnimation],
|
||||||
})
|
})
|
||||||
export class OperationComponent implements OnInit, OnDestroy {
|
export class OperationComponent implements OnInit, OnDestroy {
|
||||||
|
fileNamePrefix: string = FILE_NAME_PREFIX;
|
||||||
batchInfoSubscription: Subscription;
|
batchInfoSubscription: Subscription;
|
||||||
resultLists: OperateInfo[] = [];
|
resultLists: OperateInfo[] = [];
|
||||||
|
exportJobs: OperateInfo[] = [];
|
||||||
animationState = 'out';
|
animationState = 'out';
|
||||||
private _newMessageCount: number = 0;
|
private _newMessageCount: number = 0;
|
||||||
private _timeoutInterval;
|
private _timeoutInterval;
|
||||||
@ -42,12 +57,22 @@ export class OperationComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
timeout;
|
||||||
constructor(
|
constructor(
|
||||||
private session: SessionService,
|
private session: SessionService,
|
||||||
private operationService: OperationService,
|
private operationService: OperationService,
|
||||||
private translate: TranslateService
|
private translate: TranslateService,
|
||||||
|
private scanDataExportService: ScanDataExportService,
|
||||||
|
private event: EventService,
|
||||||
|
private msgHandler: MessageHandlerService
|
||||||
) {
|
) {
|
||||||
|
this.event.subscribe(HarborEvent.REFRESH_EXPORT_JOBS, () => {
|
||||||
|
if (this.animationState === 'out') {
|
||||||
|
this._newMessageCount += 1;
|
||||||
|
}
|
||||||
|
this.refreshExportJobs();
|
||||||
|
});
|
||||||
|
|
||||||
this.batchInfoSubscription = operationService.operationInfo$.subscribe(
|
this.batchInfoSubscription = operationService.operationInfo$.subscribe(
|
||||||
data => {
|
data => {
|
||||||
if (this.animationState === 'out') {
|
if (this.animationState === 'out') {
|
||||||
@ -91,7 +116,7 @@ export class OperationComponent implements OnInit, OnDestroy {
|
|||||||
if (!this._timeoutInterval) {
|
if (!this._timeoutInterval) {
|
||||||
this._timeoutInterval = setTimeout(() => {
|
this._timeoutInterval = setTimeout(() => {
|
||||||
this.animationState = 'out';
|
this.animationState = 'out';
|
||||||
}, 5000);
|
}, STAY_TIME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,6 +142,7 @@ export class OperationComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
if (this.session.getCurrentUser()) {
|
if (this.session.getCurrentUser()) {
|
||||||
|
this.refreshExportJobs();
|
||||||
const operationInfosString: string = localStorage.getItem(
|
const operationInfosString: string = localStorage.getItem(
|
||||||
`${OPERATION_KEY}-${this.session.getCurrentUser().user_id}`
|
`${OPERATION_KEY}-${this.session.getCurrentUser().user_id}`
|
||||||
);
|
);
|
||||||
@ -163,6 +189,10 @@ export class OperationComponent implements OnInit, OnDestroy {
|
|||||||
clearInterval(this._timeoutInterval);
|
clearInterval(this._timeoutInterval);
|
||||||
this._timeoutInterval = null;
|
this._timeoutInterval = null;
|
||||||
}
|
}
|
||||||
|
if (this.timeout) {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
this.timeout = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTitle(errorSpan: any) {
|
toggleTitle(errorSpan: any) {
|
||||||
@ -207,6 +237,16 @@ export class OperationComponent implements OnInit, OnDestroy {
|
|||||||
daysAgo
|
daysAgo
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
this.exportJobs.forEach(data => {
|
||||||
|
const timeDiff: number = new Date().getTime() - +data.timeStamp;
|
||||||
|
data.timeDiff = this.calculateTime(
|
||||||
|
timeDiff,
|
||||||
|
secondsAgo,
|
||||||
|
minutesAgo,
|
||||||
|
hoursAgo,
|
||||||
|
daysAgo
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateTime(
|
calculateTime(
|
||||||
@ -227,4 +267,92 @@ export class OperationComponent implements OnInit, OnDestroy {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
refreshExportJobs() {
|
||||||
|
if (this.session.getCurrentUser()) {
|
||||||
|
this.scanDataExportService
|
||||||
|
.getScanDataExportExecutionList({
|
||||||
|
userName: this.session?.getCurrentUser()?.username,
|
||||||
|
})
|
||||||
|
.subscribe(res => {
|
||||||
|
if (res?.items) {
|
||||||
|
this.exportJobs = [];
|
||||||
|
let flag: boolean = false;
|
||||||
|
res.items.forEach(item => {
|
||||||
|
const info: OperateInfo = {
|
||||||
|
name: 'CVE_EXPORT.EXPORT_TITLE',
|
||||||
|
state: this.MapStatus(item.status),
|
||||||
|
data: {
|
||||||
|
hasFile: item.file_present,
|
||||||
|
name: `${FILE_NAME_PREFIX}${new HarborDatetimePipe().transform(
|
||||||
|
item.start_time,
|
||||||
|
'yyyyMMddHHss'
|
||||||
|
)}`,
|
||||||
|
id: item.id,
|
||||||
|
errorInf:
|
||||||
|
item.status === ExportJobStatus.ERROR
|
||||||
|
? item.status_text
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
timeStamp: new Date(item.start_time).getTime(),
|
||||||
|
timeDiff: 'OPERATION.SECOND_AGO',
|
||||||
|
};
|
||||||
|
this.exportJobs.push(info);
|
||||||
|
if (this.isRunningState(item.status)) {
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (flag) {
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
this.refreshExportJobs();
|
||||||
|
}, TIMEOUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunningState(state: string): boolean {
|
||||||
|
if (state) {
|
||||||
|
return (
|
||||||
|
state === ExportJobStatus.RUNNING ||
|
||||||
|
state === ExportJobStatus.PENDING ||
|
||||||
|
state === ExportJobStatus.SCHEDULED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
MapStatus(originStatus: string): string {
|
||||||
|
if (originStatus) {
|
||||||
|
if (this.isRunningState(originStatus)) {
|
||||||
|
return EventState.PROGRESSING;
|
||||||
|
}
|
||||||
|
if (originStatus === ExportJobStatus.STOPPED) {
|
||||||
|
return EventState.INTERRUPT;
|
||||||
|
}
|
||||||
|
if (originStatus === ExportJobStatus.SUCCESS) {
|
||||||
|
return EventState.SUCCESS;
|
||||||
|
}
|
||||||
|
if (originStatus === ExportJobStatus.ERROR) {
|
||||||
|
return EventState.FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return EventState.FAILURE;
|
||||||
|
}
|
||||||
|
download(info: OperateInfo) {
|
||||||
|
if (info?.data?.id && info?.data?.name) {
|
||||||
|
this.scanDataExportService
|
||||||
|
.downloadScanData({
|
||||||
|
executionId: +info.data.id,
|
||||||
|
})
|
||||||
|
.subscribe(
|
||||||
|
res => {
|
||||||
|
downloadCVEs(res, info.data.name);
|
||||||
|
this.refreshExportJobs();
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
this.msgHandler.error(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { OperateInfo } from './operate';
|
import { OperateInfo } from './operate';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -15,3 +15,30 @@ export class OperationService {
|
|||||||
this.operationInfoSource.next(data);
|
this.operationInfoSource.next(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function downloadCVEs(data, filename) {
|
||||||
|
let url = window.URL.createObjectURL(data);
|
||||||
|
let a = document.createElement('a');
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.setAttribute('style', 'display: none');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
a.remove();
|
||||||
|
}
|
||||||
|
export enum EventState {
|
||||||
|
SUCCESS = 'success',
|
||||||
|
FAILURE = 'failure',
|
||||||
|
INTERRUPT = 'interrupt',
|
||||||
|
PROGRESSING = 'progressing',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ExportJobStatus {
|
||||||
|
PENDING = 'Pending',
|
||||||
|
RUNNING = 'Running',
|
||||||
|
STOPPED = 'Stopped',
|
||||||
|
ERROR = 'Error',
|
||||||
|
SUCCESS = 'Success',
|
||||||
|
SCHEDULED = 'Scheduled',
|
||||||
|
}
|
||||||
|
@ -1753,5 +1753,19 @@
|
|||||||
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured",
|
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured",
|
||||||
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",
|
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",
|
||||||
"STOP_PURGE_SUCCESS": "Trigger stopping purging operation successfully"
|
"STOP_PURGE_SUCCESS": "Trigger stopping purging operation successfully"
|
||||||
|
},
|
||||||
|
"CVE_EXPORT": {
|
||||||
|
"EXPORT_SOME_PROJECTS": "Export CVEs - {{number}} project(s)",
|
||||||
|
"EXPORT_ALL_PROJECTS": "Export CVEs - All projects",
|
||||||
|
"ALL_PROJECTS": "All projects",
|
||||||
|
"EXPORT_TITLE": "Export CVE",
|
||||||
|
"EXPORT_SUBTITLE": "Set exporting conditions",
|
||||||
|
"EXPORT_CVE_FILTER_HELP_TEXT": "Enter multiple comma separated cveIds",
|
||||||
|
"CVE_IDS": "CVE IDs",
|
||||||
|
"EXPORT_BUTTON": "EXPORT",
|
||||||
|
"JOB_NAME": "Job Name",
|
||||||
|
"JOB_NAME_REQUIRED": "Job name is required",
|
||||||
|
"JOB_NAME_EXISTING": "Job name already exists",
|
||||||
|
"TRIGGER_EXPORT_SUCCESS": "Trigger exporting CVEs successfully!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1753,5 +1753,19 @@
|
|||||||
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured",
|
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured",
|
||||||
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",
|
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",
|
||||||
"STOP_PURGE_SUCCESS": "Trigger stopping purging operation successfully"
|
"STOP_PURGE_SUCCESS": "Trigger stopping purging operation successfully"
|
||||||
|
},
|
||||||
|
"CVE_EXPORT": {
|
||||||
|
"EXPORT_SOME_PROJECTS": "Export CVEs - {{number}} project(s)",
|
||||||
|
"EXPORT_ALL_PROJECTS": "Export CVEs - All projects",
|
||||||
|
"ALL_PROJECTS": "All projects",
|
||||||
|
"EXPORT_TITLE": "Export CVE",
|
||||||
|
"EXPORT_SUBTITLE": "Set exporting conditions",
|
||||||
|
"EXPORT_CVE_FILTER_HELP_TEXT": "Enter multiple comma separated cveIds",
|
||||||
|
"CVE_IDS": "CVE IDs",
|
||||||
|
"EXPORT_BUTTON": "EXPORT",
|
||||||
|
"JOB_NAME": "Job Name",
|
||||||
|
"JOB_NAME_REQUIRED": "Job name is required",
|
||||||
|
"JOB_NAME_EXISTING": "Job name already exists",
|
||||||
|
"TRIGGER_EXPORT_SUCCESS": "Trigger exporting CVEs successfully!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1752,5 +1752,19 @@
|
|||||||
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured",
|
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured",
|
||||||
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",
|
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",
|
||||||
"STOP_PURGE_SUCCESS": "Trigger stopping purging operation successfully"
|
"STOP_PURGE_SUCCESS": "Trigger stopping purging operation successfully"
|
||||||
|
},
|
||||||
|
"CVE_EXPORT": {
|
||||||
|
"EXPORT_SOME_PROJECTS": "Export CVEs - {{number}} project(s)",
|
||||||
|
"EXPORT_ALL_PROJECTS": "Export CVEs - All projects",
|
||||||
|
"ALL_PROJECTS": "All projects",
|
||||||
|
"EXPORT_TITLE": "Export CVE",
|
||||||
|
"EXPORT_SUBTITLE": "Set exporting conditions",
|
||||||
|
"EXPORT_CVE_FILTER_HELP_TEXT": "Enter multiple comma separated cveIds",
|
||||||
|
"CVE_IDS": "CVE IDs",
|
||||||
|
"EXPORT_BUTTON": "EXPORT",
|
||||||
|
"JOB_NAME": "Job Name",
|
||||||
|
"JOB_NAME_REQUIRED": "Job name is required",
|
||||||
|
"JOB_NAME_EXISTING": "Job name already exists",
|
||||||
|
"TRIGGER_EXPORT_SUCCESS": "Trigger exporting CVEs successfully!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1722,5 +1722,19 @@
|
|||||||
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured",
|
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured",
|
||||||
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",
|
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",
|
||||||
"STOP_PURGE_SUCCESS": "Trigger stopping purging operation successfully"
|
"STOP_PURGE_SUCCESS": "Trigger stopping purging operation successfully"
|
||||||
|
},
|
||||||
|
"CVE_EXPORT": {
|
||||||
|
"EXPORT_SOME_PROJECTS": "Export CVEs - {{number}} project(s)",
|
||||||
|
"EXPORT_ALL_PROJECTS": "Export CVEs - All projects",
|
||||||
|
"ALL_PROJECTS": "All projects",
|
||||||
|
"EXPORT_TITLE": "Export CVE",
|
||||||
|
"EXPORT_SUBTITLE": "Set exporting conditions",
|
||||||
|
"EXPORT_CVE_FILTER_HELP_TEXT": "Enter multiple comma separated cveIds",
|
||||||
|
"CVE_IDS": "CVE IDs",
|
||||||
|
"EXPORT_BUTTON": "EXPORT",
|
||||||
|
"JOB_NAME": "Job Name",
|
||||||
|
"JOB_NAME_REQUIRED": "Job name is required",
|
||||||
|
"JOB_NAME_EXISTING": "Job name already exists",
|
||||||
|
"TRIGGER_EXPORT_SUCCESS": "Trigger exporting CVEs successfully!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1749,5 +1749,19 @@
|
|||||||
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured",
|
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured",
|
||||||
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",
|
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",
|
||||||
"STOP_PURGE_SUCCESS": "Trigger stopping purging operation successfully"
|
"STOP_PURGE_SUCCESS": "Trigger stopping purging operation successfully"
|
||||||
|
},
|
||||||
|
"CVE_EXPORT": {
|
||||||
|
"EXPORT_SOME_PROJECTS": "Export CVEs - {{number}} project(s)",
|
||||||
|
"EXPORT_ALL_PROJECTS": "Export CVEs - All projects",
|
||||||
|
"ALL_PROJECTS": "All projects",
|
||||||
|
"EXPORT_TITLE": "Export CVE",
|
||||||
|
"EXPORT_SUBTITLE": "Set exporting conditions",
|
||||||
|
"EXPORT_CVE_FILTER_HELP_TEXT": "Enter multiple comma separated cveIds",
|
||||||
|
"CVE_IDS": "CVE IDs",
|
||||||
|
"EXPORT_BUTTON": "EXPORT",
|
||||||
|
"JOB_NAME": "Job Name",
|
||||||
|
"JOB_NAME_REQUIRED": "Job name is required",
|
||||||
|
"JOB_NAME_EXISTING": "Job name already exists",
|
||||||
|
"TRIGGER_EXPORT_SUCCESS": "Trigger exporting CVEs successfully!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1753,5 +1753,19 @@
|
|||||||
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured",
|
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured",
|
||||||
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",
|
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",
|
||||||
"STOP_PURGE_SUCCESS": "Trigger stopping purging operation successfully"
|
"STOP_PURGE_SUCCESS": "Trigger stopping purging operation successfully"
|
||||||
|
},
|
||||||
|
"CVE_EXPORT": {
|
||||||
|
"EXPORT_SOME_PROJECTS": "Export CVEs - {{number}} project(s)",
|
||||||
|
"EXPORT_ALL_PROJECTS": "Export CVEs - All projects",
|
||||||
|
"ALL_PROJECTS": "All projects",
|
||||||
|
"EXPORT_TITLE": "Export CVE",
|
||||||
|
"EXPORT_SUBTITLE": "Set exporting conditions",
|
||||||
|
"EXPORT_CVE_FILTER_HELP_TEXT": "Enter multiple comma separated cveIds",
|
||||||
|
"CVE_IDS": "CVE IDs",
|
||||||
|
"EXPORT_BUTTON": "EXPORT",
|
||||||
|
"JOB_NAME": "Job Name",
|
||||||
|
"JOB_NAME_REQUIRED": "Job name is required",
|
||||||
|
"JOB_NAME_EXISTING": "Job name already exists",
|
||||||
|
"TRIGGER_EXPORT_SUCCESS": "Trigger exporting CVEs successfully!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1751,5 +1751,19 @@
|
|||||||
"SKIP_DATABASE_TOOLTIP": "开启此项将不会在数据库中记录日志,需先配置日志转发端点",
|
"SKIP_DATABASE_TOOLTIP": "开启此项将不会在数据库中记录日志,需先配置日志转发端点",
|
||||||
"STOP_GC_SUCCESS": "成功触发停止垃圾回收的操作",
|
"STOP_GC_SUCCESS": "成功触发停止垃圾回收的操作",
|
||||||
"STOP_PURGE_SUCCESS": "成功触发停止清理日志的操作"
|
"STOP_PURGE_SUCCESS": "成功触发停止清理日志的操作"
|
||||||
|
},
|
||||||
|
"CVE_EXPORT": {
|
||||||
|
"EXPORT_SOME_PROJECTS": "导出 CVEs - {{number}} 个项目",
|
||||||
|
"EXPORT_ALL_PROJECTS": "导出 CVEs - 全部项目",
|
||||||
|
"ALL_PROJECTS": "全部项目",
|
||||||
|
"EXPORT_TITLE": "导出 CVE",
|
||||||
|
"EXPORT_SUBTITLE": "设置导出条件",
|
||||||
|
"EXPORT_CVE_FILTER_HELP_TEXT": "使用逗号分割 cveIds",
|
||||||
|
"CVE_IDS": "CVE IDs",
|
||||||
|
"EXPORT_BUTTON": "导出",
|
||||||
|
"JOB_NAME": "任务名称",
|
||||||
|
"JOB_NAME_REQUIRED": "任务名称为必填项",
|
||||||
|
"JOB_NAME_EXISTING": "任务名称已存在",
|
||||||
|
"TRIGGER_EXPORT_SUCCESS": "触发导出 CVEs 任务成功!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1744,5 +1744,19 @@
|
|||||||
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured",
|
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured",
|
||||||
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",
|
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",
|
||||||
"STOP_PURGE_SUCCESS": "Trigger stopping purging operation successfully"
|
"STOP_PURGE_SUCCESS": "Trigger stopping purging operation successfully"
|
||||||
|
},
|
||||||
|
"CVE_EXPORT": {
|
||||||
|
"EXPORT_SOME_PROJECTS": "Export CVEs - {{number}} project(s)",
|
||||||
|
"EXPORT_ALL_PROJECTS": "Export CVEs - All projects",
|
||||||
|
"ALL_PROJECTS": "All projects",
|
||||||
|
"EXPORT_TITLE": "Export CVE",
|
||||||
|
"EXPORT_SUBTITLE": "Set exporting conditions",
|
||||||
|
"EXPORT_CVE_FILTER_HELP_TEXT": "Enter multiple comma separated cveIds",
|
||||||
|
"CVE_IDS": "CVE IDs",
|
||||||
|
"EXPORT_BUTTON": "EXPORT",
|
||||||
|
"JOB_NAME": "Job Name",
|
||||||
|
"JOB_NAME_REQUIRED": "Job name is required",
|
||||||
|
"JOB_NAME_EXISTING": "Job name already exists",
|
||||||
|
"TRIGGER_EXPORT_SUCCESS": "Trigger exporting CVEs successfully!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user