Add CVE data exporting UI (#16236)

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
Shijun Sun 2022-07-11 17:43:25 +08:00 committed by GitHub
parent 130452111b
commit aa3cdcbc6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1072 additions and 21 deletions

View File

@ -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>

View File

@ -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;
}

View File

@ -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();
});
});

View File

@ -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;
}
}

View File

@ -12,16 +12,36 @@
'PROJECT.NEW_PROJECT' | translate
}}
</button>
<button
id="delete-project"
type="button"
class="btn btn-secondary"
[disabled]="!canDelete"
(click)="deleteProjects(selectedRow)">
<clr-icon shape="times" size="16"></clr-icon>&nbsp;{{
'PROJECT.DELETE' | translate
}}
</button>
<clr-dropdown
[clrCloseMenuOnItemClick]="false"
class="btn btn-link"
clrDropdownTrigger>
<span
>{{ 'MEMBER.ACTION' | translate
}}<clr-icon
shape="caret
down"></clr-icon
></span>
<clr-dropdown-menu *clrIfOpen>
<button clrDropdownItem (click)="exportCVE()">
<clr-icon shape="export" size="16"></clr-icon>&nbsp;
<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>&nbsp;
<span>{{ 'PROJECT.DELETE' | translate }}</span>
</button>
</clr-dropdown-menu>
</clr-dropdown>
</clr-dg-action-bar>
<clr-dg-column [clrDgSortBy]="'name'">{{
'PROJECT.NAME' | translate
@ -82,3 +102,4 @@
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
<export-cve></export-cve>

View File

@ -12,7 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
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 { ProjectService, State } from '../../../../shared/services';
import { TranslateService } from '@ngx-translate/core';
@ -47,6 +53,8 @@ import {
import { ConfirmationDialogService } from '../../../global-confirmation-dialog/confirmation-dialog.service';
import { errorHandler } from '../../../../shared/units/shared.utils';
import { ConfirmationMessage } from '../../../global-confirmation-dialog/confirmation-message';
import { ExportCveComponent } from './export-cve/export-cve.component';
@Component({
selector: 'list-project',
templateUrl: 'list-project.component.html',
@ -73,6 +81,8 @@ export class ListProjectComponent implements OnDestroy {
1: 'PROJECT.PROXY_CACHE',
};
state: ClrDatagridStateInterface;
@ViewChild(ExportCveComponent)
exportCveComponent: ExportCveComponent;
constructor(
private session: SessionService,
private appConfigService: AppConfigService,
@ -201,8 +211,26 @@ export class ListProjectComponent implements OnDestroy {
this.totalCount = parseInt(xHeader, 0);
}
}
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 => {
this.msgHandler.handleError(error);
@ -297,7 +325,7 @@ export class ListProjectComponent implements OnDestroy {
this.currentPage = 1;
this.filteredType = 0;
this.searchKeyword = '';
this.selectedRow = [];
this.reload();
this.statisticHandler.refresh();
}
@ -351,4 +379,14 @@ export class ListProjectComponent implements OnDestroy {
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';
}
}

View File

@ -18,6 +18,7 @@ import { ListProjectComponent } from './list-project/list-project.component';
import { CreateProjectComponent } from './create-project/create-project.component';
import { RouterModule, Routes } from '@angular/router';
import { StatisticsPanelComponent } from './statictics/statistics-panel.component';
import { ExportCveComponent } from './list-project/export-cve/export-cve.component';
const routes: Routes = [
{
@ -33,6 +34,7 @@ const routes: Routes = [
ListProjectComponent,
CreateProjectComponent,
StatisticsPanelComponent,
ExportCveComponent,
],
providers: [],
})

View File

@ -78,4 +78,5 @@ export enum HarborEvent {
START_SCAN_ARTIFACT = 'startScanArtifact',
STOP_SCAN_ARTIFACT = 'stopScanArtifact',
UPDATE_VULNERABILITY_INFO = 'UpdateVulnerabilityInfo',
REFRESH_EXPORT_JOBS = 'refreshExportJobs',
}

View File

@ -1,7 +1,7 @@
export class OperateInfo {
name: string;
state: string;
data: { [key: string]: string | number };
data: { [key: string]: string | number | boolean };
timeStamp: number;
timeDiff: string;
constructor() {

View File

@ -7,6 +7,7 @@
padding-top: 20px;
border-left: 1px solid #e0e0e0;
}
/* stylelint-disable */
.eventInfo {display: flex; justify-content: flex-start; align-content: flex-start;
padding: 8px 5px 8px 10px; border-bottom: 1px solid #ccc;}
.iconsArea{ flex-shrink: 1;}
@ -42,6 +43,7 @@
text-decoration: none;
}
.freshIcon{float: right; margin-right: 20px; margin-top: -10px;cursor: pointer;}
:host::ng-deep#contentAll{
position: absolute;
top: 115px;
@ -49,6 +51,7 @@
width: 100%;
overflow-y: auto;
}
:host::ng-deep#contentFailed{
position: absolute;
top: 115px;
@ -56,6 +59,7 @@
width: 100%;
overflow-y: auto;
}
:host::ng-deep#contentRun{
position: absolute;
top: 115px;
@ -87,6 +91,12 @@
.hidden-info {
display: none;
}
.margin-left-5 {
margin-left: 5px;
}
.flex {
display: flex;
align-items: center;
}

View File

@ -27,6 +27,63 @@
{{ 'OPERATION.ALL' | translate }}
</button>
<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
class="eventInfo"
*ngFor="let list of resultLists">
@ -78,6 +135,60 @@
{{ 'OPERATION.RUNNING' | translate }}
</button>
<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
class="eventInfo"
*ngFor="let list of runningLists">
@ -124,6 +235,56 @@
{{ 'OPERATION.FAILED' | translate }}
</button>
<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
class="eventInfo"
*ngFor="let list of failLists">

View File

@ -1,5 +1,10 @@
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 {
OperateInfo,
@ -9,11 +14,19 @@ import {
import { SlideInOutAnimation } from '../../_animations/slide-in-out.animation';
import { TranslateService } from '@ngx-translate/core';
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 MAX_NUMBER: number = 500;
const MAX_SAVING_TIME: number = 1000 * 60 * 60 * 24 * 30; // 30 days
const TIMEOUT = 7000;
const FILE_NAME_PREFIX: string = 'csv_file_';
@Component({
selector: 'hbr-operation-model',
templateUrl: './operation.component.html',
@ -21,8 +34,10 @@ const MAX_SAVING_TIME: number = 1000 * 60 * 60 * 24 * 30; // 30 days
animations: [SlideInOutAnimation],
})
export class OperationComponent implements OnInit, OnDestroy {
fileNamePrefix: string = FILE_NAME_PREFIX;
batchInfoSubscription: Subscription;
resultLists: OperateInfo[] = [];
exportJobs: OperateInfo[] = [];
animationState = 'out';
private _newMessageCount: number = 0;
private _timeoutInterval;
@ -42,12 +57,22 @@ export class OperationComponent implements OnInit, OnDestroy {
);
}
}
timeout;
constructor(
private session: SessionService,
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(
data => {
if (this.animationState === 'out') {
@ -91,7 +116,7 @@ export class OperationComponent implements OnInit, OnDestroy {
if (!this._timeoutInterval) {
this._timeoutInterval = setTimeout(() => {
this.animationState = 'out';
}, 5000);
}, STAY_TIME);
}
}
@ -117,6 +142,7 @@ export class OperationComponent implements OnInit, OnDestroy {
init() {
if (this.session.getCurrentUser()) {
this.refreshExportJobs();
const operationInfosString: string = localStorage.getItem(
`${OPERATION_KEY}-${this.session.getCurrentUser().user_id}`
);
@ -163,6 +189,10 @@ export class OperationComponent implements OnInit, OnDestroy {
clearInterval(this._timeoutInterval);
this._timeoutInterval = null;
}
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
}
toggleTitle(errorSpan: any) {
@ -207,6 +237,16 @@ export class OperationComponent implements OnInit, OnDestroy {
daysAgo
);
});
this.exportJobs.forEach(data => {
const timeDiff: number = new Date().getTime() - +data.timeStamp;
data.timeDiff = this.calculateTime(
timeDiff,
secondsAgo,
minutesAgo,
hoursAgo,
daysAgo
);
});
}
calculateTime(
@ -227,4 +267,92 @@ export class OperationComponent implements OnInit, OnDestroy {
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);
}
);
}
}
}

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { Subject } from 'rxjs';
import { OperateInfo } from './operate';
@Injectable({
@ -15,3 +15,30 @@ export class OperationService {
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',
}

View File

@ -1753,5 +1753,19 @@
"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_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!"
}
}

View File

@ -1753,5 +1753,19 @@
"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_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!"
}
}

View File

@ -1752,5 +1752,19 @@
"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_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!"
}
}

View File

@ -1722,5 +1722,19 @@
"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_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!"
}
}

View File

@ -1749,5 +1749,19 @@
"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_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!"
}
}

View File

@ -1753,5 +1753,19 @@
"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_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!"
}
}

View File

@ -1751,5 +1751,19 @@
"SKIP_DATABASE_TOOLTIP": "开启此项将不会在数据库中记录日志,需先配置日志转发端点",
"STOP_GC_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 任务成功!"
}
}

View File

@ -1744,5 +1744,19 @@
"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_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!"
}
}