mirror of
https://github.com/goharbor/harbor.git
synced 2025-02-28 01:32:15 +01:00
Add clearances ui (#16941)
Add audit log purge and forwarding ui Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
parent
7ecd4a3f29
commit
bf317d0b26
@ -116,12 +116,12 @@ const routes: Routes = [
|
||||
).then(m => m.ProjectQuotasModule),
|
||||
},
|
||||
{
|
||||
path: 'gc',
|
||||
path: 'clearing-job',
|
||||
canActivate: [SystemAdminGuard],
|
||||
loadChildren: () =>
|
||||
import('./left-side-nav/gc-page/gc.module').then(
|
||||
m => m.GcModule
|
||||
),
|
||||
import(
|
||||
'./left-side-nav/clearing-job/clearing-job.module'
|
||||
).then(m => m.ClearingJobModule),
|
||||
},
|
||||
{
|
||||
path: 'configs',
|
||||
|
@ -160,15 +160,12 @@
|
||||
<a
|
||||
clrVerticalNavLink
|
||||
*ngIf="hasAdminRole"
|
||||
routerLink="/harbor/gc"
|
||||
routerLink="/harbor/clearing-job"
|
||||
routerLinkActive="active">
|
||||
<clr-icon
|
||||
shape="trash"
|
||||
clrVerticalNavIcon></clr-icon>
|
||||
{{
|
||||
'SIDE_NAV.SYSTEM_MGMT.GARBAGE_COLLECTION'
|
||||
| translate
|
||||
}}
|
||||
{{ 'CLEARANCES.CLEARANCES' | translate }}
|
||||
</a>
|
||||
<a
|
||||
clrVerticalNavLink
|
||||
|
@ -0,0 +1,64 @@
|
||||
<h5 class="history-header font-style mt-3" id="history-header">
|
||||
{{ 'CLEARANCES.PURGE_HISTORY' | translate }}
|
||||
</h5>
|
||||
<span class="refresh-btn" (click)="refresh()">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</span>
|
||||
<clr-datagrid [clrDgLoading]="loading" (clrDgRefresh)="getJobs(true, $event)">
|
||||
<clr-dg-column [clrDgField]="'id'">{{
|
||||
'GC.JOB_ID' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column>{{ 'GC.TRIGGER_TYPE' | translate }}</clr-dg-column>
|
||||
<clr-dg-column>{{ 'TAG_RETENTION.DRY_RUN' | translate }}</clr-dg-column>
|
||||
<clr-dg-column>{{ 'STATUS' | translate }}</clr-dg-column>
|
||||
<clr-dg-column>{{ 'CREATION_TIME' | translate }}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="'update_time'">{{
|
||||
'UPDATE_TIME' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column>{{ 'LOGS' | translate }}</clr-dg-column>
|
||||
<clr-dg-row *ngFor="let job of jobs" [clrDgItem]="job">
|
||||
<clr-dg-cell>{{ job.id }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{
|
||||
(job.schedule?.type
|
||||
? 'SCHEDULE.' + job.schedule?.type.toUpperCase()
|
||||
: ''
|
||||
) | translate
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{
|
||||
isDryRun(job?.job_parameters) | translate
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{
|
||||
job.job_status.toUpperCase() | translate
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{
|
||||
job.creation_time | harborDatetime: 'medium'
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{
|
||||
job.update_time | harborDatetime: 'medium'
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
[href]="getLogLink(job.id)">
|
||||
<clr-icon shape="list"></clr-icon>
|
||||
</a>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination
|
||||
#pagination
|
||||
[clrDgPageSize]="pageSize"
|
||||
[(clrDgPage)]="page"
|
||||
[clrDgTotalItems]="total">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[5, 25, 50]">{{
|
||||
'PAGINATION.PAGE_SIZE' | translate
|
||||
}}</clr-dg-page-size>
|
||||
<span *ngIf="total"
|
||||
>{{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }}
|
||||
{{ 'DESTINATION.OF' | translate }}</span
|
||||
>
|
||||
{{ total }} {{ 'DESTINATION.ITEMS' | translate }}
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
@ -0,0 +1,106 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
ComponentFixtureAutoDetect,
|
||||
fakeAsync,
|
||||
TestBed,
|
||||
tick,
|
||||
} from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { SharedTestingModule } from '../../../../../shared/shared.module';
|
||||
import { HttpHeaders, HttpResponse } from '@angular/common/http';
|
||||
import { CURRENT_BASE_HREF } from '../../../../../shared/units/utils';
|
||||
import { delay } from 'rxjs/operators';
|
||||
import { PurgeHistoryComponent } from './purge-history.component';
|
||||
import { ExecHistory } from '../../../../../../../ng-swagger-gen/models/exec-history';
|
||||
import { PurgeService } from 'ng-swagger-gen/services/purge.service';
|
||||
|
||||
describe('GcHistoryComponent', () => {
|
||||
let component: PurgeHistoryComponent;
|
||||
let fixture: ComponentFixture<PurgeHistoryComponent>;
|
||||
const mockJobs: ExecHistory[] = [
|
||||
{
|
||||
id: 1,
|
||||
job_name: 'test',
|
||||
job_kind: 'manual',
|
||||
schedule: null,
|
||||
job_status: 'pending',
|
||||
job_parameters: '{"dry_run":true}',
|
||||
creation_time: null,
|
||||
update_time: null,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
job_name: 'test',
|
||||
job_kind: 'manual',
|
||||
schedule: null,
|
||||
job_status: 'finished',
|
||||
job_parameters: '{"dry_run":true}',
|
||||
creation_time: null,
|
||||
update_time: null,
|
||||
},
|
||||
];
|
||||
const fakedPurgeService = {
|
||||
count: 0,
|
||||
getPurgeHistoryResponse() {
|
||||
if (this.count === 0) {
|
||||
this.count += 1;
|
||||
const response: HttpResponse<Array<ExecHistory>> =
|
||||
new HttpResponse<Array<ExecHistory>>({
|
||||
headers: new HttpHeaders({
|
||||
'x-total-count': [mockJobs[0]].length.toString(),
|
||||
}),
|
||||
body: [mockJobs[0]],
|
||||
});
|
||||
return of(response).pipe(delay(0));
|
||||
} else {
|
||||
this.count += 1;
|
||||
const response: HttpResponse<Array<ExecHistory>> =
|
||||
new HttpResponse<Array<ExecHistory>>({
|
||||
headers: new HttpHeaders({
|
||||
'x-total-count': [mockJobs[1]].length.toString(),
|
||||
}),
|
||||
body: [mockJobs[1]],
|
||||
});
|
||||
return of(response).pipe(delay(0));
|
||||
}
|
||||
},
|
||||
};
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PurgeHistoryComponent],
|
||||
imports: [SharedTestingModule],
|
||||
providers: [
|
||||
{ provide: PurgeService, useValue: fakedPurgeService },
|
||||
// open auto detect
|
||||
{ provide: ComponentFixtureAutoDetect, useValue: true },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PurgeHistoryComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
afterEach(() => {
|
||||
if (component && component.timerDelay) {
|
||||
component.timerDelay.unsubscribe();
|
||||
component.timerDelay = null;
|
||||
}
|
||||
});
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('should retry getting jobs', fakeAsync(() => {
|
||||
tick(10000);
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.jobs[0].job_status).toEqual('finished');
|
||||
});
|
||||
}));
|
||||
it('should return right log link', () => {
|
||||
expect(component.getLogLink('1')).toEqual(
|
||||
`${CURRENT_BASE_HREF}/system/purgeaudit/1/log`
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,137 @@
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { ClrDatagridStateInterface } from '@clr/angular';
|
||||
import { GCHistory } from 'ng-swagger-gen/models/gchistory';
|
||||
import { finalize, Subscription, timer } from 'rxjs';
|
||||
import { REFRESH_TIME_DIFFERENCE } from 'src/app/shared/entities/shared.const';
|
||||
import { ErrorHandler } from 'src/app/shared/units/error-handler/error-handler';
|
||||
import {
|
||||
CURRENT_BASE_HREF,
|
||||
getPageSizeFromLocalStorage,
|
||||
getSortingString,
|
||||
PageSizeMapKeys,
|
||||
setPageSizeToLocalStorage,
|
||||
} from 'src/app/shared/units/utils';
|
||||
import { PurgeService } from '../../../../../../../ng-swagger-gen/services/purge.service';
|
||||
import { JOB_STATUS, NO, YES } from '../../clearing-job-interfact';
|
||||
|
||||
@Component({
|
||||
selector: 'app-purge-history',
|
||||
templateUrl: './purge-history.component.html',
|
||||
styleUrls: ['./purge-history.component.scss'],
|
||||
})
|
||||
export class PurgeHistoryComponent implements OnDestroy {
|
||||
jobs: Array<GCHistory> = [];
|
||||
loading: boolean = true;
|
||||
timerDelay: Subscription;
|
||||
pageSize: number = getPageSizeFromLocalStorage(
|
||||
PageSizeMapKeys.GC_HISTORY_COMPONENT,
|
||||
5
|
||||
);
|
||||
page: number = 1;
|
||||
total: number = 0;
|
||||
state: ClrDatagridStateInterface;
|
||||
constructor(
|
||||
private purgeService: PurgeService,
|
||||
private errorHandler: ErrorHandler
|
||||
) {}
|
||||
refresh() {
|
||||
this.page = 1;
|
||||
this.total = 0;
|
||||
this.getJobs(true);
|
||||
}
|
||||
|
||||
getJobs(withLoading: boolean, state?: ClrDatagridStateInterface) {
|
||||
if (state) {
|
||||
this.state = state;
|
||||
}
|
||||
if (state && state.page) {
|
||||
this.pageSize = state.page.size;
|
||||
setPageSizeToLocalStorage(
|
||||
PageSizeMapKeys.GC_HISTORY_COMPONENT,
|
||||
this.pageSize
|
||||
);
|
||||
}
|
||||
let q: string;
|
||||
if (state && state.filters && state.filters.length) {
|
||||
q = encodeURIComponent(
|
||||
`${state.filters[0].property}=~${state.filters[0].value}`
|
||||
);
|
||||
}
|
||||
let sort: string;
|
||||
if (state && state.sort && state.sort.by) {
|
||||
sort = getSortingString(state);
|
||||
}
|
||||
if (withLoading) {
|
||||
this.loading = true;
|
||||
}
|
||||
this.purgeService
|
||||
.getPurgeHistoryResponse({
|
||||
page: this.page,
|
||||
pageSize: this.pageSize,
|
||||
q: q,
|
||||
sort: sort,
|
||||
})
|
||||
.pipe(finalize(() => (this.loading = false)))
|
||||
.subscribe(
|
||||
res => {
|
||||
// Get total count
|
||||
if (res.headers) {
|
||||
const xHeader: string =
|
||||
res.headers.get('X-Total-Count');
|
||||
if (xHeader) {
|
||||
this.total = parseInt(xHeader, 0);
|
||||
}
|
||||
this.jobs = res.body;
|
||||
}
|
||||
// to avoid some jobs not finished.
|
||||
if (!this.timerDelay) {
|
||||
this.timerDelay = timer(
|
||||
REFRESH_TIME_DIFFERENCE,
|
||||
REFRESH_TIME_DIFFERENCE
|
||||
).subscribe(() => {
|
||||
let count: number = 0;
|
||||
this.jobs.forEach(job => {
|
||||
if (
|
||||
job.job_status === JOB_STATUS.PENDING ||
|
||||
job.job_status === JOB_STATUS.RUNNING
|
||||
) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
if (count > 0) {
|
||||
this.getJobs(false, this.state);
|
||||
} else {
|
||||
this.timerDelay.unsubscribe();
|
||||
this.timerDelay = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
error => {
|
||||
this.errorHandler.error(error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
isDryRun(param: string): string {
|
||||
if (param) {
|
||||
const paramObj: any = JSON.parse(param);
|
||||
if (paramObj && paramObj.dry_run) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.timerDelay) {
|
||||
this.timerDelay.unsubscribe();
|
||||
this.timerDelay = null;
|
||||
}
|
||||
}
|
||||
|
||||
getLogLink(id): string {
|
||||
return `${CURRENT_BASE_HREF}/system/purgeaudit/${id}/log`;
|
||||
}
|
||||
}
|
@ -0,0 +1,211 @@
|
||||
<div [hidden]="!loadingGcStatus" class="clr-row mt-2 center">
|
||||
<span class="spinner spinner-md"></span>
|
||||
</div>
|
||||
<div [hidden]="loadingGcStatus">
|
||||
<div class="clr-row mt-1">
|
||||
<div class="clr-col-2 flex-200 font-style">
|
||||
{{ 'WEBHOOK.STATUS' | translate }}
|
||||
</div>
|
||||
<div class="clr-col">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-4">
|
||||
<span class="mr-1 font-style">{{
|
||||
'CLEARANCES.LAST_COMPLETED' | translate
|
||||
}}</span>
|
||||
<span class="mr-3">
|
||||
<span
|
||||
*ngIf="loadingLastCompletedTime"
|
||||
class="spinner spinner-inline"></span>
|
||||
<ng-container *ngIf="!loadingLastCompletedTime">
|
||||
<span *ngIf="!lastCompletedTime">{{
|
||||
'SCHEDULE.NONE' | translate
|
||||
}}</span>
|
||||
<span *ngIf="lastCompletedTime"
|
||||
>{{ lastCompletedTime | harborDatetime
|
||||
}}<span *ngIf="isDryRun"
|
||||
>({{
|
||||
'TAG_RETENTION.DRY_RUN' | translate
|
||||
}})</span
|
||||
></span
|
||||
>
|
||||
</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
<div class="clr-col">
|
||||
<span class="mr-1 font-style" *ngIf="nextScheduledTime">{{
|
||||
'CLEARANCES.NEXT_SCHEDULED_TIME' | translate
|
||||
}}</span>
|
||||
<span *ngIf="nextScheduledTime">{{
|
||||
nextScheduledTime | harborDatetime
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cron-selection">
|
||||
<cron-selection
|
||||
[externalValidation]="
|
||||
!(purgeForm.invalid || !(selectedOperations?.length > 0))
|
||||
"
|
||||
[labelCurrent]="getLabelCurrent"
|
||||
[labelEdit]="getLabelCurrent"
|
||||
[originCron]="originCron"
|
||||
(inputvalue)="saveGcSchedule($event)"></cron-selection>
|
||||
</div>
|
||||
|
||||
<form #purgeForm="ngForm" class="clr-form clr-form-horizontal p-0">
|
||||
<div class="clr-form-control mt-0">
|
||||
<span class="required font-style flex-200"
|
||||
>{{ 'CLEARANCES.KEEP_IN' | translate }}
|
||||
<clr-tooltip>
|
||||
<clr-icon
|
||||
clrTooltipTrigger
|
||||
shape="info-circle"
|
||||
size="24"></clr-icon>
|
||||
<clr-tooltip-content
|
||||
clrPosition="top-right"
|
||||
clrSize="lg"
|
||||
*clrIfOpen>
|
||||
<span>{{
|
||||
'CLEARANCES.KEEP_IN_TOOLTIP' | translate
|
||||
}}</span>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip></span
|
||||
>
|
||||
<div
|
||||
class="clr-control-container input-width"
|
||||
[class.clr-error]="
|
||||
(retentionTimeNgModel.dirty ||
|
||||
retentionTimeNgModel.touched) &&
|
||||
retentionTimeNgModel.invalid
|
||||
">
|
||||
<div class="flex">
|
||||
<div class="clr-input-wrapper">
|
||||
<input
|
||||
[disabled]="dryRunOnGoing"
|
||||
class="clr-input"
|
||||
name="retentionTime"
|
||||
type="text"
|
||||
#retentionTimeNgModel="ngModel"
|
||||
autocomplete="off"
|
||||
[(ngModel)]="retentionTime"
|
||||
required
|
||||
pattern="^[\-1-9]{1}[0-9]*$"
|
||||
id="retentionTime"
|
||||
size="20" />
|
||||
<clr-icon
|
||||
class="clr-validate-icon"
|
||||
shape="exclamation-circle"></clr-icon>
|
||||
<clr-control-error
|
||||
*ngIf="
|
||||
(retentionTimeNgModel.dirty ||
|
||||
retentionTimeNgModel.touched) &&
|
||||
retentionTimeNgModel.invalid
|
||||
">
|
||||
{{ 'CLEARANCES.KEEP_IN_ERROR' | translate }}
|
||||
</clr-control-error>
|
||||
</div>
|
||||
<div class="clr-select-wrapper unit-select">
|
||||
<select
|
||||
[(ngModel)]="retentionUnit"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
id="expiration-type"
|
||||
class="clr-select">
|
||||
<option value="days">
|
||||
{{ 'CLEARANCES.DAYS' | translate }}
|
||||
</option>
|
||||
<option value="hours">
|
||||
{{ 'CLEARANCES.HOURS' | translate }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="clr-form-control"
|
||||
[ngClass]="{
|
||||
'mt-08': !(
|
||||
(retentionTimeNgModel.dirty ||
|
||||
retentionTimeNgModel.touched) &&
|
||||
retentionTimeNgModel.invalid
|
||||
)
|
||||
}">
|
||||
<span class="font-style required flex-200"
|
||||
>{{ 'CLEARANCES.INCLUDED_OPERATIONS' | translate
|
||||
}}<clr-tooltip>
|
||||
<clr-icon
|
||||
clrTooltipTrigger
|
||||
shape="info-circle"
|
||||
size="24"></clr-icon>
|
||||
<clr-tooltip-content
|
||||
clrPosition="top-right"
|
||||
clrSize="lg"
|
||||
*clrIfOpen>
|
||||
<span>{{
|
||||
'CLEARANCES.INCLUDED_OPERATION_TOOLTIP' | translate
|
||||
}}</span>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip></span
|
||||
>
|
||||
<div
|
||||
class="clr-control-container clr-control-inline"
|
||||
[class.clr-error]="!(selectedOperations?.length > 0)">
|
||||
<div
|
||||
class="clr-checkbox-wrapper"
|
||||
*ngFor="let item of operations">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="{{ item }}"
|
||||
name="operations"
|
||||
value="{{ item }}"
|
||||
class="clr-checkbox"
|
||||
(change)="setOperation(item)"
|
||||
[checked]="hasOperation(item)" />
|
||||
<label for="{{ item }}" class="clr-control-label">{{
|
||||
operationsToText(item) | translate
|
||||
}}</label>
|
||||
</div>
|
||||
<div
|
||||
class="clr-subtext-wrapper"
|
||||
*ngIf="!(selectedOperations?.length > 0)">
|
||||
<clr-icon
|
||||
class="clr-validate-icon"
|
||||
shape="exclamation-circle"></clr-icon>
|
||||
<span class="clr-subtext">{{
|
||||
'CLEARANCES.INCLUDED_OPERATION_ERROR' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-2 flex-200">
|
||||
<button
|
||||
id="gc-now"
|
||||
class="btn btn-primary gc-start-btn"
|
||||
(click)="gcNow()"
|
||||
[disabled]="
|
||||
disableGC ||
|
||||
purgeForm.invalid ||
|
||||
!(selectedOperations?.length > 0)
|
||||
">
|
||||
{{ 'CLEARANCES.PURGE_NOW' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clr-col">
|
||||
<button
|
||||
id="gc-dry-run"
|
||||
class="btn btn-outline gc-start-btn"
|
||||
(click)="dryRun()"
|
||||
[disabled]="
|
||||
dryRunOnGoing ||
|
||||
purgeForm.invalid ||
|
||||
!(selectedOperations?.length > 0)
|
||||
">
|
||||
{{ 'TAG_RETENTION.WHAT_IF_RUN' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<app-purge-history></app-purge-history>
|
||||
</div>
|
@ -0,0 +1,36 @@
|
||||
.cron-selection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gc-start-btn {
|
||||
width:150px;
|
||||
margin-top: 35px;
|
||||
}
|
||||
|
||||
.flex-200 {
|
||||
flex: 0 0 200px;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.mt-05 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.font-weight-400 {
|
||||
font-weight: 400;
|
||||
}
|
||||
.center {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.mt-08 {
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
.unit-select {
|
||||
padding-top: 1px;
|
||||
margin-left: 2rem;
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ErrorHandler } from '../../../../../shared/units/error-handler';
|
||||
import { CronScheduleComponent } from '../../../../../shared/components/cron-schedule';
|
||||
import { CronTooltipComponent } from '../../../../../shared/components/cron-schedule';
|
||||
import { of } from 'rxjs';
|
||||
import { SharedTestingModule } from '../../../../../shared/shared.module';
|
||||
import { SetJobComponent } from './set-job.component';
|
||||
import { PurgeService } from 'ng-swagger-gen/services/purge.service';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
|
||||
describe('GcComponent', () => {
|
||||
let component: SetJobComponent;
|
||||
let fixture: ComponentFixture<SetJobComponent>;
|
||||
let purgeService: PurgeService;
|
||||
let mockSchedule = [];
|
||||
const fakedErrorHandler = {
|
||||
error(error) {
|
||||
return error;
|
||||
},
|
||||
info(info) {
|
||||
return info;
|
||||
},
|
||||
};
|
||||
let spySchedule: jasmine.Spy;
|
||||
let spyGcNow: jasmine.Spy;
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [SharedTestingModule],
|
||||
declarations: [
|
||||
SetJobComponent,
|
||||
CronScheduleComponent,
|
||||
CronTooltipComponent,
|
||||
],
|
||||
providers: [{ provide: ErrorHandler, useValue: fakedErrorHandler }],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SetJobComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
purgeService = fixture.debugElement.injector.get(PurgeService);
|
||||
spySchedule = spyOn(purgeService, 'getPurgeSchedule').and.returnValues(
|
||||
of(mockSchedule as any)
|
||||
);
|
||||
spyGcNow = spyOn(purgeService, 'createPurgeSchedule').and.returnValues(
|
||||
of(null)
|
||||
);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('should get schedule and job', () => {
|
||||
expect(spySchedule.calls.count()).toEqual(1);
|
||||
});
|
||||
it('should trigger gcNow', () => {
|
||||
const ele: HTMLButtonElement =
|
||||
fixture.nativeElement.querySelector('#gc-now');
|
||||
ele.click();
|
||||
fixture.detectChanges();
|
||||
expect(spyGcNow.calls.count()).toEqual(1);
|
||||
});
|
||||
it('should trigger dry run', () => {
|
||||
const ele: HTMLButtonElement =
|
||||
fixture.nativeElement.querySelector('#gc-dry-run');
|
||||
ele.click();
|
||||
fixture.detectChanges();
|
||||
expect(spyGcNow.calls.count()).toEqual(1);
|
||||
});
|
||||
});
|
@ -0,0 +1,307 @@
|
||||
import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ErrorHandler } from '../../../../../shared/units/error-handler';
|
||||
import { CronScheduleComponent } from '../../../../../shared/components/cron-schedule';
|
||||
import { OriginCron } from '../../../../../shared/services';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import { ScheduleType } from '../../../../../shared/entities/shared.const';
|
||||
import { GcComponent } from '../../gc-page/gc/gc.component';
|
||||
import { PurgeService } from '../../../../../../../ng-swagger-gen/services/purge.service';
|
||||
import { ExecHistory } from '../../../../../../../ng-swagger-gen/models/exec-history';
|
||||
import {
|
||||
JOB_STATUS,
|
||||
REFRESH_STATUS_TIME_DIFFERENCE,
|
||||
RETENTION_OPERATIONS,
|
||||
RETENTION_OPERATIONS_I18N_MAP,
|
||||
RetentionTimeUnit,
|
||||
} from '../../clearing-job-interfact';
|
||||
import { clone } from '../../../../../shared/units/utils';
|
||||
import { PurgeHistoryComponent } from '../history/purge-history.component';
|
||||
|
||||
const ONE_MINUTE: number = 60000;
|
||||
const ONE_DAY: number = 24;
|
||||
|
||||
@Component({
|
||||
selector: 'app-set-job',
|
||||
templateUrl: './set-job.component.html',
|
||||
styleUrls: ['./set-job.component.scss'],
|
||||
})
|
||||
export class SetJobComponent implements OnInit, OnDestroy {
|
||||
originCron: OriginCron;
|
||||
disableGC: boolean = false;
|
||||
getLabelCurrent = 'CLEARANCES.SCHEDULE_TO_PURGE';
|
||||
loadingGcStatus = false;
|
||||
@ViewChild(CronScheduleComponent)
|
||||
CronScheduleComponent: CronScheduleComponent;
|
||||
dryRunOnGoing: boolean = false;
|
||||
lastCompletedTime: string;
|
||||
loadingLastCompletedTime: boolean = false;
|
||||
isDryRun: boolean = false;
|
||||
nextScheduledTime: string;
|
||||
statusTimeout: any;
|
||||
|
||||
retentionTime: number;
|
||||
retentionUnit: string = RetentionTimeUnit.DAYS;
|
||||
operations: string[] = clone(RETENTION_OPERATIONS);
|
||||
selectedOperations: string[] = clone(RETENTION_OPERATIONS);
|
||||
@ViewChild(PurgeHistoryComponent)
|
||||
purgeHistoryComponent: PurgeHistoryComponent;
|
||||
constructor(
|
||||
private purgeService: PurgeService,
|
||||
private errorHandler: ErrorHandler
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.getCurrentSchedule(true);
|
||||
this.getStatus();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
if (this.statusTimeout) {
|
||||
clearTimeout(this.statusTimeout);
|
||||
this.statusTimeout = null;
|
||||
}
|
||||
}
|
||||
// get the latest non-dry-run execution to get the status
|
||||
getStatus() {
|
||||
this.loadingLastCompletedTime = true;
|
||||
this.purgeService
|
||||
.getPurgeHistory({
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
sort: '-update_time',
|
||||
})
|
||||
.subscribe(res => {
|
||||
if (res?.length) {
|
||||
this.isDryRun = JSON.parse(res[0]?.job_parameters).dry_run;
|
||||
this.lastCompletedTime = res[0]?.update_time;
|
||||
if (
|
||||
res[0]?.job_status === JOB_STATUS.RUNNING ||
|
||||
res[0]?.job_status === JOB_STATUS.PENDING
|
||||
) {
|
||||
this.statusTimeout = setTimeout(() => {
|
||||
this.getStatus();
|
||||
}, REFRESH_STATUS_TIME_DIFFERENCE);
|
||||
} else {
|
||||
this.loadingLastCompletedTime = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
getCurrentSchedule(withLoading: boolean) {
|
||||
if (withLoading) {
|
||||
this.loadingGcStatus = true;
|
||||
}
|
||||
this.purgeService
|
||||
.getPurgeSchedule()
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.loadingGcStatus = false;
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: schedule => {
|
||||
this.initSchedule(schedule);
|
||||
},
|
||||
error: error => {
|
||||
this.errorHandler.error(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
initSchedule(purgeHistory: ExecHistory) {
|
||||
if ((purgeHistory?.schedule as any)?.next_scheduled_time) {
|
||||
this.nextScheduledTime = (
|
||||
purgeHistory.schedule as any
|
||||
)?.next_scheduled_time;
|
||||
}
|
||||
if (purgeHistory && purgeHistory.schedule) {
|
||||
this.originCron = {
|
||||
type: purgeHistory.schedule.type,
|
||||
cron: purgeHistory.schedule.cron,
|
||||
};
|
||||
if (purgeHistory && purgeHistory.job_parameters) {
|
||||
const obj = JSON.parse(purgeHistory.job_parameters);
|
||||
if (obj?.include_operations) {
|
||||
this.selectedOperations =
|
||||
obj?.include_operations?.split(',');
|
||||
} else {
|
||||
this.selectedOperations = [];
|
||||
}
|
||||
if (
|
||||
obj?.audit_retention_hour > ONE_DAY &&
|
||||
obj?.audit_retention_hour % ONE_DAY === 0
|
||||
) {
|
||||
this.retentionTime = obj?.audit_retention_hour / ONE_DAY;
|
||||
this.retentionUnit = RetentionTimeUnit.DAYS;
|
||||
} else {
|
||||
this.retentionTime = obj?.audit_retention_hour;
|
||||
this.retentionUnit = RetentionTimeUnit.HOURS;
|
||||
}
|
||||
} else {
|
||||
this.retentionTime = null;
|
||||
this.selectedOperations = clone(RETENTION_OPERATIONS);
|
||||
this.retentionUnit = RetentionTimeUnit.DAYS;
|
||||
}
|
||||
} else {
|
||||
this.originCron = {
|
||||
type: ScheduleType.NONE,
|
||||
cron: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
gcNow(): void {
|
||||
this.disableGC = true;
|
||||
setTimeout(() => {
|
||||
this.enableGc();
|
||||
}, ONE_MINUTE);
|
||||
const retentionTime: number =
|
||||
this.retentionUnit === RetentionTimeUnit.DAYS
|
||||
? this.retentionTime * 24
|
||||
: this.retentionTime;
|
||||
this.purgeService
|
||||
.createPurgeSchedule({
|
||||
schedule: {
|
||||
parameters: {
|
||||
audit_retention_hour: +retentionTime,
|
||||
include_operations: this.selectedOperations.join(','),
|
||||
dry_run: false,
|
||||
},
|
||||
schedule: {
|
||||
type: ScheduleType.MANUAL,
|
||||
},
|
||||
},
|
||||
})
|
||||
.subscribe({
|
||||
next: response => {
|
||||
this.errorHandler.info('CLEARANCES.PURGE_NOW_SUCCESS');
|
||||
this.refresh();
|
||||
},
|
||||
error: error => {
|
||||
this.errorHandler.error(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
dryRun() {
|
||||
this.dryRunOnGoing = true;
|
||||
const retentionTime: number =
|
||||
this.retentionUnit === RetentionTimeUnit.DAYS
|
||||
? this.retentionTime * 24
|
||||
: this.retentionTime;
|
||||
this.purgeService
|
||||
.createPurgeSchedule({
|
||||
schedule: {
|
||||
parameters: {
|
||||
audit_retention_hour: +retentionTime,
|
||||
include_operations: this.selectedOperations.join(','),
|
||||
dry_run: true,
|
||||
},
|
||||
schedule: {
|
||||
type: ScheduleType.MANUAL,
|
||||
},
|
||||
},
|
||||
})
|
||||
.pipe(finalize(() => (this.dryRunOnGoing = false)))
|
||||
.subscribe({
|
||||
next: response => {
|
||||
this.errorHandler.info('GC.DRY_RUN_SUCCESS');
|
||||
this.refresh();
|
||||
},
|
||||
error: error => {
|
||||
this.errorHandler.error(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private enableGc() {
|
||||
this.disableGC = false;
|
||||
}
|
||||
|
||||
saveGcSchedule(cron: string) {
|
||||
const retentionTime: number =
|
||||
this.retentionUnit === RetentionTimeUnit.DAYS
|
||||
? this.retentionTime * 24
|
||||
: this.retentionTime;
|
||||
if (this.originCron && this.originCron.type === ScheduleType.NONE) {
|
||||
// no schedule, then create
|
||||
this.purgeService
|
||||
.createPurgeSchedule({
|
||||
schedule: {
|
||||
parameters: {
|
||||
audit_retention_hour: +retentionTime,
|
||||
include_operations:
|
||||
this.selectedOperations.join(','),
|
||||
dry_run: false,
|
||||
},
|
||||
schedule: {
|
||||
type: GcComponent.getScheduleType(cron),
|
||||
cron: cron,
|
||||
},
|
||||
},
|
||||
})
|
||||
.subscribe({
|
||||
next: response => {
|
||||
this.errorHandler.info(
|
||||
'CLEARANCES.PURGE_SCHEDULE_RESET'
|
||||
);
|
||||
this.CronScheduleComponent.resetSchedule();
|
||||
this.getCurrentSchedule(false); // refresh schedule
|
||||
},
|
||||
error: error => {
|
||||
this.errorHandler.error(error);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.purgeService
|
||||
.updatePurgeSchedule({
|
||||
schedule: {
|
||||
parameters: {
|
||||
audit_retention_hour: +retentionTime,
|
||||
include_operations:
|
||||
this.selectedOperations.join(','),
|
||||
dry_run: false,
|
||||
},
|
||||
schedule: {
|
||||
type: GcComponent.getScheduleType(cron),
|
||||
cron: cron,
|
||||
},
|
||||
},
|
||||
})
|
||||
.subscribe({
|
||||
next: response => {
|
||||
this.errorHandler.info(
|
||||
'CLEARANCES.PURGE_SCHEDULE_RESET'
|
||||
);
|
||||
this.CronScheduleComponent.resetSchedule();
|
||||
this.getCurrentSchedule(false); // refresh schedule
|
||||
},
|
||||
error: error => {
|
||||
this.errorHandler.error(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
hasOperation(operation: string): boolean {
|
||||
return this.selectedOperations?.indexOf(operation) !== -1;
|
||||
}
|
||||
operationsToText(operation: string): string {
|
||||
if (RETENTION_OPERATIONS_I18N_MAP[operation]) {
|
||||
return RETENTION_OPERATIONS_I18N_MAP[operation];
|
||||
}
|
||||
return operation;
|
||||
}
|
||||
setOperation(operation: string) {
|
||||
if (this.selectedOperations.indexOf(operation) === -1) {
|
||||
this.selectedOperations.push(operation);
|
||||
} else {
|
||||
this.selectedOperations.splice(
|
||||
this.selectedOperations.findIndex(item => item === operation),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
refresh() {
|
||||
this.getStatus();
|
||||
this.purgeHistoryComponent?.refresh();
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
export enum RetentionTimeUnit {
|
||||
HOURS = 'hours',
|
||||
DAYS = 'days',
|
||||
}
|
||||
|
||||
export const RETENTION_OPERATIONS = ['create', 'delete', 'pull'];
|
||||
|
||||
export const RETENTION_OPERATIONS_I18N_MAP = {
|
||||
pull: 'AUDIT_LOG.PULL',
|
||||
create: 'AUDIT_LOG.CREATE',
|
||||
delete: 'AUDIT_LOG.DELETE',
|
||||
};
|
||||
|
||||
export const JOB_STATUS = {
|
||||
PENDING: 'Pending',
|
||||
RUNNING: 'Running',
|
||||
};
|
||||
|
||||
export const YES: string = 'TAG_RETENTION.YES';
|
||||
export const NO: string = 'TAG_RETENTION.NO';
|
||||
|
||||
export const REFRESH_STATUS_TIME_DIFFERENCE: number = 5000;
|
@ -0,0 +1,21 @@
|
||||
<h2 class="custom-h2" sub-header-title>
|
||||
{{ 'CLEARANCES.CLEARANCES' | translate }}
|
||||
</h2>
|
||||
<nav class="mt-1">
|
||||
<ul class="nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="gc" routerLinkActive="active">{{
|
||||
'CONFIG.GC' | translate
|
||||
}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
routerLink="audit-log-purge"
|
||||
routerLinkActive="active"
|
||||
>{{ 'CLEARANCES.AUDIT_LOG' | translate }}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<router-outlet></router-outlet>
|
@ -1,30 +1,21 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { SessionService } from '../../../shared/services/session.service';
|
||||
import { GcPageComponent } from './gc-page.component';
|
||||
import { ClearingJobComponent } from './clearing-job.component';
|
||||
import { SharedTestingModule } from '../../../shared/shared.module';
|
||||
|
||||
describe('GcPageComponent', () => {
|
||||
let component: GcPageComponent;
|
||||
let fixture: ComponentFixture<GcPageComponent>;
|
||||
let fakeSessionService = {
|
||||
getCurrentUser: function () {
|
||||
return { has_admin_role: true };
|
||||
},
|
||||
};
|
||||
let component: ClearingJobComponent;
|
||||
let fixture: ComponentFixture<ClearingJobComponent>;
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [GcPageComponent],
|
||||
declarations: [ClearingJobComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
imports: [SharedTestingModule],
|
||||
providers: [
|
||||
{ provide: SessionService, useValue: fakeSessionService },
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(GcPageComponent);
|
||||
fixture = TestBed.createComponent(ClearingJobComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-clearing-job',
|
||||
templateUrl: './clearing-job.component.html',
|
||||
styleUrls: ['./clearing-job.component.scss'],
|
||||
})
|
||||
export class ClearingJobComponent {
|
||||
inProgress: boolean = true;
|
||||
constructor() {}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { GcComponent } from './gc-page/gc/gc.component';
|
||||
import { GcHistoryComponent } from './gc-page/gc/gc-history/gc-history.component';
|
||||
import { SharedModule } from '../../../shared/shared.module';
|
||||
import { SetJobComponent } from './audit-log-purge/set-job/set-job.component';
|
||||
import { ClearingJobComponent } from './clearing-job.component';
|
||||
import { PurgeHistoryComponent } from './audit-log-purge/history/purge-history.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ClearingJobComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'gc',
|
||||
component: GcComponent,
|
||||
},
|
||||
{
|
||||
path: 'audit-log-purge',
|
||||
component: SetJobComponent,
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'gc',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@NgModule({
|
||||
imports: [SharedModule, RouterModule.forChild(routes)],
|
||||
declarations: [
|
||||
GcComponent,
|
||||
GcHistoryComponent,
|
||||
ClearingJobComponent,
|
||||
SetJobComponent,
|
||||
PurgeHistoryComponent,
|
||||
],
|
||||
})
|
||||
export class ClearingJobModule {}
|
@ -1,4 +1,4 @@
|
||||
<h5 class="history-header" id="history-header">
|
||||
<h5 class="history-header mt-3 font-style" id="history-header">
|
||||
{{ 'GC.JOB_HISTORY' | translate }}
|
||||
</h5>
|
||||
<span class="refresh-btn" (click)="refresh()">
|
||||
@ -51,7 +51,7 @@
|
||||
[clrDgPageSize]="pageSize"
|
||||
[(clrDgPage)]="page"
|
||||
[clrDgTotalItems]="total">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[15, 25, 50]">{{
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[5, 25, 50]">{{
|
||||
'PAGINATION.PAGE_SIZE' | translate
|
||||
}}</clr-dg-page-size>
|
||||
<span *ngIf="total"
|
@ -0,0 +1,13 @@
|
||||
.history-header {
|
||||
margin:20px 0 6px 0;
|
||||
display: inline-block;
|
||||
width: 97%;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #007CBB;
|
||||
}
|
||||
}
|
||||
|
@ -7,12 +7,12 @@ import {
|
||||
} from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { GcHistoryComponent } from './gc-history.component';
|
||||
import { SharedTestingModule } from '../../../../../shared/shared.module';
|
||||
import { GCHistory } from '../../../../../../../ng-swagger-gen/models/gchistory';
|
||||
import { SharedTestingModule } from '../../../../../../shared/shared.module';
|
||||
import { GCHistory } from '../../../../../../../../ng-swagger-gen/models/gchistory';
|
||||
import { HttpHeaders, HttpResponse } from '@angular/common/http';
|
||||
import { Registry } from '../../../../../../../ng-swagger-gen/models/registry';
|
||||
import { GcService } from '../../../../../../../ng-swagger-gen/services/gc.service';
|
||||
import { CURRENT_BASE_HREF } from '../../../../../shared/units/utils';
|
||||
import { Registry } from '../../../../../../../../ng-swagger-gen/models/registry';
|
||||
import { GcService } from '../../../../../../../../ng-swagger-gen/services/gc.service';
|
||||
import { CURRENT_BASE_HREF } from '../../../../../../shared/units/utils';
|
||||
import { delay } from 'rxjs/operators';
|
||||
|
||||
describe('GcHistoryComponent', () => {
|
@ -1,25 +1,19 @@
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { ErrorHandler } from '../../../../../shared/units/error-handler';
|
||||
import { ErrorHandler } from '../../../../../../shared/units/error-handler';
|
||||
import { Subscription, timer } from 'rxjs';
|
||||
import { REFRESH_TIME_DIFFERENCE } from '../../../../../shared/entities/shared.const';
|
||||
import { GcService } from '../../../../../../../ng-swagger-gen/services/gc.service';
|
||||
import { REFRESH_TIME_DIFFERENCE } from '../../../../../../shared/entities/shared.const';
|
||||
import { GcService } from '../../../../../../../../ng-swagger-gen/services/gc.service';
|
||||
import {
|
||||
CURRENT_BASE_HREF,
|
||||
getPageSizeFromLocalStorage,
|
||||
getSortingString,
|
||||
PageSizeMapKeys,
|
||||
setPageSizeToLocalStorage,
|
||||
} from '../../../../../shared/units/utils';
|
||||
} from '../../../../../../shared/units/utils';
|
||||
import { ClrDatagridStateInterface } from '@clr/angular';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import { GCHistory } from '../../../../../../../ng-swagger-gen/models/gchistory';
|
||||
|
||||
const JOB_STATUS = {
|
||||
PENDING: 'Pending',
|
||||
RUNNING: 'Running',
|
||||
};
|
||||
const YES: string = 'TAG_RETENTION.YES';
|
||||
const NO: string = 'TAG_RETENTION.NO';
|
||||
import { GCHistory } from '../../../../../../../../ng-swagger-gen/models/gchistory';
|
||||
import { JOB_STATUS, NO, YES } from '../../../clearing-job-interfact';
|
||||
|
||||
@Component({
|
||||
selector: 'gc-history',
|
||||
@ -31,7 +25,8 @@ export class GcHistoryComponent implements OnDestroy {
|
||||
loading: boolean = true;
|
||||
timerDelay: Subscription;
|
||||
pageSize: number = getPageSizeFromLocalStorage(
|
||||
PageSizeMapKeys.GC_HISTORY_COMPONENT
|
||||
PageSizeMapKeys.GC_HISTORY_COMPONENT,
|
||||
5
|
||||
);
|
||||
page: number = 1;
|
||||
total: number = 0;
|
@ -0,0 +1,97 @@
|
||||
<div [hidden]="!loadingGcStatus" class="clr-row mt-2 center">
|
||||
<span class="spinner spinner-md"></span>
|
||||
</div>
|
||||
<div [hidden]="loadingGcStatus">
|
||||
<div class="clr-row mt-1">
|
||||
<div class="clr-col-2 flex-200 font-style">
|
||||
{{ 'WEBHOOK.STATUS' | translate }}
|
||||
</div>
|
||||
<div class="clr-col">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-4">
|
||||
<span class="mr-1 font-style">{{
|
||||
'CLEARANCES.LAST_COMPLETED' | translate
|
||||
}}</span>
|
||||
<span class="mr-3">
|
||||
<span
|
||||
*ngIf="loadingLastCompletedTime"
|
||||
class="spinner spinner-inline"></span>
|
||||
<ng-container *ngIf="!loadingLastCompletedTime">
|
||||
<span *ngIf="!lastCompletedTime">{{
|
||||
'SCHEDULE.NONE' | translate
|
||||
}}</span>
|
||||
<span *ngIf="lastCompletedTime"
|
||||
>{{ lastCompletedTime | harborDatetime
|
||||
}}<span *ngIf="isDryRun"
|
||||
>({{
|
||||
'TAG_RETENTION.DRY_RUN' | translate
|
||||
}})</span
|
||||
></span
|
||||
>
|
||||
</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
<div class="clr-col">
|
||||
<span class="mr-1 font-style" *ngIf="nextScheduledTime">{{
|
||||
'CLEARANCES.NEXT_SCHEDULED_TIME' | translate
|
||||
}}</span>
|
||||
<span *ngIf="nextScheduledTime">{{
|
||||
nextScheduledTime | harborDatetime
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cron-selection">
|
||||
<cron-selection
|
||||
[labelCurrent]="getLabelCurrent"
|
||||
[labelEdit]="getLabelCurrent"
|
||||
[originCron]="originCron"
|
||||
(inputvalue)="saveGcSchedule($event)"></cron-selection>
|
||||
</div>
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-2 flex-200"></div>
|
||||
<div class="clr-col">
|
||||
<span class="explain">{{ 'GC.EXPLAIN' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-2 flex-200"></div>
|
||||
<div class="clr-col">
|
||||
<clr-toggle-container class="mt-05">
|
||||
<clr-toggle-wrapper>
|
||||
<input
|
||||
type="checkbox"
|
||||
clrToggle
|
||||
name="delete_untagged"
|
||||
id="delete_untagged"
|
||||
[(ngModel)]="shouldDeleteUntagged" />
|
||||
<label class="font-weight-400" for="delete_untagged">{{
|
||||
'GC.DELETE_UNTAGGED' | translate
|
||||
}}</label>
|
||||
</clr-toggle-wrapper>
|
||||
</clr-toggle-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-2 flex-200">
|
||||
<button
|
||||
id="gc-now"
|
||||
class="btn btn-primary gc-start-btn"
|
||||
(click)="gcNow()"
|
||||
[disabled]="disableGC">
|
||||
{{ 'GC.GC_NOW' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clr-col">
|
||||
<button
|
||||
id="gc-dry-run"
|
||||
class="btn btn-outline gc-start-btn"
|
||||
(click)="dryRun()"
|
||||
[disabled]="dryRunOnGoing">
|
||||
{{ 'TAG_RETENTION.WHAT_IF_RUN' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<gc-history></gc-history>
|
||||
</div>
|
@ -1,5 +1,4 @@
|
||||
.cron-selection {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@ -26,3 +25,7 @@
|
||||
font-weight: 100;
|
||||
font-size: 10px;
|
||||
}
|
||||
.center {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { GcComponent } from './gc.component';
|
||||
import { ErrorHandler } from '../../../../shared/units/error-handler';
|
||||
import { CronScheduleComponent } from '../../../../shared/components/cron-schedule';
|
||||
import { CronTooltipComponent } from '../../../../shared/components/cron-schedule';
|
||||
import { ErrorHandler } from '../../../../../shared/units/error-handler';
|
||||
import { CronScheduleComponent } from '../../../../../shared/components/cron-schedule';
|
||||
import { CronTooltipComponent } from '../../../../../shared/components/cron-schedule';
|
||||
import { of } from 'rxjs';
|
||||
import { SharedTestingModule } from '../../../../shared/shared.module';
|
||||
import { GcService } from '../../../../../../ng-swagger-gen/services/gc.service';
|
||||
import { ScheduleType } from '../../../../shared/entities/shared.const';
|
||||
import { SharedTestingModule } from '../../../../../shared/shared.module';
|
||||
import { GcService } from '../../../../../../../ng-swagger-gen/services/gc.service';
|
||||
import { ScheduleType } from '../../../../../shared/entities/shared.const';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
|
||||
describe('GcComponent', () => {
|
||||
let component: GcComponent;
|
||||
@ -23,6 +24,7 @@ describe('GcComponent', () => {
|
||||
};
|
||||
let spySchedule: jasmine.Spy;
|
||||
let spyGcNow: jasmine.Spy;
|
||||
let spyStatus: jasmine.Spy;
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [SharedTestingModule],
|
||||
@ -32,6 +34,7 @@ describe('GcComponent', () => {
|
||||
CronTooltipComponent,
|
||||
],
|
||||
providers: [{ provide: ErrorHandler, useValue: fakedErrorHandler }],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
@ -46,6 +49,20 @@ describe('GcComponent', () => {
|
||||
spyGcNow = spyOn(gcRepoService, 'createGCSchedule').and.returnValues(
|
||||
of(null)
|
||||
);
|
||||
spyStatus = spyOn(gcRepoService, 'getGCHistory').and.returnValues(
|
||||
of([
|
||||
{
|
||||
id: 1,
|
||||
job_name: 'test',
|
||||
job_kind: 'manual',
|
||||
schedule: null,
|
||||
job_status: 'finished',
|
||||
job_parameters: '{"dry_run":true}',
|
||||
creation_time: null,
|
||||
update_time: null,
|
||||
},
|
||||
])
|
||||
);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should create', () => {
|
@ -1,17 +1,16 @@
|
||||
import {
|
||||
Component,
|
||||
Output,
|
||||
EventEmitter,
|
||||
ViewChild,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { ErrorHandler } from '../../../../shared/units/error-handler';
|
||||
import { CronScheduleComponent } from '../../../../shared/components/cron-schedule';
|
||||
import { OriginCron } from '../../../../shared/services';
|
||||
import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ErrorHandler } from '../../../../../shared/units/error-handler';
|
||||
import { CronScheduleComponent } from '../../../../../shared/components/cron-schedule';
|
||||
import { OriginCron } from '../../../../../shared/services';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import { GcService } from '../../../../../../ng-swagger-gen/services/gc.service';
|
||||
import { GCHistory } from '../../../../../../ng-swagger-gen/models/gchistory';
|
||||
import { ScheduleType } from '../../../../shared/entities/shared.const';
|
||||
import { GcService } from '../../../../../../../ng-swagger-gen/services/gc.service';
|
||||
import { GCHistory } from '../../../../../../../ng-swagger-gen/models/gchistory';
|
||||
import { ScheduleType } from '../../../../../shared/entities/shared.const';
|
||||
import { GcHistoryComponent } from './gc-history/gc-history.component';
|
||||
import {
|
||||
JOB_STATUS,
|
||||
REFRESH_STATUS_TIME_DIFFERENCE,
|
||||
} from '../../clearing-job-interfact';
|
||||
|
||||
const ONE_MINUTE = 60000;
|
||||
|
||||
@ -20,16 +19,22 @@ const ONE_MINUTE = 60000;
|
||||
templateUrl: './gc.component.html',
|
||||
styleUrls: ['./gc.component.scss'],
|
||||
})
|
||||
export class GcComponent implements OnInit {
|
||||
export class GcComponent implements OnInit, OnDestroy {
|
||||
originCron: OriginCron;
|
||||
disableGC: boolean = false;
|
||||
getLabelCurrent = 'GC.CURRENT_SCHEDULE';
|
||||
@Output() loadingGcStatus = new EventEmitter<boolean>();
|
||||
loadingGcStatus = false;
|
||||
@ViewChild(CronScheduleComponent)
|
||||
CronScheduleComponent: CronScheduleComponent;
|
||||
shouldDeleteUntagged: boolean;
|
||||
dryRunOnGoing: boolean = false;
|
||||
|
||||
lastCompletedTime: string;
|
||||
loadingLastCompletedTime: boolean = false;
|
||||
isDryRun: boolean = false;
|
||||
nextScheduledTime: string;
|
||||
statusTimeout: any;
|
||||
@ViewChild(GcHistoryComponent) gcHistoryComponent: GcHistoryComponent;
|
||||
constructor(
|
||||
private gcService: GcService,
|
||||
private errorHandler: ErrorHandler
|
||||
@ -37,15 +42,47 @@ export class GcComponent implements OnInit {
|
||||
|
||||
ngOnInit() {
|
||||
this.getCurrentSchedule();
|
||||
this.getStatus();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
if (this.statusTimeout) {
|
||||
clearTimeout(this.statusTimeout);
|
||||
this.statusTimeout = null;
|
||||
}
|
||||
}
|
||||
// get the latest non-dry-run execution to get the status
|
||||
getStatus() {
|
||||
this.loadingLastCompletedTime = true;
|
||||
this.gcService
|
||||
.getGCHistory({
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
sort: '-update_time',
|
||||
})
|
||||
.subscribe(res => {
|
||||
if (res?.length) {
|
||||
this.isDryRun = JSON.parse(res[0]?.job_parameters).dry_run;
|
||||
this.lastCompletedTime = res[0]?.update_time;
|
||||
if (
|
||||
res[0]?.job_status === JOB_STATUS.RUNNING ||
|
||||
res[0]?.job_status === JOB_STATUS.PENDING
|
||||
) {
|
||||
this.statusTimeout = setTimeout(() => {
|
||||
this.getStatus();
|
||||
}, REFRESH_STATUS_TIME_DIFFERENCE);
|
||||
} else {
|
||||
this.loadingLastCompletedTime = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentSchedule() {
|
||||
this.loadingGcStatus.emit(true);
|
||||
this.loadingGcStatus = true;
|
||||
this.gcService
|
||||
.getGCSchedule()
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.loadingGcStatus.emit(false);
|
||||
this.loadingGcStatus = false;
|
||||
})
|
||||
)
|
||||
.subscribe(
|
||||
@ -59,6 +96,11 @@ export class GcComponent implements OnInit {
|
||||
}
|
||||
|
||||
private initSchedule(gcHistory: GCHistory) {
|
||||
if ((gcHistory?.schedule as any)?.next_scheduled_time) {
|
||||
this.nextScheduledTime = (
|
||||
gcHistory.schedule as any
|
||||
)?.next_scheduled_time;
|
||||
}
|
||||
if (gcHistory && gcHistory.schedule) {
|
||||
this.originCron = {
|
||||
type: gcHistory.schedule.type,
|
||||
@ -97,14 +139,15 @@ export class GcComponent implements OnInit {
|
||||
},
|
||||
},
|
||||
})
|
||||
.subscribe(
|
||||
response => {
|
||||
.subscribe({
|
||||
next: response => {
|
||||
this.errorHandler.info('GC.MSG_SUCCESS');
|
||||
this.refresh();
|
||||
},
|
||||
error => {
|
||||
error: error => {
|
||||
this.errorHandler.error(error);
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
dryRun() {
|
||||
@ -122,14 +165,15 @@ export class GcComponent implements OnInit {
|
||||
},
|
||||
})
|
||||
.pipe(finalize(() => (this.dryRunOnGoing = false)))
|
||||
.subscribe(
|
||||
response => {
|
||||
.subscribe({
|
||||
next: response => {
|
||||
this.errorHandler.info('GC.DRY_RUN_SUCCESS');
|
||||
this.refresh();
|
||||
},
|
||||
error => {
|
||||
error: error => {
|
||||
this.errorHandler.error(error);
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private enableGc() {
|
||||
@ -137,7 +181,7 @@ export class GcComponent implements OnInit {
|
||||
}
|
||||
|
||||
saveGcSchedule(cron: string) {
|
||||
if (this.originCron && this.originCron.type !== ScheduleType.NONE) {
|
||||
if (this.originCron && this.originCron.type === ScheduleType.NONE) {
|
||||
// no schedule, then create
|
||||
this.gcService
|
||||
.createGCSchedule({
|
||||
@ -206,4 +250,8 @@ export class GcComponent implements OnInit {
|
||||
}
|
||||
return ScheduleType.NONE;
|
||||
}
|
||||
refresh() {
|
||||
this.getStatus();
|
||||
this.gcHistoryComponent?.refresh();
|
||||
}
|
||||
}
|
@ -27,6 +27,16 @@
|
||||
{{ 'CONFIG.EMAIL' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
<li role="presentation" class="nav-item">
|
||||
<button
|
||||
id="config-security"
|
||||
class="btn btn-link nav-link"
|
||||
type="button"
|
||||
routerLink="security"
|
||||
routerLinkActive="active">
|
||||
{{ 'HELM_CHART.SECURITY' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
<li role="presentation" class="nav-item">
|
||||
<button
|
||||
id="config-system"
|
||||
|
@ -20,6 +20,7 @@ import { ConfigurationEmailComponent } from './email/config-email.component';
|
||||
import { SystemSettingsComponent } from './system/system-settings.component';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { ConfigService } from './config.service';
|
||||
import { SecurityComponent } from './security/security.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@ -34,6 +35,10 @@ const routes: Routes = [
|
||||
path: 'email',
|
||||
component: ConfigurationEmailComponent,
|
||||
},
|
||||
{
|
||||
path: 'security',
|
||||
component: SecurityComponent,
|
||||
},
|
||||
{
|
||||
path: 'setting',
|
||||
component: SystemSettingsComponent,
|
||||
@ -53,6 +58,7 @@ const routes: Routes = [
|
||||
ConfigurationAuthComponent,
|
||||
ConfigurationEmailComponent,
|
||||
SystemSettingsComponent,
|
||||
SecurityComponent,
|
||||
],
|
||||
providers: [ConfigService],
|
||||
})
|
||||
|
@ -10,7 +10,7 @@ import { ConfigureService } from 'ng-swagger-gen/services/configure.service';
|
||||
import { clone } from '../../../shared/units/utils';
|
||||
import { MessageHandlerService } from '../../../shared/services/message-handler.service';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
|
||||
const fakePass = 'aWpLOSYkIzJTTU4wMDkx';
|
||||
|
||||
@ -116,4 +116,10 @@ export class ConfigService {
|
||||
);
|
||||
this.confirmService.openComfirmDialog(msg);
|
||||
}
|
||||
|
||||
saveConfiguration(changes: any): Observable<any> {
|
||||
return this.configureService.updateConfigurations({
|
||||
configurations: changes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -108,6 +108,8 @@ export class Configuration {
|
||||
cfg_expiration: NumberValueItem;
|
||||
oidc_groups_claim: StringValueItem;
|
||||
oidc_admin_group: StringValueItem;
|
||||
audit_log_forward_endpoint: StringValueItem;
|
||||
skip_audit_log_database: BoolValueItem;
|
||||
public constructor() {
|
||||
this.auth_mode = new StringValueItem('db_auth', true);
|
||||
this.project_creation_restriction = new StringValueItem(
|
||||
@ -178,6 +180,8 @@ export class Configuration {
|
||||
this.oidc_user_claim = new StringValueItem('', true);
|
||||
this.count_per_project = new NumberValueItem(-1, true);
|
||||
this.storage_per_project = new NumberValueItem(-1, true);
|
||||
this.audit_log_forward_endpoint = new StringValueItem('', true);
|
||||
this.skip_audit_log_database = new BoolValueItem(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,156 @@
|
||||
<form class="clr-form clr-form-horizontal">
|
||||
<section>
|
||||
<div class="clr-form-control d-f">
|
||||
<label class="clr-control-label">{{
|
||||
'CVE_ALLOWLIST.DEPLOYMENT_SECURITY' | translate
|
||||
}}</label>
|
||||
<div class="form-content w-100">
|
||||
<div class="font-size-13">
|
||||
<div class="mt-05">
|
||||
<span class="title font-size-13">{{
|
||||
'CVE_ALLOWLIST.CVE_ALLOWLIST' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="mt-05">
|
||||
<span>{{
|
||||
'CVE_ALLOWLIST.SYS_ALLOWLIST_EXPLAIN' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="mt-05">
|
||||
<span>{{ 'CVE_ALLOWLIST.ADD_SYS' | translate }}</span>
|
||||
</div>
|
||||
<div class="mt-05" *ngIf="hasExpired">
|
||||
<span class="label label-warning">{{
|
||||
'CVE_ALLOWLIST.WARNING_SYS' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-row width-90per">
|
||||
<div class="position-relative pl-05">
|
||||
<div>
|
||||
<button
|
||||
id="show-add-modal-button"
|
||||
(click)="showAddModal = !showAddModal"
|
||||
class="btn btn-link">
|
||||
{{ 'CVE_ALLOWLIST.ADD' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="add-modal add-modal-dark"
|
||||
*ngIf="showAddModal">
|
||||
<clr-icon
|
||||
(click)="showAddModal = false"
|
||||
class="float-lg-right margin-top-4"
|
||||
shape="window-close"></clr-icon>
|
||||
<div>
|
||||
<clr-textarea-container
|
||||
class="flex-direction-column">
|
||||
<label>{{
|
||||
'CVE_ALLOWLIST.ENTER' | translate
|
||||
}}</label>
|
||||
<textarea
|
||||
id="allowlist-textarea"
|
||||
class="w-100 font-italic"
|
||||
clrTextarea
|
||||
[(ngModel)]="cveIds"
|
||||
name="cveIds"></textarea>
|
||||
<clr-control-helper>{{
|
||||
'CVE_ALLOWLIST.HELP' | translate
|
||||
}}</clr-control-helper>
|
||||
</clr-textarea-container>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
id="add-to-system"
|
||||
[disabled]="isDisabled()"
|
||||
(click)="addToSystemAllowlist()"
|
||||
class="btn btn-link">
|
||||
{{ 'CVE_ALLOWLIST.ADD' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="allowlist-window">
|
||||
<li
|
||||
*ngIf="systemAllowlist?.items?.length < 1"
|
||||
class="none">
|
||||
{{ 'CVE_ALLOWLIST.NONE' | translate }}
|
||||
</li>
|
||||
<li
|
||||
*ngFor="
|
||||
let item of systemAllowlist?.items;
|
||||
let i = index
|
||||
">
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
(click)="goToDetail(item.cve_id)"
|
||||
>{{ item.cve_id }}</a
|
||||
>
|
||||
<a
|
||||
class="float-lg-right"
|
||||
href="javascript:void(0)"
|
||||
(click)="deleteItem(i)">
|
||||
<clr-icon shape="times-circle"></clr-icon>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="clr-col padding-top-8 ml-1">
|
||||
<div class="clr-row expire-data">
|
||||
<label class="bottom-line clr-col-2">{{
|
||||
'CVE_ALLOWLIST.EXPIRES_AT' | translate
|
||||
}}</label>
|
||||
<div>
|
||||
<input
|
||||
#dateInput
|
||||
placeholder="{{
|
||||
'CVE_ALLOWLIST.NEVER_EXPIRES'
|
||||
| translate
|
||||
}}"
|
||||
readonly
|
||||
type="date"
|
||||
[(clrDate)]="expiresDate"
|
||||
newFormLayout="true" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-row">
|
||||
<label class="clr-col-2"></label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input
|
||||
[checked]="neverExpires"
|
||||
[(ngModel)]="neverExpires"
|
||||
type="checkbox"
|
||||
clrCheckbox
|
||||
name="neverExpires"
|
||||
id="neverExpires" />
|
||||
<label>
|
||||
{{
|
||||
'CVE_ALLOWLIST.NEVER_EXPIRES'
|
||||
| translate
|
||||
}}
|
||||
</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
id="security_save"
|
||||
class="btn btn-primary"
|
||||
(click)="save()"
|
||||
[disabled]="!hasAllowlistChanged || inProgress">
|
||||
{{ 'BUTTON.SAVE' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="security_cancel"
|
||||
class="btn btn-outline"
|
||||
(click)="cancel()"
|
||||
[disabled]="!hasAllowlistChanged || inProgress">
|
||||
{{ 'BUTTON.CANCEL' | translate }}
|
||||
</button>
|
||||
</div>
|
@ -0,0 +1,95 @@
|
||||
.clr-form-horizontal {
|
||||
.clr-form-control {
|
||||
& >.clr-control-label {
|
||||
width: 14rem;
|
||||
}
|
||||
}
|
||||
.flex-direction-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.bottom-line {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
.expire-data {
|
||||
min-width: 12.5rem;
|
||||
margin-top: -1rem;
|
||||
}
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
.pl-05 {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
.mt-1 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.d-f {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.font-size-13 {
|
||||
font-size: 13px;
|
||||
}
|
||||
.mt-05 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.margin-top-4 {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.allowlist-window {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
padding: 12px;
|
||||
height: 224px;
|
||||
width: 222px;
|
||||
overflow-y: auto;
|
||||
li {
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
list-style-type: none;
|
||||
}
|
||||
}
|
||||
|
||||
.width-90per {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.none {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.color-0079bb {
|
||||
color: #0079bb;
|
||||
}
|
||||
|
||||
.padding-top-8 {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.padding-left-80 {
|
||||
padding-left: 80px;
|
||||
}
|
||||
|
||||
.add-modal {
|
||||
position: absolute;
|
||||
padding: 0 8px;
|
||||
background-color: rgb(238, 238, 238);
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { SystemInfoService } from '../../../../shared/services';
|
||||
import { ErrorHandler } from '../../../../shared/units/error-handler';
|
||||
import { of } from 'rxjs';
|
||||
import { SharedTestingModule } from '../../../../shared/shared.module';
|
||||
import { SecurityComponent } from './security.component';
|
||||
describe('SecurityComponent', () => {
|
||||
let component: SecurityComponent;
|
||||
let fixture: ComponentFixture<SecurityComponent>;
|
||||
const mockedAllowlist = {
|
||||
id: 1,
|
||||
project_id: 1,
|
||||
expires_at: null,
|
||||
items: [{ cve_id: 'CVE-2019-1234' }],
|
||||
};
|
||||
const fakedSystemInfoService = {
|
||||
getSystemAllowlist() {
|
||||
return of(mockedAllowlist);
|
||||
},
|
||||
updateSystemAllowlist() {
|
||||
return of(true);
|
||||
},
|
||||
};
|
||||
const fakedErrorHandler = {
|
||||
info() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [SharedTestingModule],
|
||||
providers: [
|
||||
{ provide: ErrorHandler, useValue: fakedErrorHandler },
|
||||
{
|
||||
provide: SystemInfoService,
|
||||
useValue: fakedSystemInfoService,
|
||||
},
|
||||
],
|
||||
declarations: [SecurityComponent],
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SecurityComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('cancel button and save button should enable', async () => {
|
||||
component.systemAllowlist.items.push({ cve_id: 'CVE-2019-456' });
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
const cancel: HTMLButtonElement =
|
||||
fixture.nativeElement.querySelector('#security_cancel');
|
||||
expect(cancel.disabled).toBeFalse();
|
||||
const save: HTMLButtonElement =
|
||||
fixture.nativeElement.querySelector('#security_save');
|
||||
expect(save.disabled).toBeFalse();
|
||||
});
|
||||
it('save button should works', async () => {
|
||||
component.systemAllowlist.items[0].cve_id = 'CVE-2019-789';
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
const save: HTMLButtonElement =
|
||||
fixture.nativeElement.querySelector('#security_save');
|
||||
save.click();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(component.systemAllowlistOrigin.items[0].cve_id).toEqual(
|
||||
'CVE-2019-789'
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,221 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { clone, compareValue } from '../../../../shared/units/utils';
|
||||
import { ErrorHandler } from '../../../../shared/units/error-handler';
|
||||
import {
|
||||
ConfirmationState,
|
||||
ConfirmationTargets,
|
||||
} from '../../../../shared/entities/shared.const';
|
||||
import {
|
||||
SystemCVEAllowlist,
|
||||
SystemInfoService,
|
||||
} from '../../../../shared/services';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import { ConfirmationDialogService } from '../../../global-confirmation-dialog/confirmation-dialog.service';
|
||||
import { ConfirmationMessage } from '../../../global-confirmation-dialog/confirmation-message';
|
||||
|
||||
const ONE_THOUSAND: number = 1000;
|
||||
const CVE_DETAIL_PRE_URL = `https://nvd.nist.gov/vuln/detail/`;
|
||||
const TARGET_BLANK = '_blank';
|
||||
|
||||
@Component({
|
||||
selector: 'app-security',
|
||||
templateUrl: './security.component.html',
|
||||
styleUrls: ['./security.component.scss'],
|
||||
})
|
||||
export class SecurityComponent implements OnInit, OnDestroy {
|
||||
onGoing = false;
|
||||
systemAllowlist: SystemCVEAllowlist;
|
||||
systemAllowlistOrigin: SystemCVEAllowlist;
|
||||
cveIds: string;
|
||||
showAddModal: boolean = false;
|
||||
@ViewChild('dateInput') dateInput: ElementRef;
|
||||
private _confirmSub: Subscription;
|
||||
constructor(
|
||||
private errorHandler: ErrorHandler,
|
||||
private systemInfoService: SystemInfoService,
|
||||
private confirmService: ConfirmationDialogService
|
||||
) {}
|
||||
ngOnInit() {
|
||||
this.getSystemAllowlist();
|
||||
this.subscribeConfirmation();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
if (this._confirmSub) {
|
||||
this._confirmSub.unsubscribe();
|
||||
this._confirmSub = null;
|
||||
}
|
||||
}
|
||||
save(): void {
|
||||
if (!compareValue(this.systemAllowlistOrigin, this.systemAllowlist)) {
|
||||
this.onGoing = true;
|
||||
this.systemInfoService
|
||||
.updateSystemAllowlist(this.systemAllowlist)
|
||||
.pipe(finalize(() => (this.onGoing = false)))
|
||||
.subscribe({
|
||||
next: res => {
|
||||
this.systemAllowlistOrigin = clone(
|
||||
this.systemAllowlist
|
||||
);
|
||||
this.errorHandler.info('CONFIG.SAVE_SUCCESS');
|
||||
},
|
||||
error: err => {
|
||||
this.errorHandler.error(err);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Inprop situation, should not come here
|
||||
console.error('Save abort because nothing changed');
|
||||
}
|
||||
}
|
||||
|
||||
get inProgress(): boolean {
|
||||
return this.onGoing;
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
if (!compareValue(this.systemAllowlistOrigin, this.systemAllowlist)) {
|
||||
const msg = new ConfirmationMessage(
|
||||
'CONFIG.CONFIRM_TITLE',
|
||||
'CONFIG.CONFIRM_SUMMARY',
|
||||
'',
|
||||
null,
|
||||
ConfirmationTargets.CONFIG
|
||||
);
|
||||
this.confirmService.openComfirmDialog(msg);
|
||||
} else {
|
||||
// Invalid situation, should not come here
|
||||
console.error('Nothing changed');
|
||||
}
|
||||
}
|
||||
|
||||
subscribeConfirmation() {
|
||||
this._confirmSub = this.confirmService.confirmationConfirm$.subscribe(
|
||||
confirmation => {
|
||||
if (
|
||||
confirmation &&
|
||||
confirmation.state === ConfirmationState.CONFIRMED
|
||||
) {
|
||||
if (
|
||||
!compareValue(
|
||||
this.systemAllowlistOrigin,
|
||||
this.systemAllowlist
|
||||
)
|
||||
) {
|
||||
this.systemAllowlist = clone(
|
||||
this.systemAllowlistOrigin
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getSystemAllowlist() {
|
||||
this.onGoing = true;
|
||||
this.systemInfoService.getSystemAllowlist().subscribe(
|
||||
systemAllowlist => {
|
||||
this.onGoing = false;
|
||||
if (!systemAllowlist.items) {
|
||||
systemAllowlist.items = [];
|
||||
}
|
||||
if (!systemAllowlist.expires_at) {
|
||||
systemAllowlist.expires_at = null;
|
||||
}
|
||||
this.systemAllowlist = systemAllowlist;
|
||||
this.systemAllowlistOrigin = clone(systemAllowlist);
|
||||
},
|
||||
error => {
|
||||
this.onGoing = false;
|
||||
console.error(
|
||||
'An error occurred during getting systemAllowlist'
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
deleteItem(index: number) {
|
||||
this.systemAllowlist.items.splice(index, 1);
|
||||
}
|
||||
|
||||
addToSystemAllowlist() {
|
||||
// remove duplication and add to systemAllowlist
|
||||
let map = {};
|
||||
this.systemAllowlist.items.forEach(item => {
|
||||
map[item.cve_id] = true;
|
||||
});
|
||||
this.cveIds.split(/[\n,]+/).forEach(id => {
|
||||
let cveObj: any = {};
|
||||
cveObj.cve_id = id.trim();
|
||||
if (!map[cveObj.cve_id]) {
|
||||
map[cveObj.cve_id] = true;
|
||||
this.systemAllowlist.items.push(cveObj);
|
||||
}
|
||||
});
|
||||
// clear modal and close modal
|
||||
this.cveIds = null;
|
||||
this.showAddModal = false;
|
||||
}
|
||||
|
||||
get hasAllowlistChanged(): boolean {
|
||||
return !compareValue(this.systemAllowlistOrigin, this.systemAllowlist);
|
||||
}
|
||||
|
||||
isDisabled(): boolean {
|
||||
let str = this.cveIds;
|
||||
return !(str && str.trim());
|
||||
}
|
||||
|
||||
get expiresDate() {
|
||||
if (this.systemAllowlist && this.systemAllowlist.expires_at) {
|
||||
return new Date(this.systemAllowlist.expires_at * ONE_THOUSAND);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
set expiresDate(date) {
|
||||
if (this.systemAllowlist && date) {
|
||||
this.systemAllowlist.expires_at = Math.floor(
|
||||
date.getTime() / ONE_THOUSAND
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get neverExpires(): boolean {
|
||||
return !(this.systemAllowlist && this.systemAllowlist.expires_at);
|
||||
}
|
||||
|
||||
set neverExpires(flag) {
|
||||
if (flag) {
|
||||
this.systemAllowlist.expires_at = null;
|
||||
this.systemInfoService.resetDateInput(this.dateInput);
|
||||
} else {
|
||||
this.systemAllowlist.expires_at = Math.floor(
|
||||
new Date().getTime() / ONE_THOUSAND
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get hasExpired(): boolean {
|
||||
if (
|
||||
this.systemAllowlistOrigin &&
|
||||
this.systemAllowlistOrigin.expires_at
|
||||
) {
|
||||
return (
|
||||
new Date().getTime() >
|
||||
this.systemAllowlistOrigin.expires_at * ONE_THOUSAND
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
goToDetail(cveId) {
|
||||
window.open(CVE_DETAIL_PRE_URL + `${cveId}`, TARGET_BLANK);
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@
|
||||
clrSelect
|
||||
id="proCreation"
|
||||
name="proCreation"
|
||||
class="pro-creation"
|
||||
[(ngModel)]="currentConfig.project_creation_restriction.value"
|
||||
[disabled]="
|
||||
disabled(currentConfig.project_creation_restriction)
|
||||
@ -189,142 +190,7 @@
|
||||
(ngModelChange)="setRepoReadOnlyValue($event)" />
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="clr-form-control d-f">
|
||||
<label class="clr-control-label">{{
|
||||
'CVE_ALLOWLIST.DEPLOYMENT_SECURITY' | translate
|
||||
}}</label>
|
||||
<div class="form-content">
|
||||
<div class="font-size-13">
|
||||
<div class="mt-05">
|
||||
<span class="title font-size-13">{{
|
||||
'CVE_ALLOWLIST.CVE_ALLOWLIST' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="mt-05">
|
||||
<span>{{
|
||||
'CVE_ALLOWLIST.SYS_ALLOWLIST_EXPLAIN' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="mt-05">
|
||||
<span>{{ 'CVE_ALLOWLIST.ADD_SYS' | translate }}</span>
|
||||
</div>
|
||||
<div class="mt-05" *ngIf="hasExpired">
|
||||
<span class="label label-warning">{{
|
||||
'CVE_ALLOWLIST.WARNING_SYS' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-row width-90per">
|
||||
<div class="position-relative pl-05">
|
||||
<div>
|
||||
<button
|
||||
id="show-add-modal-button"
|
||||
(click)="showAddModal = !showAddModal"
|
||||
class="btn btn-link">
|
||||
{{ 'CVE_ALLOWLIST.ADD' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="add-modal add-modal-dark"
|
||||
*ngIf="showAddModal">
|
||||
<clr-icon
|
||||
(click)="showAddModal = false"
|
||||
class="float-lg-right margin-top-4"
|
||||
shape="window-close"></clr-icon>
|
||||
<div>
|
||||
<clr-textarea-container
|
||||
class="flex-direction-column">
|
||||
<label>{{
|
||||
'CVE_ALLOWLIST.ENTER' | translate
|
||||
}}</label>
|
||||
<textarea
|
||||
id="allowlist-textarea"
|
||||
class="w-100 font-italic"
|
||||
clrTextarea
|
||||
[(ngModel)]="cveIds"
|
||||
name="cveIds"></textarea>
|
||||
<clr-control-helper>{{
|
||||
'CVE_ALLOWLIST.HELP' | translate
|
||||
}}</clr-control-helper>
|
||||
</clr-textarea-container>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
id="add-to-system"
|
||||
[disabled]="isDisabled()"
|
||||
(click)="addToSystemAllowlist()"
|
||||
class="btn btn-link">
|
||||
{{ 'CVE_ALLOWLIST.ADD' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="allowlist-window">
|
||||
<li
|
||||
*ngIf="systemAllowlist?.items?.length < 1"
|
||||
class="none">
|
||||
{{ 'CVE_ALLOWLIST.NONE' | translate }}
|
||||
</li>
|
||||
<li
|
||||
*ngFor="
|
||||
let item of systemAllowlist?.items;
|
||||
let i = index
|
||||
">
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
(click)="goToDetail(item.cve_id)"
|
||||
>{{ item.cve_id }}</a
|
||||
>
|
||||
<a
|
||||
class="float-lg-right"
|
||||
href="javascript:void(0)"
|
||||
(click)="deleteItem(i)">
|
||||
<clr-icon shape="times-circle"></clr-icon>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="clr-col padding-top-8">
|
||||
<div class="clr-row expire-data">
|
||||
<label class="bottom-line clr-col-4">{{
|
||||
'CVE_ALLOWLIST.EXPIRES_AT' | translate
|
||||
}}</label>
|
||||
<div>
|
||||
<input
|
||||
#dateInput
|
||||
placeholder="{{
|
||||
'CVE_ALLOWLIST.NEVER_EXPIRES'
|
||||
| translate
|
||||
}}"
|
||||
readonly
|
||||
type="date"
|
||||
[(clrDate)]="expiresDate"
|
||||
newFormLayout="true" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-row">
|
||||
<label class="clr-col-4"></label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input
|
||||
[checked]="neverExpires"
|
||||
[(ngModel)]="neverExpires"
|
||||
type="checkbox"
|
||||
clrCheckbox
|
||||
name="neverExpires"
|
||||
id="neverExpires" />
|
||||
<label>
|
||||
{{
|
||||
'CVE_ALLOWLIST.NEVER_EXPIRES'
|
||||
| translate
|
||||
}}
|
||||
</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<clr-checkbox-container>
|
||||
<clr-checkbox-container class="center">
|
||||
<label for="webhookNotificationEnabled"
|
||||
>{{ 'CONFIG.WEBHOOK_NOTIFICATION_ENABLED' | translate }}
|
||||
<clr-tooltip>
|
||||
@ -353,6 +219,67 @@
|
||||
[disabled]="!currentConfig.notification_enable.editable" />
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
<clr-input-container>
|
||||
<label for="auditLogForwardEndpoint">
|
||||
{{ 'CLEARANCES.FORWARD_ENDPOINT' | translate }}
|
||||
<clr-tooltip>
|
||||
<clr-icon
|
||||
clrTooltipTrigger
|
||||
shape="info-circle"
|
||||
size="24"></clr-icon>
|
||||
<clr-tooltip-content
|
||||
clrPosition="top-right"
|
||||
clrSize="lg"
|
||||
*clrIfOpen>
|
||||
<span>{{
|
||||
'CLEARANCES.FORWARD_ENDPOINT_TOOLTIP' | translate
|
||||
}}</span>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
</label>
|
||||
<input
|
||||
clrInput
|
||||
name="auditLogForwardEndpoint"
|
||||
type="text"
|
||||
[(ngModel)]="currentConfig.audit_log_forward_endpoint.value"
|
||||
id="auditLogForwardEndpoint"
|
||||
size="20"
|
||||
(input)="checkAuditLogForwardEndpoint($event)"
|
||||
[disabled]="
|
||||
!currentConfig?.audit_log_forward_endpoint?.editable
|
||||
" />
|
||||
</clr-input-container>
|
||||
<clr-checkbox-container class="center">
|
||||
<label for="skipAuditLogDatabase"
|
||||
>{{ 'CLEARANCES.SKIP_DATABASE' | translate }}
|
||||
<clr-tooltip>
|
||||
<clr-icon
|
||||
clrTooltipTrigger
|
||||
shape="info-circle"
|
||||
size="24"></clr-icon>
|
||||
<clr-tooltip-content
|
||||
clrPosition="top-right"
|
||||
clrSize="lg"
|
||||
*clrIfOpen>
|
||||
<span>{{
|
||||
'CLEARANCES.SKIP_DATABASE_TOOLTIP' | translate
|
||||
}}</span>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input
|
||||
type="checkbox"
|
||||
clrCheckbox
|
||||
name="skipAuditLogDatabase"
|
||||
id="skipAuditLogDatabase"
|
||||
[(ngModel)]="currentConfig.skip_audit_log_database.value"
|
||||
[disabled]="
|
||||
!currentConfig.skip_audit_log_database?.editable ||
|
||||
!currentConfig.audit_log_forward_endpoint?.value
|
||||
" />
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
</section>
|
||||
</form>
|
||||
<div>
|
||||
@ -361,10 +288,7 @@
|
||||
id="config_system_save"
|
||||
class="btn btn-primary"
|
||||
(click)="save()"
|
||||
[disabled]="
|
||||
((!isValid() || !hasChanges()) && !hasAllowlistChanged) ||
|
||||
inProgress
|
||||
">
|
||||
[disabled]="!isValid() || !hasChanges() || inProgress">
|
||||
{{ 'BUTTON.SAVE' | translate }}
|
||||
</button>
|
||||
<button
|
||||
@ -372,10 +296,7 @@
|
||||
id="config_system_cancel"
|
||||
class="btn btn-outline"
|
||||
(click)="cancel()"
|
||||
[disabled]="
|
||||
((!isValid() || !hasChanges()) && !hasAllowlistChanged) ||
|
||||
inProgress
|
||||
">
|
||||
[disabled]="!isValid() || !hasChanges() || inProgress">
|
||||
{{ 'BUTTON.CANCEL' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -6,7 +6,7 @@
|
||||
.clr-form-horizontal {
|
||||
.clr-form-control {
|
||||
& >.clr-control-label {
|
||||
width: 12rem;
|
||||
width: 14rem;
|
||||
}
|
||||
}
|
||||
.flex-direction-column {
|
||||
@ -136,3 +136,13 @@
|
||||
.margin-top-3px {
|
||||
margin-top: 3px;
|
||||
}
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.clr-input {
|
||||
width: 12rem;
|
||||
}
|
||||
.pro-creation {
|
||||
width: 12rem;
|
||||
}
|
||||
|
@ -1,41 +1,21 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
ComponentFixtureAutoDetect,
|
||||
TestBed,
|
||||
} from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { SystemSettingsComponent } from './system-settings.component';
|
||||
import { SystemInfoService } from '../../../../shared/services';
|
||||
import { ErrorHandler } from '../../../../shared/units/error-handler';
|
||||
import { of } from 'rxjs';
|
||||
import { Configuration, StringValueItem } from '../config';
|
||||
import { Configuration, NumberValueItem, StringValueItem } from '../config';
|
||||
import { SharedTestingModule } from '../../../../shared/shared.module';
|
||||
import { ConfigService } from '../config.service';
|
||||
import { AppConfigService } from '../../../../services/app-config.service';
|
||||
|
||||
describe('SystemSettingsComponent', () => {
|
||||
let component: SystemSettingsComponent;
|
||||
let fixture: ComponentFixture<SystemSettingsComponent>;
|
||||
const mockedAllowlist = {
|
||||
id: 1,
|
||||
project_id: 1,
|
||||
expires_at: null,
|
||||
items: [{ cve_id: 'CVE-2019-1234' }],
|
||||
};
|
||||
const fakedSystemInfoService = {
|
||||
getSystemAllowlist() {
|
||||
return of(mockedAllowlist);
|
||||
},
|
||||
getSystemInfo() {
|
||||
return of({});
|
||||
},
|
||||
updateSystemAllowlist() {
|
||||
return of(true);
|
||||
},
|
||||
};
|
||||
const fakedErrorHandler = {
|
||||
info() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const fakeConfigService = {
|
||||
config: new Configuration(),
|
||||
getConfig() {
|
||||
@ -53,6 +33,9 @@ describe('SystemSettingsComponent', () => {
|
||||
confirmUnsavedChanges() {},
|
||||
updateConfig() {},
|
||||
resetConfig() {},
|
||||
saveConfiguration() {
|
||||
return of(null);
|
||||
},
|
||||
};
|
||||
const fakedAppConfigService = {
|
||||
getConfig() {
|
||||
@ -69,12 +52,6 @@ describe('SystemSettingsComponent', () => {
|
||||
{ provide: AppConfigService, useValue: fakedAppConfigService },
|
||||
{ provide: ConfigService, useValue: fakeConfigService },
|
||||
{ provide: ErrorHandler, useValue: fakedErrorHandler },
|
||||
{
|
||||
provide: SystemInfoService,
|
||||
useValue: fakedSystemInfoService,
|
||||
},
|
||||
// open auto detect
|
||||
{ provide: ComponentFixtureAutoDetect, useValue: true },
|
||||
],
|
||||
declarations: [SystemSettingsComponent],
|
||||
});
|
||||
@ -82,46 +59,30 @@ describe('SystemSettingsComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SystemSettingsComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.currentConfig.auth_mode = new StringValueItem(
|
||||
'db_auth',
|
||||
false
|
||||
);
|
||||
fixture.detectChanges();
|
||||
fixture.autoDetectChanges(true);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('cancel button should works', () => {
|
||||
const spy: jasmine.Spy = spyOn(
|
||||
fakeConfigService,
|
||||
'confirmUnsavedChanges'
|
||||
).and.returnValue(undefined);
|
||||
component.systemAllowlist.items.push({ cve_id: 'CVE-2019-456' });
|
||||
const readOnly: HTMLElement =
|
||||
fixture.nativeElement.querySelector('#repoReadOnly');
|
||||
readOnly.click();
|
||||
fixture.detectChanges();
|
||||
it('cancel button should work', () => {
|
||||
const spy: jasmine.Spy = spyOn(component, 'cancel').and.returnValue(
|
||||
undefined
|
||||
);
|
||||
const cancel: HTMLButtonElement = fixture.nativeElement.querySelector(
|
||||
'#config_system_cancel'
|
||||
);
|
||||
cancel.click();
|
||||
fixture.detectChanges();
|
||||
cancel.dispatchEvent(new Event('click'));
|
||||
expect(spy.calls.count()).toEqual(1);
|
||||
});
|
||||
it('save button should works', () => {
|
||||
component.systemAllowlist.items[0].cve_id = 'CVE-2019-789';
|
||||
const readOnly: HTMLElement =
|
||||
fixture.nativeElement.querySelector('#repoReadOnly');
|
||||
readOnly.click();
|
||||
fixture.detectChanges();
|
||||
it('save button should work', () => {
|
||||
const input = fixture.nativeElement.querySelector('#robotNamePrefix');
|
||||
input.value = 'test';
|
||||
input.dispatchEvent(new Event('input'));
|
||||
const save: HTMLButtonElement = fixture.nativeElement.querySelector(
|
||||
'#config_system_save'
|
||||
);
|
||||
save.click();
|
||||
fixture.detectChanges();
|
||||
expect(component.systemAllowlistOrigin.items[0].cve_id).toEqual(
|
||||
'CVE-2019-789'
|
||||
);
|
||||
save.dispatchEvent(new Event('click'));
|
||||
expect(input.value).toEqual('test');
|
||||
});
|
||||
});
|
||||
|
@ -1,32 +1,15 @@
|
||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
import { Configuration } from '../config';
|
||||
import {
|
||||
clone,
|
||||
compareValue,
|
||||
CURRENT_BASE_HREF,
|
||||
getChanges,
|
||||
isEmpty,
|
||||
} from '../../../../shared/units/utils';
|
||||
import { ErrorHandler } from '../../../../shared/units/error-handler';
|
||||
import {
|
||||
ConfirmationState,
|
||||
ConfirmationTargets,
|
||||
} from '../../../../shared/entities/shared.const';
|
||||
import { ConfirmationAcknowledgement } from '../../../global-confirmation-dialog/confirmation-state-message';
|
||||
import {
|
||||
SystemCVEAllowlist,
|
||||
SystemInfo,
|
||||
SystemInfoService,
|
||||
} from '../../../../shared/services';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { ConfigurationService } from '../../../../services/config.service';
|
||||
import { ConfigService } from '../config.service';
|
||||
import { AppConfigService } from '../../../../services/app-config.service';
|
||||
|
||||
const ONE_THOUSAND: number = 1000;
|
||||
const CVE_DETAIL_PRE_URL = `https://nvd.nist.gov/vuln/detail/`;
|
||||
const TARGET_BLANK = '_blank';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'system-settings',
|
||||
@ -36,11 +19,6 @@ const TARGET_BLANK = '_blank';
|
||||
export class SystemSettingsComponent implements OnInit {
|
||||
onGoing = false;
|
||||
downloadLink: string;
|
||||
systemAllowlist: SystemCVEAllowlist;
|
||||
systemAllowlistOrigin: SystemCVEAllowlist;
|
||||
cveIds: string;
|
||||
showAddModal: boolean = false;
|
||||
systemInfo: SystemInfo;
|
||||
get currentConfig(): Configuration {
|
||||
return this.conf.getConfig();
|
||||
}
|
||||
@ -49,7 +27,18 @@ export class SystemSettingsComponent implements OnInit {
|
||||
this.conf.setConfig(cfg);
|
||||
}
|
||||
@ViewChild('systemConfigFrom') systemSettingsForm: NgForm;
|
||||
@ViewChild('dateInput') dateInput: ElementRef;
|
||||
|
||||
constructor(
|
||||
private appConfigService: AppConfigService,
|
||||
private errorHandler: ErrorHandler,
|
||||
private conf: ConfigService
|
||||
) {
|
||||
this.downloadLink = CURRENT_BASE_HREF + '/systeminfo/getcert';
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.conf.resetConfig();
|
||||
}
|
||||
|
||||
get editable(): boolean {
|
||||
return (
|
||||
@ -121,7 +110,9 @@ export class SystemSettingsComponent implements OnInit {
|
||||
prop === 'project_creation_restriction' ||
|
||||
prop === 'robot_token_duration' ||
|
||||
prop === 'notification_enable' ||
|
||||
prop === 'robot_name_prefix'
|
||||
prop === 'robot_name_prefix' ||
|
||||
prop === 'audit_log_forward_endpoint' ||
|
||||
prop === 'skip_audit_log_database'
|
||||
) {
|
||||
changes[prop] = allChanges[prop];
|
||||
}
|
||||
@ -153,82 +144,41 @@ export class SystemSettingsComponent implements OnInit {
|
||||
*/
|
||||
public save(): void {
|
||||
let changes = this.getChanges();
|
||||
if (
|
||||
!isEmpty(changes) ||
|
||||
!compareValue(this.systemAllowlistOrigin, this.systemAllowlist)
|
||||
) {
|
||||
if (!isEmpty(changes)) {
|
||||
this.onGoing = true;
|
||||
let observables = [];
|
||||
if (!isEmpty(changes)) {
|
||||
observables.push(this.configService.saveConfiguration(changes));
|
||||
}
|
||||
if (
|
||||
!compareValue(this.systemAllowlistOrigin, this.systemAllowlist)
|
||||
) {
|
||||
observables.push(
|
||||
this.systemInfoService.updateSystemAllowlist(
|
||||
this.systemAllowlist
|
||||
)
|
||||
);
|
||||
}
|
||||
forkJoin(observables).subscribe(
|
||||
result => {
|
||||
this.onGoing = false;
|
||||
if (!isEmpty(changes)) {
|
||||
// API should return the updated configurations here
|
||||
// Unfortunately API does not do that
|
||||
// To refresh the view, we can clone the original data copy
|
||||
// or force refresh by calling service.
|
||||
// HERE we choose force way
|
||||
this.conf.updateConfig();
|
||||
// Reload bootstrap option
|
||||
this.appConfigService.load().subscribe(
|
||||
() => {},
|
||||
error =>
|
||||
console.error(
|
||||
'Failed to reload bootstrap option with error: ',
|
||||
error
|
||||
)
|
||||
);
|
||||
}
|
||||
if (
|
||||
!compareValue(
|
||||
this.systemAllowlistOrigin,
|
||||
this.systemAllowlist
|
||||
)
|
||||
) {
|
||||
this.systemAllowlistOrigin = clone(
|
||||
this.systemAllowlist
|
||||
);
|
||||
}
|
||||
this.errorHandler.info('CONFIG.SAVE_SUCCESS');
|
||||
},
|
||||
error => {
|
||||
this.onGoing = false;
|
||||
this.errorHandler.error(error);
|
||||
}
|
||||
);
|
||||
this.conf
|
||||
.saveConfiguration(changes)
|
||||
.pipe(finalize(() => (this.onGoing = false)))
|
||||
.subscribe({
|
||||
next: result => {
|
||||
if (!isEmpty(changes)) {
|
||||
// API should return the updated configurations here
|
||||
// Unfortunately API does not do that
|
||||
// To refresh the view, we can clone the original data copy
|
||||
// or force refresh by calling service.
|
||||
// HERE we choose force way
|
||||
this.conf.updateConfig();
|
||||
// Reload bootstrap option
|
||||
this.appConfigService.load().subscribe(
|
||||
() => {},
|
||||
error =>
|
||||
console.error(
|
||||
'Failed to reload bootstrap option with error: ',
|
||||
error
|
||||
)
|
||||
);
|
||||
}
|
||||
this.errorHandler.info('CONFIG.SAVE_SUCCESS');
|
||||
},
|
||||
error: error => {
|
||||
this.errorHandler.error(error);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Inprop situation, should not come here
|
||||
console.error('Save abort because nothing changed');
|
||||
}
|
||||
}
|
||||
|
||||
confirmCancel(ack: ConfirmationAcknowledgement): void {
|
||||
if (
|
||||
ack &&
|
||||
ack.source === ConfirmationTargets.CONFIG &&
|
||||
ack.state === ConfirmationState.CONFIRMED
|
||||
) {
|
||||
this.conf.resetConfig();
|
||||
if (
|
||||
!compareValue(this.systemAllowlistOrigin, this.systemAllowlist)
|
||||
) {
|
||||
this.systemAllowlist = clone(this.systemAllowlistOrigin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get inProgress(): boolean {
|
||||
return this.onGoing || this.conf.getLoadingConfigStatus();
|
||||
}
|
||||
@ -241,10 +191,7 @@ export class SystemSettingsComponent implements OnInit {
|
||||
*/
|
||||
public cancel(): void {
|
||||
let changes = this.getChanges();
|
||||
if (
|
||||
!isEmpty(changes) ||
|
||||
!compareValue(this.systemAllowlistOrigin, this.systemAllowlist)
|
||||
) {
|
||||
if (!isEmpty(changes)) {
|
||||
this.conf.confirmUnsavedChanges(changes);
|
||||
} else {
|
||||
// Invalid situation, should not come here
|
||||
@ -252,129 +199,9 @@ export class SystemSettingsComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private appConfigService: AppConfigService,
|
||||
private configService: ConfigurationService,
|
||||
private errorHandler: ErrorHandler,
|
||||
private systemInfoService: SystemInfoService,
|
||||
private conf: ConfigService
|
||||
) {
|
||||
this.downloadLink = CURRENT_BASE_HREF + '/systeminfo/getcert';
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.conf.resetConfig();
|
||||
this.getSystemAllowlist();
|
||||
this.getSystemInfo();
|
||||
}
|
||||
|
||||
getSystemInfo() {
|
||||
this.systemInfoService.getSystemInfo().subscribe(
|
||||
systemInfo => (this.systemInfo = systemInfo),
|
||||
error => this.errorHandler.error(error)
|
||||
);
|
||||
}
|
||||
|
||||
getSystemAllowlist() {
|
||||
this.onGoing = true;
|
||||
this.systemInfoService.getSystemAllowlist().subscribe(
|
||||
systemAllowlist => {
|
||||
this.onGoing = false;
|
||||
if (!systemAllowlist.items) {
|
||||
systemAllowlist.items = [];
|
||||
}
|
||||
if (!systemAllowlist.expires_at) {
|
||||
systemAllowlist.expires_at = null;
|
||||
}
|
||||
this.systemAllowlist = systemAllowlist;
|
||||
this.systemAllowlistOrigin = clone(systemAllowlist);
|
||||
},
|
||||
error => {
|
||||
this.onGoing = false;
|
||||
console.error(
|
||||
'An error occurred during getting systemAllowlist'
|
||||
);
|
||||
// this.errorHandler.error(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
deleteItem(index: number) {
|
||||
this.systemAllowlist.items.splice(index, 1);
|
||||
}
|
||||
|
||||
addToSystemAllowlist() {
|
||||
// remove duplication and add to systemAllowlist
|
||||
let map = {};
|
||||
this.systemAllowlist.items.forEach(item => {
|
||||
map[item.cve_id] = true;
|
||||
});
|
||||
this.cveIds.split(/[\n,]+/).forEach(id => {
|
||||
let cveObj: any = {};
|
||||
cveObj.cve_id = id.trim();
|
||||
if (!map[cveObj.cve_id]) {
|
||||
map[cveObj.cve_id] = true;
|
||||
this.systemAllowlist.items.push(cveObj);
|
||||
}
|
||||
});
|
||||
// clear modal and close modal
|
||||
this.cveIds = null;
|
||||
this.showAddModal = false;
|
||||
}
|
||||
|
||||
get hasAllowlistChanged(): boolean {
|
||||
return !compareValue(this.systemAllowlistOrigin, this.systemAllowlist);
|
||||
}
|
||||
|
||||
isDisabled(): boolean {
|
||||
let str = this.cveIds;
|
||||
return !(str && str.trim());
|
||||
}
|
||||
|
||||
get expiresDate() {
|
||||
if (this.systemAllowlist && this.systemAllowlist.expires_at) {
|
||||
return new Date(this.systemAllowlist.expires_at * ONE_THOUSAND);
|
||||
checkAuditLogForwardEndpoint(e: any) {
|
||||
if (!e?.target?.value) {
|
||||
this.currentConfig.skip_audit_log_database.value = false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
set expiresDate(date) {
|
||||
if (this.systemAllowlist && date) {
|
||||
this.systemAllowlist.expires_at = Math.floor(
|
||||
date.getTime() / ONE_THOUSAND
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get neverExpires(): boolean {
|
||||
return !(this.systemAllowlist && this.systemAllowlist.expires_at);
|
||||
}
|
||||
|
||||
set neverExpires(flag) {
|
||||
if (flag) {
|
||||
this.systemAllowlist.expires_at = null;
|
||||
this.systemInfoService.resetDateInput(this.dateInput);
|
||||
} else {
|
||||
this.systemAllowlist.expires_at = Math.floor(
|
||||
new Date().getTime() / ONE_THOUSAND
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get hasExpired(): boolean {
|
||||
if (
|
||||
this.systemAllowlistOrigin &&
|
||||
this.systemAllowlistOrigin.expires_at
|
||||
) {
|
||||
return (
|
||||
new Date().getTime() >
|
||||
this.systemAllowlistOrigin.expires_at * ONE_THOUSAND
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
goToDetail(cveId) {
|
||||
window.open(CVE_DETAIL_PRE_URL + `${cveId}`, TARGET_BLANK);
|
||||
}
|
||||
}
|
||||
|
@ -1,33 +0,0 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<h2 class="custom-h2 gc-title">
|
||||
{{ 'CONFIG.GC' | translate
|
||||
}}<span
|
||||
*ngIf="inProgress"
|
||||
class="spinner spinner-inline ml-1 v-mid"></span>
|
||||
</h2>
|
||||
<clr-tabs>
|
||||
<clr-tab *ngIf="hasAdminRole">
|
||||
<button id="config-gc" clrTabLink>
|
||||
{{ 'CONFIG.GC' | translate }}
|
||||
</button>
|
||||
<ng-template [(clrIfActive)]="gcActive">
|
||||
<clr-tab-content id="gc" *ngIf="hasAdminRole">
|
||||
<gc-config
|
||||
(loadingGcStatus)="getGcStatus($event)"></gc-config>
|
||||
</clr-tab-content>
|
||||
</ng-template>
|
||||
</clr-tab>
|
||||
<clr-tab *ngIf="hasAdminRole">
|
||||
<button id="gc-log" clrTabLink>
|
||||
{{ 'CONFIG.HISTORY' | translate }}
|
||||
</button>
|
||||
<ng-template [(clrIfActive)]="historyActive">
|
||||
<clr-tab-content id="history" *ngIf="hasAdminRole">
|
||||
<gc-history></gc-history>
|
||||
</clr-tab-content>
|
||||
</ng-template>
|
||||
</clr-tab>
|
||||
</clr-tabs>
|
||||
</div>
|
||||
</div>
|
@ -1,6 +0,0 @@
|
||||
.gc-title {
|
||||
display: inline-block;
|
||||
}
|
||||
.v-mid {
|
||||
vertical-align: middle;
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { SessionService } from '../../../shared/services/session.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-gc-page',
|
||||
templateUrl: './gc-page.component.html',
|
||||
styleUrls: ['./gc-page.component.scss'],
|
||||
})
|
||||
export class GcPageComponent {
|
||||
inProgress: boolean = true;
|
||||
constructor(private session: SessionService) {}
|
||||
|
||||
public get hasAdminRole(): boolean {
|
||||
return (
|
||||
this.session.getCurrentUser() &&
|
||||
this.session.getCurrentUser().has_admin_role
|
||||
);
|
||||
}
|
||||
|
||||
getGcStatus(status: boolean) {
|
||||
this.inProgress = status;
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { GcPageComponent } from './gc-page.component';
|
||||
import { GcComponent } from './gc/gc.component';
|
||||
import { GcHistoryComponent } from './gc/gc-history/gc-history.component';
|
||||
import { SharedModule } from '../../../shared/shared.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: GcPageComponent,
|
||||
},
|
||||
];
|
||||
@NgModule({
|
||||
imports: [SharedModule, RouterModule.forChild(routes)],
|
||||
declarations: [GcPageComponent, GcComponent, GcHistoryComponent],
|
||||
})
|
||||
export class GcModule {}
|
@ -1,52 +0,0 @@
|
||||
<div class="cron-selection">
|
||||
<cron-selection
|
||||
[labelCurrent]="getLabelCurrent"
|
||||
#CronScheduleComponent
|
||||
[labelEdit]="getLabelCurrent"
|
||||
[originCron]="originCron"
|
||||
(inputvalue)="saveGcSchedule($event)"></cron-selection>
|
||||
</div>
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-2 flex-200"></div>
|
||||
<div class="clr-col">
|
||||
<span class="explain">{{ 'GC.EXPLAIN' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-2 flex-200"></div>
|
||||
<div class="clr-col">
|
||||
<clr-toggle-container class="mt-05">
|
||||
<clr-toggle-wrapper>
|
||||
<input
|
||||
type="checkbox"
|
||||
clrToggle
|
||||
name="delete_untagged"
|
||||
id="delete_untagged"
|
||||
[(ngModel)]="shouldDeleteUntagged" />
|
||||
<label class="font-weight-400" for="delete_untagged">{{
|
||||
'GC.DELETE_UNTAGGED' | translate
|
||||
}}</label>
|
||||
</clr-toggle-wrapper>
|
||||
</clr-toggle-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-2 flex-200">
|
||||
<button
|
||||
id="gc-now"
|
||||
class="btn btn-primary gc-start-btn"
|
||||
(click)="gcNow()"
|
||||
[disabled]="disableGC">
|
||||
{{ 'GC.GC_NOW' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clr-col">
|
||||
<button
|
||||
id="gc-dry-run"
|
||||
class="btn btn-outline gc-start-btn"
|
||||
(click)="dryRun()"
|
||||
[disabled]="dryRunOnGoing">
|
||||
{{ 'TAG_RETENTION.WHAT_IF_RUN' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
@ -136,7 +136,11 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-link" (click)="save()" id="config-save">
|
||||
<button
|
||||
class="btn btn-link"
|
||||
[disabled]="!externalValidation"
|
||||
(click)="save()"
|
||||
id="config-save">
|
||||
{{ 'BUTTON.SAVE' | translate }}
|
||||
</button>
|
||||
<button class="btn btn-link" (click)="isEditMode = false">
|
||||
|
@ -5,8 +5,6 @@
|
||||
|
||||
.font-style {
|
||||
display: inline-block;
|
||||
color: #000;
|
||||
font-size: .541667rem;
|
||||
}
|
||||
|
||||
span.required {
|
||||
|
@ -25,6 +25,7 @@ const PREFIX: string = '0 ';
|
||||
styleUrls: ['./cron-schedule.component.scss'],
|
||||
})
|
||||
export class CronScheduleComponent implements OnChanges {
|
||||
@Input() externalValidation: boolean = true; //extra check
|
||||
@Input() isInlineModel: boolean = false;
|
||||
@Input() originCron: OriginCron;
|
||||
@Input() labelEdit: string;
|
||||
@ -46,7 +47,7 @@ export class CronScheduleComponent implements OnChanges {
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
let cronChange: SimpleChange = changes['originCron'];
|
||||
if (cronChange.currentValue) {
|
||||
if (cronChange?.currentValue) {
|
||||
this.originScheduleType = cronChange.currentValue.type;
|
||||
this.oriCron = cronChange.currentValue.cron;
|
||||
}
|
||||
|
@ -1727,5 +1727,28 @@
|
||||
"CO_SIGN": "Cosign",
|
||||
"NOTARY": "Notary",
|
||||
"PLACEHOLDER": "Es konnten keine Anhänge gefunden werden!"
|
||||
},
|
||||
"CLEARANCES": {
|
||||
"CLEARANCES": "Clean Up",
|
||||
"AUDIT_LOG": "Log Rotation",
|
||||
"LAST_COMPLETED": "Last completed",
|
||||
"NEXT_SCHEDULED_TIME": "Next scheduled time",
|
||||
"SCHEDULE_TO_PURGE": "Schedule to purge",
|
||||
"KEEP_IN": "Keep records in",
|
||||
"KEEP_IN_TOOLTIP": "Keep the records in this interval",
|
||||
"KEEP_IN_ERROR": "This filed is required",
|
||||
"DAYS": "Days",
|
||||
"HOURS": "Hours",
|
||||
"INCLUDED_OPERATIONS": "Included operations",
|
||||
"INCLUDED_OPERATION_TOOLTIP": "Remove audit logs for the selected operations",
|
||||
"INCLUDED_OPERATION_ERROR": "Please select at lease one operation",
|
||||
"PURGE_NOW": "PURGE NOW",
|
||||
"PURGE_NOW_SUCCESS": "Purge triggered successfully",
|
||||
"PURGE_SCHEDULE_RESET": "Purge schedule has been reset",
|
||||
"PURGE_HISTORY": "Purge History",
|
||||
"FORWARD_ENDPOINT": "Audit Log Forward Endpoint",
|
||||
"FORWARD_ENDPOINT_TOOLTIP": "Forward audit logs to the syslog endpoint, for example: harbor-log:10514",
|
||||
"SKIP_DATABASE": "Skip Audit Log Database",
|
||||
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured"
|
||||
}
|
||||
}
|
||||
|
@ -1727,5 +1727,28 @@
|
||||
"CO_SIGN": "Cosign",
|
||||
"NOTARY": "Notary",
|
||||
"PLACEHOLDER": "We couldn't find any accessories!"
|
||||
},
|
||||
"CLEARANCES": {
|
||||
"CLEARANCES": "Clean Up",
|
||||
"AUDIT_LOG": "Log Rotation",
|
||||
"LAST_COMPLETED": "Last completed",
|
||||
"NEXT_SCHEDULED_TIME": "Next scheduled time",
|
||||
"SCHEDULE_TO_PURGE": "Schedule to purge",
|
||||
"KEEP_IN": "Keep records in",
|
||||
"KEEP_IN_TOOLTIP": "Keep the records in this interval",
|
||||
"KEEP_IN_ERROR": "This filed is required",
|
||||
"DAYS": "Days",
|
||||
"HOURS": "Hours",
|
||||
"INCLUDED_OPERATIONS": "Included operations",
|
||||
"INCLUDED_OPERATION_TOOLTIP": "Remove audit logs for the selected operations",
|
||||
"INCLUDED_OPERATION_ERROR": "Please select at lease one operation",
|
||||
"PURGE_NOW": "PURGE NOW",
|
||||
"PURGE_NOW_SUCCESS": "Purge triggered successfully",
|
||||
"PURGE_SCHEDULE_RESET": "Purge schedule has been reset",
|
||||
"PURGE_HISTORY": "Purge History",
|
||||
"FORWARD_ENDPOINT": "Audit Log Forward Endpoint",
|
||||
"FORWARD_ENDPOINT_TOOLTIP": "Forward audit logs to the syslog endpoint, for example: harbor-log:10514",
|
||||
"SKIP_DATABASE": "Skip Audit Log Database",
|
||||
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured"
|
||||
}
|
||||
}
|
||||
|
@ -1726,5 +1726,28 @@
|
||||
"CO_SIGN": "Cosign",
|
||||
"NOTARY": "Notary",
|
||||
"PLACEHOLDER": "We couldn't find any accessories!"
|
||||
},
|
||||
"CLEARANCES": {
|
||||
"CLEARANCES": "Clean Up",
|
||||
"AUDIT_LOG": "Log Rotation",
|
||||
"LAST_COMPLETED": "Last completed",
|
||||
"NEXT_SCHEDULED_TIME": "Next scheduled time",
|
||||
"SCHEDULE_TO_PURGE": "Schedule to purge",
|
||||
"KEEP_IN": "Keep records in",
|
||||
"KEEP_IN_TOOLTIP": "Keep the records in this interval",
|
||||
"KEEP_IN_ERROR": "This filed is required",
|
||||
"DAYS": "Days",
|
||||
"HOURS": "Hours",
|
||||
"INCLUDED_OPERATIONS": "Included operations",
|
||||
"INCLUDED_OPERATION_TOOLTIP": "Remove audit logs for the selected operations",
|
||||
"INCLUDED_OPERATION_ERROR": "Please select at lease one operation",
|
||||
"PURGE_NOW": "PURGE NOW",
|
||||
"PURGE_NOW_SUCCESS": "Purge triggered successfully",
|
||||
"PURGE_SCHEDULE_RESET": "Purge schedule has been reset",
|
||||
"PURGE_HISTORY": "Purge History",
|
||||
"FORWARD_ENDPOINT": "Audit Log Forward Endpoint",
|
||||
"FORWARD_ENDPOINT_TOOLTIP": "Forward audit logs to the syslog endpoint, for example: harbor-log:10514",
|
||||
"SKIP_DATABASE": "Skip Audit Log Database",
|
||||
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured"
|
||||
}
|
||||
}
|
||||
|
@ -1696,5 +1696,28 @@
|
||||
"CO_SIGN": "Cosign",
|
||||
"NOTARY": "Notary",
|
||||
"PLACEHOLDER": "Nous n'avons trouvé aucun accessoire !"
|
||||
},
|
||||
"CLEARANCES": {
|
||||
"CLEARANCES": "Clean Up",
|
||||
"AUDIT_LOG": "Log Rotation",
|
||||
"LAST_COMPLETED": "Last completed",
|
||||
"NEXT_SCHEDULED_TIME": "Next scheduled time",
|
||||
"SCHEDULE_TO_PURGE": "Schedule to purge",
|
||||
"KEEP_IN": "Keep records in",
|
||||
"KEEP_IN_TOOLTIP": "Keep the records in this interval",
|
||||
"KEEP_IN_ERROR": "This filed is required",
|
||||
"DAYS": "Days",
|
||||
"HOURS": "Hours",
|
||||
"INCLUDED_OPERATIONS": "Included operations",
|
||||
"INCLUDED_OPERATION_TOOLTIP": "Remove audit logs for the selected operations",
|
||||
"INCLUDED_OPERATION_ERROR": "Please select at lease one operation",
|
||||
"PURGE_NOW": "PURGE NOW",
|
||||
"PURGE_NOW_SUCCESS": "Purge triggered successfully",
|
||||
"PURGE_SCHEDULE_RESET": "Purge schedule has been reset",
|
||||
"PURGE_HISTORY": "Purge History",
|
||||
"FORWARD_ENDPOINT": "Audit Log Forward Endpoint",
|
||||
"FORWARD_ENDPOINT_TOOLTIP": "Forward audit logs to the syslog endpoint, for example: harbor-log:10514",
|
||||
"SKIP_DATABASE": "Skip Audit Log Database",
|
||||
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured"
|
||||
}
|
||||
}
|
||||
|
@ -1723,5 +1723,28 @@
|
||||
"CO_SIGN": "Cosign",
|
||||
"NOTARY": "Notary",
|
||||
"PLACEHOLDER": "We couldn't find any accessories!"
|
||||
},
|
||||
"CLEARANCES": {
|
||||
"CLEARANCES": "Clean Up",
|
||||
"AUDIT_LOG": "Log Rotation",
|
||||
"LAST_COMPLETED": "Last completed",
|
||||
"NEXT_SCHEDULED_TIME": "Next scheduled time",
|
||||
"SCHEDULE_TO_PURGE": "Schedule to purge",
|
||||
"KEEP_IN": "Keep records in",
|
||||
"KEEP_IN_TOOLTIP": "Keep the records in this interval",
|
||||
"KEEP_IN_ERROR": "This filed is required",
|
||||
"DAYS": "Days",
|
||||
"HOURS": "Hours",
|
||||
"INCLUDED_OPERATIONS": "Included operations",
|
||||
"INCLUDED_OPERATION_TOOLTIP": "Remove audit logs for the selected operations",
|
||||
"INCLUDED_OPERATION_ERROR": "Please select at lease one operation",
|
||||
"PURGE_NOW": "PURGE NOW",
|
||||
"PURGE_NOW_SUCCESS": "Purge triggered successfully",
|
||||
"PURGE_SCHEDULE_RESET": "Purge schedule has been reset",
|
||||
"PURGE_HISTORY": "Purge History",
|
||||
"FORWARD_ENDPOINT": "Audit Log Forward Endpoint",
|
||||
"FORWARD_ENDPOINT_TOOLTIP": "Forward audit logs to the syslog endpoint, for example: harbor-log:10514",
|
||||
"SKIP_DATABASE": "Skip Audit Log Database",
|
||||
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured"
|
||||
}
|
||||
}
|
||||
|
@ -1727,5 +1727,28 @@
|
||||
"CO_SIGN": "Cosign",
|
||||
"NOTARY": "Notary",
|
||||
"PLACEHOLDER": "We couldn't find any accessories!"
|
||||
},
|
||||
"CLEARANCES": {
|
||||
"CLEARANCES": "Clean Up",
|
||||
"AUDIT_LOG": "Log Rotation",
|
||||
"LAST_COMPLETED": "Last completed",
|
||||
"NEXT_SCHEDULED_TIME": "Next scheduled time",
|
||||
"SCHEDULE_TO_PURGE": "Schedule to purge",
|
||||
"KEEP_IN": "Keep records in",
|
||||
"KEEP_IN_TOOLTIP": "Keep the records in this interval",
|
||||
"KEEP_IN_ERROR": "This filed is required",
|
||||
"DAYS": "Days",
|
||||
"HOURS": "Hours",
|
||||
"INCLUDED_OPERATIONS": "Included operations",
|
||||
"INCLUDED_OPERATION_TOOLTIP": "Remove audit logs for the selected operations",
|
||||
"INCLUDED_OPERATION_ERROR": "Please select at lease one operation",
|
||||
"PURGE_NOW": "PURGE NOW",
|
||||
"PURGE_NOW_SUCCESS": "Purge triggered successfully",
|
||||
"PURGE_SCHEDULE_RESET": "Purge schedule has been reset",
|
||||
"PURGE_HISTORY": "Purge History",
|
||||
"FORWARD_ENDPOINT": "Audit Log Forward Endpoint",
|
||||
"FORWARD_ENDPOINT_TOOLTIP": "Forward audit logs to the syslog endpoint, for example: harbor-log:10514",
|
||||
"SKIP_DATABASE": "Skip Audit Log Database",
|
||||
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured"
|
||||
}
|
||||
}
|
||||
|
@ -1725,5 +1725,28 @@
|
||||
"CO_SIGN": "Cosign",
|
||||
"NOTARY": "Notary",
|
||||
"PLACEHOLDER": "未发现任何附件!"
|
||||
},
|
||||
"CLEARANCES": {
|
||||
"CLEARANCES": "清理服务",
|
||||
"AUDIT_LOG": "日志轮替",
|
||||
"LAST_COMPLETED": "最近完成时间",
|
||||
"NEXT_SCHEDULED_TIME": "下次执行时间",
|
||||
"SCHEDULE_TO_PURGE": "当前定时任务",
|
||||
"KEEP_IN": "保留记录",
|
||||
"KEEP_IN_TOOLTIP": "保留指定时间内的日志记录",
|
||||
"KEEP_IN_ERROR": "此项为必填项",
|
||||
"DAYS": "天",
|
||||
"HOURS": "小时",
|
||||
"INCLUDED_OPERATIONS": "包含操作",
|
||||
"INCLUDED_OPERATION_TOOLTIP": "删除指定操作类型的日志",
|
||||
"INCLUDED_OPERATION_ERROR": "请至少选择一种操作类型",
|
||||
"PURGE_NOW": "立即清理",
|
||||
"PURGE_NOW_SUCCESS": "触发清理成功",
|
||||
"PURGE_SCHEDULE_RESET": "清理计划已被重置",
|
||||
"PURGE_HISTORY": "清理历史",
|
||||
"FORWARD_ENDPOINT": "日志转发端点",
|
||||
"FORWARD_ENDPOINT_TOOLTIP": "将日志转发到指定的 syslog 端点,例如:harbor-log:10514",
|
||||
"SKIP_DATABASE": "跳过日志数据库",
|
||||
"SKIP_DATABASE_TOOLTIP": "开启此项将不会在数据库中记录日志,需先配置日志转发端点"
|
||||
}
|
||||
}
|
||||
|
@ -1718,5 +1718,28 @@
|
||||
"CO_SIGN": "Cosign",
|
||||
"NOTARY": "Notary",
|
||||
"PLACEHOLDER": "We couldn't find any accessories!"
|
||||
},
|
||||
"CLEARANCES": {
|
||||
"CLEARANCES": "Clean Up",
|
||||
"AUDIT_LOG": "Log Rotation",
|
||||
"LAST_COMPLETED": "Last completed",
|
||||
"NEXT_SCHEDULED_TIME": "Next scheduled time",
|
||||
"SCHEDULE_TO_PURGE": "Schedule to purge",
|
||||
"KEEP_IN": "Keep records in",
|
||||
"KEEP_IN_TOOLTIP": "Keep the records in this interval",
|
||||
"KEEP_IN_ERROR": "This filed is required",
|
||||
"DAYS": "Days",
|
||||
"HOURS": "Hours",
|
||||
"INCLUDED_OPERATIONS": "Included operations",
|
||||
"INCLUDED_OPERATION_TOOLTIP": "Remove audit logs for the selected operations",
|
||||
"INCLUDED_OPERATION_ERROR": "Please select at lease one operation",
|
||||
"PURGE_NOW": "PURGE NOW",
|
||||
"PURGE_NOW_SUCCESS": "Purge triggered successfully",
|
||||
"PURGE_SCHEDULE_RESET": "Purge schedule has been reset",
|
||||
"PURGE_HISTORY": "Purge History",
|
||||
"FORWARD_ENDPOINT": "Audit Log Forward Endpoint",
|
||||
"FORWARD_ENDPOINT_TOOLTIP": "Forward audit logs to the syslog endpoint, for example: harbor-log:10514",
|
||||
"SKIP_DATABASE": "Skip Audit Log Database",
|
||||
"SKIP_DATABASE_TOOLTIP": "Skip to log audit log in the database, only available when audit log forward endpoint is configured"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user