mirror of
https://github.com/goharbor/harbor.git
synced 2025-01-14 11:41:31 +01: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
|
||||
}}
|
||||
</button>
|
||||
<button
|
||||
id="delete-project"
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
[disabled]="!canDelete"
|
||||
(click)="deleteProjects(selectedRow)">
|
||||
<clr-icon shape="times" size="16"></clr-icon> {{
|
||||
'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>
|
||||
<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-column [clrDgSortBy]="'name'">{{
|
||||
'PROJECT.NAME' | translate
|
||||
@ -82,3 +102,4 @@
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
<export-cve></export-cve>
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
@ -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: [],
|
||||
})
|
||||
|
@ -78,4 +78,5 @@ export enum HarborEvent {
|
||||
START_SCAN_ARTIFACT = 'startScanArtifact',
|
||||
STOP_SCAN_ARTIFACT = 'stopScanArtifact',
|
||||
UPDATE_VULNERABILITY_INFO = 'UpdateVulnerabilityInfo',
|
||||
REFRESH_EXPORT_JOBS = 'refreshExportJobs',
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
}
|
||||
|
@ -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!"
|
||||
}
|
||||
}
|
||||
|
@ -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!"
|
||||
}
|
||||
}
|
||||
|
@ -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!"
|
||||
}
|
||||
}
|
||||
|
@ -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!"
|
||||
}
|
||||
}
|
||||
|
@ -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!"
|
||||
}
|
||||
}
|
||||
|
@ -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!"
|
||||
}
|
||||
}
|
||||
|
@ -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 任务成功!"
|
||||
}
|
||||
}
|
||||
|
@ -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!"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user