mirror of
https://github.com/goharbor/harbor.git
synced 2024-06-30 16:55:07 +02: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),
|
).then(m => m.ProjectQuotasModule),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'gc',
|
path: 'clearing-job',
|
||||||
canActivate: [SystemAdminGuard],
|
canActivate: [SystemAdminGuard],
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./left-side-nav/gc-page/gc.module').then(
|
import(
|
||||||
m => m.GcModule
|
'./left-side-nav/clearing-job/clearing-job.module'
|
||||||
),
|
).then(m => m.ClearingJobModule),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'configs',
|
path: 'configs',
|
||||||
|
|
|
@ -160,15 +160,12 @@
|
||||||
<a
|
<a
|
||||||
clrVerticalNavLink
|
clrVerticalNavLink
|
||||||
*ngIf="hasAdminRole"
|
*ngIf="hasAdminRole"
|
||||||
routerLink="/harbor/gc"
|
routerLink="/harbor/clearing-job"
|
||||||
routerLinkActive="active">
|
routerLinkActive="active">
|
||||||
<clr-icon
|
<clr-icon
|
||||||
shape="trash"
|
shape="trash"
|
||||||
clrVerticalNavIcon></clr-icon>
|
clrVerticalNavIcon></clr-icon>
|
||||||
{{
|
{{ 'CLEARANCES.CLEARANCES' | translate }}
|
||||||
'SIDE_NAV.SYSTEM_MGMT.GARBAGE_COLLECTION'
|
|
||||||
| translate
|
|
||||||
}}
|
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
clrVerticalNavLink
|
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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { SessionService } from '../../../shared/services/session.service';
|
import { ClearingJobComponent } from './clearing-job.component';
|
||||||
import { GcPageComponent } from './gc-page.component';
|
|
||||||
import { SharedTestingModule } from '../../../shared/shared.module';
|
import { SharedTestingModule } from '../../../shared/shared.module';
|
||||||
|
|
||||||
describe('GcPageComponent', () => {
|
describe('GcPageComponent', () => {
|
||||||
let component: GcPageComponent;
|
let component: ClearingJobComponent;
|
||||||
let fixture: ComponentFixture<GcPageComponent>;
|
let fixture: ComponentFixture<ClearingJobComponent>;
|
||||||
let fakeSessionService = {
|
|
||||||
getCurrentUser: function () {
|
|
||||||
return { has_admin_role: true };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [GcPageComponent],
|
declarations: [ClearingJobComponent],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
imports: [SharedTestingModule],
|
imports: [SharedTestingModule],
|
||||||
providers: [
|
|
||||||
{ provide: SessionService, useValue: fakeSessionService },
|
|
||||||
],
|
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(GcPageComponent);
|
fixture = TestBed.createComponent(ClearingJobComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
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 }}
|
{{ 'GC.JOB_HISTORY' | translate }}
|
||||||
</h5>
|
</h5>
|
||||||
<span class="refresh-btn" (click)="refresh()">
|
<span class="refresh-btn" (click)="refresh()">
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
[clrDgPageSize]="pageSize"
|
[clrDgPageSize]="pageSize"
|
||||||
[(clrDgPage)]="page"
|
[(clrDgPage)]="page"
|
||||||
[clrDgTotalItems]="total">
|
[clrDgTotalItems]="total">
|
||||||
<clr-dg-page-size [clrPageSizeOptions]="[15, 25, 50]">{{
|
<clr-dg-page-size [clrPageSizeOptions]="[5, 25, 50]">{{
|
||||||
'PAGINATION.PAGE_SIZE' | translate
|
'PAGINATION.PAGE_SIZE' | translate
|
||||||
}}</clr-dg-page-size>
|
}}</clr-dg-page-size>
|
||||||
<span *ngIf="total"
|
<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';
|
} from '@angular/core/testing';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { GcHistoryComponent } from './gc-history.component';
|
import { GcHistoryComponent } from './gc-history.component';
|
||||||
import { SharedTestingModule } from '../../../../../shared/shared.module';
|
import { SharedTestingModule } from '../../../../../../shared/shared.module';
|
||||||
import { GCHistory } from '../../../../../../../ng-swagger-gen/models/gchistory';
|
import { GCHistory } from '../../../../../../../../ng-swagger-gen/models/gchistory';
|
||||||
import { HttpHeaders, HttpResponse } from '@angular/common/http';
|
import { HttpHeaders, HttpResponse } from '@angular/common/http';
|
||||||
import { Registry } from '../../../../../../../ng-swagger-gen/models/registry';
|
import { Registry } from '../../../../../../../../ng-swagger-gen/models/registry';
|
||||||
import { GcService } from '../../../../../../../ng-swagger-gen/services/gc.service';
|
import { GcService } from '../../../../../../../../ng-swagger-gen/services/gc.service';
|
||||||
import { CURRENT_BASE_HREF } from '../../../../../shared/units/utils';
|
import { CURRENT_BASE_HREF } from '../../../../../../shared/units/utils';
|
||||||
import { delay } from 'rxjs/operators';
|
import { delay } from 'rxjs/operators';
|
||||||
|
|
||||||
describe('GcHistoryComponent', () => {
|
describe('GcHistoryComponent', () => {
|
|
@ -1,25 +1,19 @@
|
||||||
import { Component, OnDestroy } from '@angular/core';
|
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 { Subscription, timer } from 'rxjs';
|
||||||
import { REFRESH_TIME_DIFFERENCE } from '../../../../../shared/entities/shared.const';
|
import { REFRESH_TIME_DIFFERENCE } from '../../../../../../shared/entities/shared.const';
|
||||||
import { GcService } from '../../../../../../../ng-swagger-gen/services/gc.service';
|
import { GcService } from '../../../../../../../../ng-swagger-gen/services/gc.service';
|
||||||
import {
|
import {
|
||||||
CURRENT_BASE_HREF,
|
CURRENT_BASE_HREF,
|
||||||
getPageSizeFromLocalStorage,
|
getPageSizeFromLocalStorage,
|
||||||
getSortingString,
|
getSortingString,
|
||||||
PageSizeMapKeys,
|
PageSizeMapKeys,
|
||||||
setPageSizeToLocalStorage,
|
setPageSizeToLocalStorage,
|
||||||
} from '../../../../../shared/units/utils';
|
} from '../../../../../../shared/units/utils';
|
||||||
import { ClrDatagridStateInterface } from '@clr/angular';
|
import { ClrDatagridStateInterface } from '@clr/angular';
|
||||||
import { finalize } from 'rxjs/operators';
|
import { finalize } from 'rxjs/operators';
|
||||||
import { GCHistory } from '../../../../../../../ng-swagger-gen/models/gchistory';
|
import { GCHistory } from '../../../../../../../../ng-swagger-gen/models/gchistory';
|
||||||
|
import { JOB_STATUS, NO, YES } from '../../../clearing-job-interfact';
|
||||||
const JOB_STATUS = {
|
|
||||||
PENDING: 'Pending',
|
|
||||||
RUNNING: 'Running',
|
|
||||||
};
|
|
||||||
const YES: string = 'TAG_RETENTION.YES';
|
|
||||||
const NO: string = 'TAG_RETENTION.NO';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gc-history',
|
selector: 'gc-history',
|
||||||
|
@ -31,7 +25,8 @@ export class GcHistoryComponent implements OnDestroy {
|
||||||
loading: boolean = true;
|
loading: boolean = true;
|
||||||
timerDelay: Subscription;
|
timerDelay: Subscription;
|
||||||
pageSize: number = getPageSizeFromLocalStorage(
|
pageSize: number = getPageSizeFromLocalStorage(
|
||||||
PageSizeMapKeys.GC_HISTORY_COMPONENT
|
PageSizeMapKeys.GC_HISTORY_COMPONENT,
|
||||||
|
5
|
||||||
);
|
);
|
||||||
page: number = 1;
|
page: number = 1;
|
||||||
total: number = 0;
|
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 {
|
.cron-selection {
|
||||||
margin-top: 1rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
@ -26,3 +25,7 @@
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
.center {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
|
@ -1,12 +1,13 @@
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { GcComponent } from './gc.component';
|
import { GcComponent } from './gc.component';
|
||||||
import { ErrorHandler } from '../../../../shared/units/error-handler';
|
import { ErrorHandler } from '../../../../../shared/units/error-handler';
|
||||||
import { CronScheduleComponent } from '../../../../shared/components/cron-schedule';
|
import { CronScheduleComponent } from '../../../../../shared/components/cron-schedule';
|
||||||
import { CronTooltipComponent } from '../../../../shared/components/cron-schedule';
|
import { CronTooltipComponent } from '../../../../../shared/components/cron-schedule';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { SharedTestingModule } from '../../../../shared/shared.module';
|
import { SharedTestingModule } from '../../../../../shared/shared.module';
|
||||||
import { GcService } from '../../../../../../ng-swagger-gen/services/gc.service';
|
import { GcService } from '../../../../../../../ng-swagger-gen/services/gc.service';
|
||||||
import { ScheduleType } from '../../../../shared/entities/shared.const';
|
import { ScheduleType } from '../../../../../shared/entities/shared.const';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
|
||||||
describe('GcComponent', () => {
|
describe('GcComponent', () => {
|
||||||
let component: GcComponent;
|
let component: GcComponent;
|
||||||
|
@ -23,6 +24,7 @@ describe('GcComponent', () => {
|
||||||
};
|
};
|
||||||
let spySchedule: jasmine.Spy;
|
let spySchedule: jasmine.Spy;
|
||||||
let spyGcNow: jasmine.Spy;
|
let spyGcNow: jasmine.Spy;
|
||||||
|
let spyStatus: jasmine.Spy;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [SharedTestingModule],
|
imports: [SharedTestingModule],
|
||||||
|
@ -32,6 +34,7 @@ describe('GcComponent', () => {
|
||||||
CronTooltipComponent,
|
CronTooltipComponent,
|
||||||
],
|
],
|
||||||
providers: [{ provide: ErrorHandler, useValue: fakedErrorHandler }],
|
providers: [{ provide: ErrorHandler, useValue: fakedErrorHandler }],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -46,6 +49,20 @@ describe('GcComponent', () => {
|
||||||
spyGcNow = spyOn(gcRepoService, 'createGCSchedule').and.returnValues(
|
spyGcNow = spyOn(gcRepoService, 'createGCSchedule').and.returnValues(
|
||||||
of(null)
|
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();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('should create', () => {
|
it('should create', () => {
|
|
@ -1,17 +1,16 @@
|
||||||
import {
|
import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core';
|
||||||
Component,
|
import { ErrorHandler } from '../../../../../shared/units/error-handler';
|
||||||
Output,
|
import { CronScheduleComponent } from '../../../../../shared/components/cron-schedule';
|
||||||
EventEmitter,
|
import { OriginCron } from '../../../../../shared/services';
|
||||||
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 { finalize } from 'rxjs/operators';
|
import { finalize } from 'rxjs/operators';
|
||||||
import { GcService } from '../../../../../../ng-swagger-gen/services/gc.service';
|
import { GcService } from '../../../../../../../ng-swagger-gen/services/gc.service';
|
||||||
import { GCHistory } from '../../../../../../ng-swagger-gen/models/gchistory';
|
import { GCHistory } from '../../../../../../../ng-swagger-gen/models/gchistory';
|
||||||
import { ScheduleType } from '../../../../shared/entities/shared.const';
|
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;
|
const ONE_MINUTE = 60000;
|
||||||
|
|
||||||
|
@ -20,16 +19,22 @@ const ONE_MINUTE = 60000;
|
||||||
templateUrl: './gc.component.html',
|
templateUrl: './gc.component.html',
|
||||||
styleUrls: ['./gc.component.scss'],
|
styleUrls: ['./gc.component.scss'],
|
||||||
})
|
})
|
||||||
export class GcComponent implements OnInit {
|
export class GcComponent implements OnInit, OnDestroy {
|
||||||
originCron: OriginCron;
|
originCron: OriginCron;
|
||||||
disableGC: boolean = false;
|
disableGC: boolean = false;
|
||||||
getLabelCurrent = 'GC.CURRENT_SCHEDULE';
|
getLabelCurrent = 'GC.CURRENT_SCHEDULE';
|
||||||
@Output() loadingGcStatus = new EventEmitter<boolean>();
|
loadingGcStatus = false;
|
||||||
@ViewChild(CronScheduleComponent)
|
@ViewChild(CronScheduleComponent)
|
||||||
CronScheduleComponent: CronScheduleComponent;
|
CronScheduleComponent: CronScheduleComponent;
|
||||||
shouldDeleteUntagged: boolean;
|
shouldDeleteUntagged: boolean;
|
||||||
dryRunOnGoing: boolean = false;
|
dryRunOnGoing: boolean = false;
|
||||||
|
|
||||||
|
lastCompletedTime: string;
|
||||||
|
loadingLastCompletedTime: boolean = false;
|
||||||
|
isDryRun: boolean = false;
|
||||||
|
nextScheduledTime: string;
|
||||||
|
statusTimeout: any;
|
||||||
|
@ViewChild(GcHistoryComponent) gcHistoryComponent: GcHistoryComponent;
|
||||||
constructor(
|
constructor(
|
||||||
private gcService: GcService,
|
private gcService: GcService,
|
||||||
private errorHandler: ErrorHandler
|
private errorHandler: ErrorHandler
|
||||||
|
@ -37,15 +42,47 @@ export class GcComponent implements OnInit {
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.getCurrentSchedule();
|
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() {
|
getCurrentSchedule() {
|
||||||
this.loadingGcStatus.emit(true);
|
this.loadingGcStatus = true;
|
||||||
this.gcService
|
this.gcService
|
||||||
.getGCSchedule()
|
.getGCSchedule()
|
||||||
.pipe(
|
.pipe(
|
||||||
finalize(() => {
|
finalize(() => {
|
||||||
this.loadingGcStatus.emit(false);
|
this.loadingGcStatus = false;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
@ -59,6 +96,11 @@ export class GcComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
private initSchedule(gcHistory: GCHistory) {
|
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) {
|
if (gcHistory && gcHistory.schedule) {
|
||||||
this.originCron = {
|
this.originCron = {
|
||||||
type: gcHistory.schedule.type,
|
type: gcHistory.schedule.type,
|
||||||
|
@ -97,14 +139,15 @@ export class GcComponent implements OnInit {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.subscribe(
|
.subscribe({
|
||||||
response => {
|
next: response => {
|
||||||
this.errorHandler.info('GC.MSG_SUCCESS');
|
this.errorHandler.info('GC.MSG_SUCCESS');
|
||||||
|
this.refresh();
|
||||||
},
|
},
|
||||||
error => {
|
error: error => {
|
||||||
this.errorHandler.error(error);
|
this.errorHandler.error(error);
|
||||||
}
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dryRun() {
|
dryRun() {
|
||||||
|
@ -122,14 +165,15 @@ export class GcComponent implements OnInit {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.pipe(finalize(() => (this.dryRunOnGoing = false)))
|
.pipe(finalize(() => (this.dryRunOnGoing = false)))
|
||||||
.subscribe(
|
.subscribe({
|
||||||
response => {
|
next: response => {
|
||||||
this.errorHandler.info('GC.DRY_RUN_SUCCESS');
|
this.errorHandler.info('GC.DRY_RUN_SUCCESS');
|
||||||
|
this.refresh();
|
||||||
},
|
},
|
||||||
error => {
|
error: error => {
|
||||||
this.errorHandler.error(error);
|
this.errorHandler.error(error);
|
||||||
}
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private enableGc() {
|
private enableGc() {
|
||||||
|
@ -137,7 +181,7 @@ export class GcComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
saveGcSchedule(cron: string) {
|
saveGcSchedule(cron: string) {
|
||||||
if (this.originCron && this.originCron.type !== ScheduleType.NONE) {
|
if (this.originCron && this.originCron.type === ScheduleType.NONE) {
|
||||||
// no schedule, then create
|
// no schedule, then create
|
||||||
this.gcService
|
this.gcService
|
||||||
.createGCSchedule({
|
.createGCSchedule({
|
||||||
|
@ -206,4 +250,8 @@ export class GcComponent implements OnInit {
|
||||||
}
|
}
|
||||||
return ScheduleType.NONE;
|
return ScheduleType.NONE;
|
||||||
}
|
}
|
||||||
|
refresh() {
|
||||||
|
this.getStatus();
|
||||||
|
this.gcHistoryComponent?.refresh();
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -27,6 +27,16 @@
|
||||||
{{ 'CONFIG.EMAIL' | translate }}
|
{{ 'CONFIG.EMAIL' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</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">
|
<li role="presentation" class="nav-item">
|
||||||
<button
|
<button
|
||||||
id="config-system"
|
id="config-system"
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { ConfigurationEmailComponent } from './email/config-email.component';
|
||||||
import { SystemSettingsComponent } from './system/system-settings.component';
|
import { SystemSettingsComponent } from './system/system-settings.component';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigService } from './config.service';
|
||||||
|
import { SecurityComponent } from './security/security.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -34,6 +35,10 @@ const routes: Routes = [
|
||||||
path: 'email',
|
path: 'email',
|
||||||
component: ConfigurationEmailComponent,
|
component: ConfigurationEmailComponent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'security',
|
||||||
|
component: SecurityComponent,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'setting',
|
path: 'setting',
|
||||||
component: SystemSettingsComponent,
|
component: SystemSettingsComponent,
|
||||||
|
@ -53,6 +58,7 @@ const routes: Routes = [
|
||||||
ConfigurationAuthComponent,
|
ConfigurationAuthComponent,
|
||||||
ConfigurationEmailComponent,
|
ConfigurationEmailComponent,
|
||||||
SystemSettingsComponent,
|
SystemSettingsComponent,
|
||||||
|
SecurityComponent,
|
||||||
],
|
],
|
||||||
providers: [ConfigService],
|
providers: [ConfigService],
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { ConfigureService } from 'ng-swagger-gen/services/configure.service';
|
||||||
import { clone } from '../../../shared/units/utils';
|
import { clone } from '../../../shared/units/utils';
|
||||||
import { MessageHandlerService } from '../../../shared/services/message-handler.service';
|
import { MessageHandlerService } from '../../../shared/services/message-handler.service';
|
||||||
import { finalize } from 'rxjs/operators';
|
import { finalize } from 'rxjs/operators';
|
||||||
import { Subscription } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
|
|
||||||
const fakePass = 'aWpLOSYkIzJTTU4wMDkx';
|
const fakePass = 'aWpLOSYkIzJTTU4wMDkx';
|
||||||
|
|
||||||
|
@ -116,4 +116,10 @@ export class ConfigService {
|
||||||
);
|
);
|
||||||
this.confirmService.openComfirmDialog(msg);
|
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;
|
cfg_expiration: NumberValueItem;
|
||||||
oidc_groups_claim: StringValueItem;
|
oidc_groups_claim: StringValueItem;
|
||||||
oidc_admin_group: StringValueItem;
|
oidc_admin_group: StringValueItem;
|
||||||
|
audit_log_forward_endpoint: StringValueItem;
|
||||||
|
skip_audit_log_database: BoolValueItem;
|
||||||
public constructor() {
|
public constructor() {
|
||||||
this.auth_mode = new StringValueItem('db_auth', true);
|
this.auth_mode = new StringValueItem('db_auth', true);
|
||||||
this.project_creation_restriction = new StringValueItem(
|
this.project_creation_restriction = new StringValueItem(
|
||||||
|
@ -178,6 +180,8 @@ export class Configuration {
|
||||||
this.oidc_user_claim = new StringValueItem('', true);
|
this.oidc_user_claim = new StringValueItem('', true);
|
||||||
this.count_per_project = new NumberValueItem(-1, true);
|
this.count_per_project = new NumberValueItem(-1, true);
|
||||||
this.storage_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
|
clrSelect
|
||||||
id="proCreation"
|
id="proCreation"
|
||||||
name="proCreation"
|
name="proCreation"
|
||||||
|
class="pro-creation"
|
||||||
[(ngModel)]="currentConfig.project_creation_restriction.value"
|
[(ngModel)]="currentConfig.project_creation_restriction.value"
|
||||||
[disabled]="
|
[disabled]="
|
||||||
disabled(currentConfig.project_creation_restriction)
|
disabled(currentConfig.project_creation_restriction)
|
||||||
|
@ -189,142 +190,7 @@
|
||||||
(ngModelChange)="setRepoReadOnlyValue($event)" />
|
(ngModelChange)="setRepoReadOnlyValue($event)" />
|
||||||
</clr-checkbox-wrapper>
|
</clr-checkbox-wrapper>
|
||||||
</clr-checkbox-container>
|
</clr-checkbox-container>
|
||||||
|
<clr-checkbox-container class="center">
|
||||||
<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>
|
|
||||||
<label for="webhookNotificationEnabled"
|
<label for="webhookNotificationEnabled"
|
||||||
>{{ 'CONFIG.WEBHOOK_NOTIFICATION_ENABLED' | translate }}
|
>{{ 'CONFIG.WEBHOOK_NOTIFICATION_ENABLED' | translate }}
|
||||||
<clr-tooltip>
|
<clr-tooltip>
|
||||||
|
@ -353,6 +219,67 @@
|
||||||
[disabled]="!currentConfig.notification_enable.editable" />
|
[disabled]="!currentConfig.notification_enable.editable" />
|
||||||
</clr-checkbox-wrapper>
|
</clr-checkbox-wrapper>
|
||||||
</clr-checkbox-container>
|
</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>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
<div>
|
<div>
|
||||||
|
@ -361,10 +288,7 @@
|
||||||
id="config_system_save"
|
id="config_system_save"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
(click)="save()"
|
(click)="save()"
|
||||||
[disabled]="
|
[disabled]="!isValid() || !hasChanges() || inProgress">
|
||||||
((!isValid() || !hasChanges()) && !hasAllowlistChanged) ||
|
|
||||||
inProgress
|
|
||||||
">
|
|
||||||
{{ 'BUTTON.SAVE' | translate }}
|
{{ 'BUTTON.SAVE' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
@ -372,10 +296,7 @@
|
||||||
id="config_system_cancel"
|
id="config_system_cancel"
|
||||||
class="btn btn-outline"
|
class="btn btn-outline"
|
||||||
(click)="cancel()"
|
(click)="cancel()"
|
||||||
[disabled]="
|
[disabled]="!isValid() || !hasChanges() || inProgress">
|
||||||
((!isValid() || !hasChanges()) && !hasAllowlistChanged) ||
|
|
||||||
inProgress
|
|
||||||
">
|
|
||||||
{{ 'BUTTON.CANCEL' | translate }}
|
{{ 'BUTTON.CANCEL' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
.clr-form-horizontal {
|
.clr-form-horizontal {
|
||||||
.clr-form-control {
|
.clr-form-control {
|
||||||
& >.clr-control-label {
|
& >.clr-control-label {
|
||||||
width: 12rem;
|
width: 14rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.flex-direction-column {
|
.flex-direction-column {
|
||||||
|
@ -136,3 +136,13 @@
|
||||||
.margin-top-3px {
|
.margin-top-3px {
|
||||||
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 {
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
ComponentFixture,
|
|
||||||
ComponentFixtureAutoDetect,
|
|
||||||
TestBed,
|
|
||||||
} from '@angular/core/testing';
|
|
||||||
import { SystemSettingsComponent } from './system-settings.component';
|
import { SystemSettingsComponent } from './system-settings.component';
|
||||||
import { SystemInfoService } from '../../../../shared/services';
|
|
||||||
import { ErrorHandler } from '../../../../shared/units/error-handler';
|
import { ErrorHandler } from '../../../../shared/units/error-handler';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { Configuration, StringValueItem } from '../config';
|
import { Configuration, NumberValueItem, StringValueItem } from '../config';
|
||||||
import { SharedTestingModule } from '../../../../shared/shared.module';
|
import { SharedTestingModule } from '../../../../shared/shared.module';
|
||||||
import { ConfigService } from '../config.service';
|
import { ConfigService } from '../config.service';
|
||||||
import { AppConfigService } from '../../../../services/app-config.service';
|
import { AppConfigService } from '../../../../services/app-config.service';
|
||||||
|
|
||||||
describe('SystemSettingsComponent', () => {
|
describe('SystemSettingsComponent', () => {
|
||||||
let component: SystemSettingsComponent;
|
let component: SystemSettingsComponent;
|
||||||
let fixture: ComponentFixture<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 = {
|
const fakedErrorHandler = {
|
||||||
info() {
|
info() {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const fakeConfigService = {
|
const fakeConfigService = {
|
||||||
config: new Configuration(),
|
config: new Configuration(),
|
||||||
getConfig() {
|
getConfig() {
|
||||||
|
@ -53,6 +33,9 @@ describe('SystemSettingsComponent', () => {
|
||||||
confirmUnsavedChanges() {},
|
confirmUnsavedChanges() {},
|
||||||
updateConfig() {},
|
updateConfig() {},
|
||||||
resetConfig() {},
|
resetConfig() {},
|
||||||
|
saveConfiguration() {
|
||||||
|
return of(null);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const fakedAppConfigService = {
|
const fakedAppConfigService = {
|
||||||
getConfig() {
|
getConfig() {
|
||||||
|
@ -69,12 +52,6 @@ describe('SystemSettingsComponent', () => {
|
||||||
{ provide: AppConfigService, useValue: fakedAppConfigService },
|
{ provide: AppConfigService, useValue: fakedAppConfigService },
|
||||||
{ provide: ConfigService, useValue: fakeConfigService },
|
{ provide: ConfigService, useValue: fakeConfigService },
|
||||||
{ provide: ErrorHandler, useValue: fakedErrorHandler },
|
{ provide: ErrorHandler, useValue: fakedErrorHandler },
|
||||||
{
|
|
||||||
provide: SystemInfoService,
|
|
||||||
useValue: fakedSystemInfoService,
|
|
||||||
},
|
|
||||||
// open auto detect
|
|
||||||
{ provide: ComponentFixtureAutoDetect, useValue: true },
|
|
||||||
],
|
],
|
||||||
declarations: [SystemSettingsComponent],
|
declarations: [SystemSettingsComponent],
|
||||||
});
|
});
|
||||||
|
@ -82,46 +59,30 @@ describe('SystemSettingsComponent', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(SystemSettingsComponent);
|
fixture = TestBed.createComponent(SystemSettingsComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
component.currentConfig.auth_mode = new StringValueItem(
|
fixture.autoDetectChanges(true);
|
||||||
'db_auth',
|
|
||||||
false
|
|
||||||
);
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
it('cancel button should works', () => {
|
it('cancel button should work', () => {
|
||||||
const spy: jasmine.Spy = spyOn(
|
const spy: jasmine.Spy = spyOn(component, 'cancel').and.returnValue(
|
||||||
fakeConfigService,
|
undefined
|
||||||
'confirmUnsavedChanges'
|
);
|
||||||
).and.returnValue(undefined);
|
|
||||||
component.systemAllowlist.items.push({ cve_id: 'CVE-2019-456' });
|
|
||||||
const readOnly: HTMLElement =
|
|
||||||
fixture.nativeElement.querySelector('#repoReadOnly');
|
|
||||||
readOnly.click();
|
|
||||||
fixture.detectChanges();
|
|
||||||
const cancel: HTMLButtonElement = fixture.nativeElement.querySelector(
|
const cancel: HTMLButtonElement = fixture.nativeElement.querySelector(
|
||||||
'#config_system_cancel'
|
'#config_system_cancel'
|
||||||
);
|
);
|
||||||
cancel.click();
|
cancel.dispatchEvent(new Event('click'));
|
||||||
fixture.detectChanges();
|
|
||||||
expect(spy.calls.count()).toEqual(1);
|
expect(spy.calls.count()).toEqual(1);
|
||||||
});
|
});
|
||||||
it('save button should works', () => {
|
it('save button should work', () => {
|
||||||
component.systemAllowlist.items[0].cve_id = 'CVE-2019-789';
|
const input = fixture.nativeElement.querySelector('#robotNamePrefix');
|
||||||
const readOnly: HTMLElement =
|
input.value = 'test';
|
||||||
fixture.nativeElement.querySelector('#repoReadOnly');
|
input.dispatchEvent(new Event('input'));
|
||||||
readOnly.click();
|
|
||||||
fixture.detectChanges();
|
|
||||||
const save: HTMLButtonElement = fixture.nativeElement.querySelector(
|
const save: HTMLButtonElement = fixture.nativeElement.querySelector(
|
||||||
'#config_system_save'
|
'#config_system_save'
|
||||||
);
|
);
|
||||||
save.click();
|
save.dispatchEvent(new Event('click'));
|
||||||
fixture.detectChanges();
|
expect(input.value).toEqual('test');
|
||||||
expect(component.systemAllowlistOrigin.items[0].cve_id).toEqual(
|
|
||||||
'CVE-2019-789'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 { NgForm } from '@angular/forms';
|
||||||
import { Configuration } from '../config';
|
import { Configuration } from '../config';
|
||||||
import {
|
import {
|
||||||
clone,
|
|
||||||
compareValue,
|
|
||||||
CURRENT_BASE_HREF,
|
CURRENT_BASE_HREF,
|
||||||
getChanges,
|
getChanges,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
} from '../../../../shared/units/utils';
|
} from '../../../../shared/units/utils';
|
||||||
import { ErrorHandler } from '../../../../shared/units/error-handler';
|
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 { ConfigService } from '../config.service';
|
||||||
import { AppConfigService } from '../../../../services/app-config.service';
|
import { AppConfigService } from '../../../../services/app-config.service';
|
||||||
|
import { finalize } from 'rxjs/operators';
|
||||||
const ONE_THOUSAND: number = 1000;
|
|
||||||
const CVE_DETAIL_PRE_URL = `https://nvd.nist.gov/vuln/detail/`;
|
|
||||||
const TARGET_BLANK = '_blank';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'system-settings',
|
selector: 'system-settings',
|
||||||
|
@ -36,11 +19,6 @@ const TARGET_BLANK = '_blank';
|
||||||
export class SystemSettingsComponent implements OnInit {
|
export class SystemSettingsComponent implements OnInit {
|
||||||
onGoing = false;
|
onGoing = false;
|
||||||
downloadLink: string;
|
downloadLink: string;
|
||||||
systemAllowlist: SystemCVEAllowlist;
|
|
||||||
systemAllowlistOrigin: SystemCVEAllowlist;
|
|
||||||
cveIds: string;
|
|
||||||
showAddModal: boolean = false;
|
|
||||||
systemInfo: SystemInfo;
|
|
||||||
get currentConfig(): Configuration {
|
get currentConfig(): Configuration {
|
||||||
return this.conf.getConfig();
|
return this.conf.getConfig();
|
||||||
}
|
}
|
||||||
|
@ -49,7 +27,18 @@ export class SystemSettingsComponent implements OnInit {
|
||||||
this.conf.setConfig(cfg);
|
this.conf.setConfig(cfg);
|
||||||
}
|
}
|
||||||
@ViewChild('systemConfigFrom') systemSettingsForm: NgForm;
|
@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 {
|
get editable(): boolean {
|
||||||
return (
|
return (
|
||||||
|
@ -121,7 +110,9 @@ export class SystemSettingsComponent implements OnInit {
|
||||||
prop === 'project_creation_restriction' ||
|
prop === 'project_creation_restriction' ||
|
||||||
prop === 'robot_token_duration' ||
|
prop === 'robot_token_duration' ||
|
||||||
prop === 'notification_enable' ||
|
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];
|
changes[prop] = allChanges[prop];
|
||||||
}
|
}
|
||||||
|
@ -153,27 +144,13 @@ export class SystemSettingsComponent implements OnInit {
|
||||||
*/
|
*/
|
||||||
public save(): void {
|
public save(): void {
|
||||||
let changes = this.getChanges();
|
let changes = this.getChanges();
|
||||||
if (
|
|
||||||
!isEmpty(changes) ||
|
|
||||||
!compareValue(this.systemAllowlistOrigin, this.systemAllowlist)
|
|
||||||
) {
|
|
||||||
this.onGoing = true;
|
|
||||||
let observables = [];
|
|
||||||
if (!isEmpty(changes)) {
|
if (!isEmpty(changes)) {
|
||||||
observables.push(this.configService.saveConfiguration(changes));
|
this.onGoing = true;
|
||||||
}
|
this.conf
|
||||||
if (
|
.saveConfiguration(changes)
|
||||||
!compareValue(this.systemAllowlistOrigin, this.systemAllowlist)
|
.pipe(finalize(() => (this.onGoing = false)))
|
||||||
) {
|
.subscribe({
|
||||||
observables.push(
|
next: result => {
|
||||||
this.systemInfoService.updateSystemAllowlist(
|
|
||||||
this.systemAllowlist
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
forkJoin(observables).subscribe(
|
|
||||||
result => {
|
|
||||||
this.onGoing = false;
|
|
||||||
if (!isEmpty(changes)) {
|
if (!isEmpty(changes)) {
|
||||||
// API should return the updated configurations here
|
// API should return the updated configurations here
|
||||||
// Unfortunately API does not do that
|
// Unfortunately API does not do that
|
||||||
|
@ -191,44 +168,17 @@ export class SystemSettingsComponent implements OnInit {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
!compareValue(
|
|
||||||
this.systemAllowlistOrigin,
|
|
||||||
this.systemAllowlist
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.systemAllowlistOrigin = clone(
|
|
||||||
this.systemAllowlist
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.errorHandler.info('CONFIG.SAVE_SUCCESS');
|
this.errorHandler.info('CONFIG.SAVE_SUCCESS');
|
||||||
},
|
},
|
||||||
error => {
|
error: error => {
|
||||||
this.onGoing = false;
|
|
||||||
this.errorHandler.error(error);
|
this.errorHandler.error(error);
|
||||||
}
|
},
|
||||||
);
|
});
|
||||||
} else {
|
} else {
|
||||||
// Inprop situation, should not come here
|
// Inprop situation, should not come here
|
||||||
console.error('Save abort because nothing changed');
|
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 {
|
public get inProgress(): boolean {
|
||||||
return this.onGoing || this.conf.getLoadingConfigStatus();
|
return this.onGoing || this.conf.getLoadingConfigStatus();
|
||||||
}
|
}
|
||||||
|
@ -241,10 +191,7 @@ export class SystemSettingsComponent implements OnInit {
|
||||||
*/
|
*/
|
||||||
public cancel(): void {
|
public cancel(): void {
|
||||||
let changes = this.getChanges();
|
let changes = this.getChanges();
|
||||||
if (
|
if (!isEmpty(changes)) {
|
||||||
!isEmpty(changes) ||
|
|
||||||
!compareValue(this.systemAllowlistOrigin, this.systemAllowlist)
|
|
||||||
) {
|
|
||||||
this.conf.confirmUnsavedChanges(changes);
|
this.conf.confirmUnsavedChanges(changes);
|
||||||
} else {
|
} else {
|
||||||
// Invalid situation, should not come here
|
// Invalid situation, should not come here
|
||||||
|
@ -252,129 +199,9 @@ export class SystemSettingsComponent implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
checkAuditLogForwardEndpoint(e: any) {
|
||||||
private appConfigService: AppConfigService,
|
if (!e?.target?.value) {
|
||||||
private configService: ConfigurationService,
|
this.currentConfig.skip_audit_log_database.value = false;
|
||||||
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);
|
|
||||||
}
|
|
||||||
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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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.SAVE' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-link" (click)="isEditMode = false">
|
<button class="btn btn-link" (click)="isEditMode = false">
|
||||||
|
|
|
@ -5,8 +5,6 @@
|
||||||
|
|
||||||
.font-style {
|
.font-style {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: #000;
|
|
||||||
font-size: .541667rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
span.required {
|
span.required {
|
||||||
|
|
|
@ -25,6 +25,7 @@ const PREFIX: string = '0 ';
|
||||||
styleUrls: ['./cron-schedule.component.scss'],
|
styleUrls: ['./cron-schedule.component.scss'],
|
||||||
})
|
})
|
||||||
export class CronScheduleComponent implements OnChanges {
|
export class CronScheduleComponent implements OnChanges {
|
||||||
|
@Input() externalValidation: boolean = true; //extra check
|
||||||
@Input() isInlineModel: boolean = false;
|
@Input() isInlineModel: boolean = false;
|
||||||
@Input() originCron: OriginCron;
|
@Input() originCron: OriginCron;
|
||||||
@Input() labelEdit: string;
|
@Input() labelEdit: string;
|
||||||
|
@ -46,7 +47,7 @@ export class CronScheduleComponent implements OnChanges {
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
let cronChange: SimpleChange = changes['originCron'];
|
let cronChange: SimpleChange = changes['originCron'];
|
||||||
if (cronChange.currentValue) {
|
if (cronChange?.currentValue) {
|
||||||
this.originScheduleType = cronChange.currentValue.type;
|
this.originScheduleType = cronChange.currentValue.type;
|
||||||
this.oriCron = cronChange.currentValue.cron;
|
this.oriCron = cronChange.currentValue.cron;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1727,5 +1727,28 @@
|
||||||
"CO_SIGN": "Cosign",
|
"CO_SIGN": "Cosign",
|
||||||
"NOTARY": "Notary",
|
"NOTARY": "Notary",
|
||||||
"PLACEHOLDER": "Es konnten keine Anhänge gefunden werden!"
|
"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",
|
"CO_SIGN": "Cosign",
|
||||||
"NOTARY": "Notary",
|
"NOTARY": "Notary",
|
||||||
"PLACEHOLDER": "We couldn't find any accessories!"
|
"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",
|
"CO_SIGN": "Cosign",
|
||||||
"NOTARY": "Notary",
|
"NOTARY": "Notary",
|
||||||
"PLACEHOLDER": "We couldn't find any accessories!"
|
"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",
|
"CO_SIGN": "Cosign",
|
||||||
"NOTARY": "Notary",
|
"NOTARY": "Notary",
|
||||||
"PLACEHOLDER": "Nous n'avons trouvé aucun accessoire !"
|
"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",
|
"CO_SIGN": "Cosign",
|
||||||
"NOTARY": "Notary",
|
"NOTARY": "Notary",
|
||||||
"PLACEHOLDER": "We couldn't find any accessories!"
|
"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",
|
"CO_SIGN": "Cosign",
|
||||||
"NOTARY": "Notary",
|
"NOTARY": "Notary",
|
||||||
"PLACEHOLDER": "We couldn't find any accessories!"
|
"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",
|
"CO_SIGN": "Cosign",
|
||||||
"NOTARY": "Notary",
|
"NOTARY": "Notary",
|
||||||
"PLACEHOLDER": "未发现任何附件!"
|
"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",
|
"CO_SIGN": "Cosign",
|
||||||
"NOTARY": "Notary",
|
"NOTARY": "Notary",
|
||||||
"PLACEHOLDER": "We couldn't find any accessories!"
|
"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