support to audit logs (#21377)

Signed-off-by: Lichao Xue <lichao.xue@broadcom.com>
Co-authored-by: Lichao Xue <lichao.xue@broadcom.com>
This commit is contained in:
Lichao Xue 2025-02-21 13:44:48 +08:00 committed by GitHub
parent 45659070b7
commit b837bbb716
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 1650 additions and 101 deletions

View File

@ -141,7 +141,7 @@
)
}">
<span class="font-style required flex-200"
>{{ 'CLEARANCES.INCLUDED_OPERATIONS' | translate
>{{ 'CLEARANCES.INCLUDED_EVENT_TYPES' | translate
}}<clr-tooltip>
<clr-icon
clrTooltipTrigger
@ -152,37 +152,35 @@
clrSize="lg"
*clrIfOpen>
<span>{{
'CLEARANCES.INCLUDED_OPERATION_TOOLTIP' | translate
'CLEARANCES.INCLUDED_EVENT_TYPE_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-control-container">
<div
class="clr-checkbox-wrapper"
*ngFor="let item of operations">
class="clr-checkbox-wrapper float-left"
*ngFor="let item of eventTypes">
<input
type="checkbox"
id="{{ item }}"
name="operations"
value="{{ item }}"
id="{{ item.id }}"
name="eventTypes"
value="{{ item.value }}"
class="clr-checkbox"
(change)="setOperation(item)"
[checked]="hasOperation(item)" />
<label for="{{ item }}" class="clr-control-label">{{
operationsToText(item) | translate
(change)="setEventType(item.value)"
[checked]="hasEventType(item.value)" />
<label for="{{ item.id }}" class="clr-control-label">{{
item.label
}}</label>
</div>
<div
class="clr-subtext-wrapper"
*ngIf="!(selectedOperations?.length > 0)">
*ngIf="!(selectedEventTypes?.length > 0)">
<clr-icon
class="clr-validate-icon"
shape="exclamation-circle"></clr-icon>
<span class="clr-subtext">{{
'CLEARANCES.INCLUDED_OPERATION_ERROR' | translate
'CLEARANCES.INCLUDED_EVENT_TYPE_ERROR' | translate
}}</span>
</div>
</div>
@ -197,7 +195,7 @@
[disabled]="
disableGC ||
purgeForm.invalid ||
!(selectedOperations?.length > 0)
!(selectedEventTypes?.length > 0)
">
{{ 'CLEARANCES.PURGE_NOW' | translate }}
</button>
@ -210,7 +208,7 @@
[disabled]="
dryRunOnGoing ||
purgeForm.invalid ||
!(selectedOperations?.length > 0)
!(selectedEventTypes?.length > 0)
">
{{ 'TAG_RETENTION.WHAT_IF_RUN' | translate }}
</button>

View File

@ -51,3 +51,8 @@
padding-left: .6rem;
padding-right: .6rem;
}
.float-left {
float:left;
margin-right: 1rem;
}

View File

@ -2,16 +2,19 @@ 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 { delay, 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';
import { AuditlogService } from 'ng-swagger-gen/services';
import { HttpHeaders, HttpResponse } from '@angular/common/http';
describe('GcComponent', () => {
let component: SetJobComponent;
let fixture: ComponentFixture<SetJobComponent>;
let purgeService: PurgeService;
let auditlogService: AuditlogService;
let mockSchedule = [];
const fakedErrorHandler = {
error(error) {
@ -23,6 +26,29 @@ describe('GcComponent', () => {
};
let spySchedule: jasmine.Spy;
let spyGcNow: jasmine.Spy;
const mockedAuditLogs = [
{
event_type: 'create_artifact',
},
{
event_type: 'delete_artifact',
},
{
event_type: 'pull_artifact',
},
];
const fakedAuditlogService = {
listAuditLogEventTypesResponse() {
return of(
new HttpResponse({
body: mockedAuditLogs,
headers: new HttpHeaders({
'x-total-count': '18',
}),
})
).pipe(delay(0));
},
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [SharedTestingModule],
@ -31,7 +57,10 @@ describe('GcComponent', () => {
CronScheduleComponent,
CronTooltipComponent,
],
providers: [{ provide: ErrorHandler, useValue: fakedErrorHandler }],
providers: [
{ provide: ErrorHandler, useValue: fakedErrorHandler },
{ provide: AuditlogService, useValue: fakedAuditlogService },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
});
@ -39,7 +68,7 @@ describe('GcComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(SetJobComponent);
component = fixture.componentInstance;
auditlogService = fixture.debugElement.injector.get(AuditlogService);
purgeService = fixture.debugElement.injector.get(PurgeService);
spySchedule = spyOn(purgeService, 'getPurgeSchedule').and.returnValues(
of(mockSchedule as any)
@ -47,6 +76,7 @@ describe('GcComponent', () => {
spyGcNow = spyOn(purgeService, 'createPurgeSchedule').and.returnValues(
of(null)
);
component.selectedEventTypes = ['create_artifact'];
fixture.detectChanges();
});
it('should create', () => {

View File

@ -10,13 +10,14 @@ import { ExecHistory } from '../../../../../../../ng-swagger-gen/models/exec-his
import {
JOB_STATUS,
REFRESH_STATUS_TIME_DIFFERENCE,
RETENTION_OPERATIONS,
RETENTION_OPERATIONS_I18N_MAP,
RESOURCE_TYPES,
RetentionTimeUnit,
} from '../../clearing-job-interfact';
import { clone } from '../../../../../shared/units/utils';
import { PurgeHistoryComponent } from '../history/purge-history.component';
import { NgForm } from '@angular/forms';
import { AuditlogService } from 'ng-swagger-gen/services';
import { AuditLogEventType } from 'ng-swagger-gen/models';
const ONE_MINUTE: number = 60000;
const ONE_DAY: number = 24;
@ -30,6 +31,7 @@ const MAX_RETENTION_DAYS: number = 10000;
export class SetJobComponent implements OnInit, OnDestroy {
originCron: OriginCron;
disableGC: boolean = false;
loading: boolean = false;
getLabelCurrent = 'CLEARANCES.SCHEDULE_TO_PURGE';
loadingGcStatus = false;
@ViewChild(CronScheduleComponent)
@ -43,20 +45,23 @@ export class SetJobComponent implements OnInit, OnDestroy {
retentionTime: number;
retentionUnit: string = RetentionTimeUnit.DAYS;
operations: string[] = clone(RETENTION_OPERATIONS);
selectedOperations: string[] = clone(RETENTION_OPERATIONS);
eventTypes: Record<string, string>[] = [];
selectedEventTypes: string[] = clone([]);
@ViewChild(PurgeHistoryComponent)
purgeHistoryComponent: PurgeHistoryComponent;
@ViewChild('purgeForm')
purgeForm: NgForm;
constructor(
private purgeService: PurgeService,
private logService: AuditlogService,
private errorHandler: ErrorHandler
) {}
ngOnInit() {
this.getCurrentSchedule(true);
this.getStatus();
this.initEventTypes();
}
ngOnDestroy() {
if (this.statusTimeout) {
@ -64,6 +69,43 @@ export class SetJobComponent implements OnInit, OnDestroy {
this.statusTimeout = null;
}
}
initEventTypes() {
this.loading = true;
this.logService
.listAuditLogEventTypesResponse()
.pipe(finalize(() => (this.loading = false)))
.subscribe(
response => {
const auditLogEventTypes =
response.body as AuditLogEventType[];
this.eventTypes = [
...auditLogEventTypes
.filter(item =>
RESOURCE_TYPES.includes(item.event_type)
)
.map(event => ({
label:
event.event_type.charAt(0).toUpperCase() +
event.event_type
.slice(1)
.replace(/_/g, ' '),
value: event.event_type,
id: event.event_type,
})),
{
label: 'Other events',
value: 'other',
id: 'other_events',
},
];
},
error => {
this.errorHandler.error(error);
}
);
}
// get the latest non-dry-run execution to get the status
getStatus() {
this.loadingLastCompletedTime = true;
@ -122,11 +164,11 @@ export class SetJobComponent implements OnInit, OnDestroy {
};
if (purgeHistory && purgeHistory.job_parameters) {
const obj = JSON.parse(purgeHistory.job_parameters);
if (obj?.include_operations) {
this.selectedOperations =
obj?.include_operations?.split(',');
if (obj?.include_event_types) {
this.selectedEventTypes =
obj?.include_event_types?.split(',');
} else {
this.selectedOperations = [];
this.selectedEventTypes = [];
}
if (
obj?.audit_retention_hour > ONE_DAY &&
@ -140,7 +182,7 @@ export class SetJobComponent implements OnInit, OnDestroy {
}
} else {
this.retentionTime = null;
this.selectedOperations = clone(RETENTION_OPERATIONS);
this.selectedEventTypes = clone([]);
this.retentionUnit = RetentionTimeUnit.DAYS;
}
} else {
@ -165,7 +207,7 @@ export class SetJobComponent implements OnInit, OnDestroy {
schedule: {
parameters: {
audit_retention_hour: +retentionTime,
include_operations: this.selectedOperations.join(','),
include_event_types: this.selectedEventTypes.join(','),
dry_run: false,
},
schedule: {
@ -195,7 +237,7 @@ export class SetJobComponent implements OnInit, OnDestroy {
schedule: {
parameters: {
audit_retention_hour: +retentionTime,
include_operations: this.selectedOperations.join(','),
include_event_types: this.selectedEventTypes.join(','),
dry_run: true,
},
schedule: {
@ -231,8 +273,8 @@ export class SetJobComponent implements OnInit, OnDestroy {
schedule: {
parameters: {
audit_retention_hour: +retentionTime,
include_operations:
this.selectedOperations.join(','),
include_event_types:
this.selectedEventTypes.join(','),
dry_run: false,
},
schedule: {
@ -259,8 +301,8 @@ export class SetJobComponent implements OnInit, OnDestroy {
schedule: {
parameters: {
audit_retention_hour: +retentionTime,
include_operations:
this.selectedOperations.join(','),
include_event_types:
this.selectedEventTypes.join(','),
dry_run: false,
},
schedule: {
@ -283,21 +325,16 @@ export class SetJobComponent implements OnInit, OnDestroy {
});
}
}
hasOperation(operation: string): boolean {
return this.selectedOperations?.indexOf(operation) !== -1;
hasEventType(eventType: string): boolean {
return this.selectedEventTypes?.indexOf(eventType) !== -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);
setEventType(eventType: string) {
if (this.selectedEventTypes.indexOf(eventType) === -1) {
this.selectedEventTypes.push(eventType);
} else {
this.selectedOperations.splice(
this.selectedOperations.findIndex(item => item === operation),
this.selectedEventTypes.splice(
this.selectedEventTypes.findIndex(item => item === eventType),
1
);
}
@ -311,7 +348,7 @@ export class SetJobComponent implements OnInit, OnDestroy {
return true;
}
return !(
this.purgeForm?.invalid || !(this.selectedOperations?.length > 0)
this.purgeForm?.invalid || !(this.selectedEventTypes?.length > 0)
);
}
isRetentionTimeValid() {

View File

@ -3,12 +3,19 @@ export enum RetentionTimeUnit {
DAYS = 'days',
}
export const RETENTION_OPERATIONS = ['create', 'delete', 'pull'];
export const RESOURCE_TYPES = [
'create_artifact',
'delete_artifact',
'pull_artifact',
];
export const RETENTION_OPERATIONS_I18N_MAP = {
pull: 'AUDIT_LOG.PULL',
create: 'AUDIT_LOG.CREATE',
delete: 'AUDIT_LOG.DELETE',
export const RESOURCE_TYPES_I18N_MAP = {
artifact: 'AUDIT_LOG.ARTIFACT',
user_login_logout: 'AUDIT_LOG.USER_LOGIN_LOGOUT',
user: 'AUDIT_LOG.USER',
project: 'AUDIT_LOG.PROJECT',
configuration: 'AUDIT_LOG.CONFIGURATION',
project_member: 'AUDIT_LOG.PROJECT_MEMBER',
};
export const JOB_STATUS = {

View File

@ -85,6 +85,10 @@ export class ConfigService {
);
this._currentConfig.oidc_client_secret =
new StringValueItem(fakePass, true);
if (!this._currentConfig.disabled_audit_log_event_types) {
this._currentConfig.disabled_audit_log_event_types =
new StringValueItem('', true);
}
// Keep the original copy of the data
this._originalConfig = clone(this._currentConfig);
},

View File

@ -112,6 +112,7 @@ export class Configuration {
oidc_admin_group: StringValueItem;
oidc_group_filter: StringValueItem;
audit_log_forward_endpoint: StringValueItem;
disabled_audit_log_event_types: StringValueItem;
skip_audit_log_database: BoolValueItem;
session_timeout: NumberValueItem;
scanner_skip_update_pulltime: BoolValueItem;
@ -189,6 +190,7 @@ export class Configuration {
this.count_per_project = new NumberValueItem(-1, true);
this.storage_per_project = new NumberValueItem(-1, true);
this.audit_log_forward_endpoint = new StringValueItem('', true);
this.disabled_audit_log_event_types = new StringValueItem('', true);
this.skip_audit_log_database = new BoolValueItem(false, true);
this.session_timeout = new NumberValueItem(60, true);
this.scanner_skip_update_pulltime = new BoolValueItem(false, true);

View File

@ -315,6 +315,50 @@
!currentConfig?.audit_log_forward_endpoint?.editable
" />
</clr-input-container>
<div class="clr-form-control">
<label for="disableAuditLogEventList" class="clr-control-label">
{{ 'CLEARANCES.DISABLE_AUDIT_LOG_EVENT_TYPE' | translate }}
<clr-tooltip>
<clr-icon
clrTooltipTrigger
shape="info-circle"
size="24"></clr-icon>
<clr-tooltip-content
clrPosition="top-right"
clrSize="lg"
*clrIfOpen>
<span>{{
'CLEARANCES.DISABLE_AUDIT_LOG_EVENT_TYPE_TOOLTIP'
| translate
}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
<div *ngIf="logEventTypes.length === 0">
{{ 'CLEARANCES.AUDIT_LOG_EVENT_TYPE_EMPTY' | translate }}
</div>
<div class="clr-control-container">
<div
class="clr-checkbox-wrapper float-left"
*ngFor="let item of logEventTypes">
<input
type="checkbox"
id="{{ item.id }}"
name="logEventTypes"
value="{{ item.value }}"
class="clr-checkbox"
[disabled]="
!currentConfig?.disabled_audit_log_event_types
?.editable
"
(change)="setLogEventType(item.value)"
[checked]="hasLogEventType(item.value)" />
<label for="{{ item.id }}" class="clr-control-label">{{
item.label
}}</label>
</div>
</div>
</div>
<clr-checkbox-container class="center">
<label for="skipAuditLogDatabase"
>{{ 'CLEARANCES.SKIP_DATABASE' | translate }}

View File

@ -212,3 +212,11 @@ $message-type-width: 12rem;
.date {
width: 6rem;
}
.float-left {
float:left;
label {
min-width: 7rem;
}
}

View File

@ -1,11 +1,13 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SystemSettingsComponent } from './system-settings.component';
import { ErrorHandler } from '../../../../shared/units/error-handler';
import { of } from 'rxjs';
import { delay, of, Subscription } from 'rxjs';
import { Configuration } from '../config';
import { SharedTestingModule } from '../../../../shared/shared.module';
import { ConfigService } from '../config.service';
import { AppConfigService } from '../../../../services/app-config.service';
import { AuditlogService } from 'ng-swagger-gen/services';
import { HttpHeaders, HttpResponse } from '@angular/common/http';
describe('SystemSettingsComponent', () => {
let component: SystemSettingsComponent;
@ -39,19 +41,50 @@ describe('SystemSettingsComponent', () => {
return of(null);
},
};
beforeEach(() => {
TestBed.configureTestingModule({
const mockedAuditLogs = [
{
event_type: 'create_artifact',
},
{
event_type: 'delete_artifact',
},
{
event_type: 'pull_artifact',
},
];
const fakeAuditlogService = {
listAuditLogEventTypesResponse() {
return of(
new HttpResponse({
body: mockedAuditLogs,
headers: new HttpHeaders({
'x-total-count': '18',
}),
})
).pipe(delay(0));
},
};
const fakedErrorHandler = {
error() {
return undefined;
},
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SharedTestingModule],
providers: [
{ provide: AppConfigService, useValue: fakedAppConfigService },
{ provide: ErrorHandler, useValue: fakedErrorHandler },
{ provide: ConfigService, useValue: fakeConfigService },
{ provide: AuditlogService, useValue: fakeAuditlogService },
],
declarations: [SystemSettingsComponent],
});
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SystemSettingsComponent);
component = fixture.componentInstance;
component.selectedLogEventTypes = ['create_artifact'];
fixture.autoDetectChanges(true);
});

View File

@ -1,4 +1,11 @@
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import {
AfterViewChecked,
ChangeDetectorRef,
Component,
OnDestroy,
OnInit,
ViewChild,
} from '@angular/core';
import { NgForm } from '@angular/forms';
import {
BannerMessage,
@ -7,6 +14,7 @@ import {
Configuration,
} from '../config';
import {
clone,
CURRENT_BASE_HREF,
getChanges,
isEmpty,
@ -20,15 +28,20 @@ import {
HarborEvent,
} from '../../../../services/event-service/event.service';
import { Subscription } from 'rxjs';
import { AuditlogService } from 'ng-swagger-gen/services';
import { AuditLogEventType } from 'ng-swagger-gen/models';
@Component({
selector: 'system-settings',
templateUrl: './system-settings.component.html',
styleUrls: ['./system-settings.component.scss'],
})
export class SystemSettingsComponent implements OnInit, OnDestroy {
export class SystemSettingsComponent
implements OnInit, OnDestroy, AfterViewChecked
{
bannerMessageTypes: string[] = Object.values(BannerMessageType);
onGoing = false;
loading = false;
downloadLink: string;
get currentConfig(): Configuration {
return this.conf.getConfig();
@ -51,13 +64,17 @@ export class SystemSettingsComponent implements OnInit, OnDestroy {
messageToDateCopy: Date;
bannerRefreshSub: Subscription;
currentDate: Date = new Date();
logEventTypes: Record<string, string>[] = [];
selectedLogEventTypes: string[] = clone([]);
@ViewChild('systemConfigFrom') systemSettingsForm: NgForm;
constructor(
private appConfigService: AppConfigService,
private errorHandler: MessageHandlerService,
private conf: ConfigService,
private event: EventService
private logService: AuditlogService,
private event: EventService,
private errorHandler: MessageHandlerService,
private changeDetectorRef: ChangeDetectorRef
) {
this.downloadLink = CURRENT_BASE_HREF + '/systeminfo/getcert';
}
@ -65,16 +82,23 @@ export class SystemSettingsComponent implements OnInit, OnDestroy {
ngOnInit() {
this.conf.resetConfig();
if (!this.bannerRefreshSub) {
this.bannerRefreshSub = this.event.subscribe(
this.bannerRefreshSub = this.event?.subscribe(
HarborEvent.REFRESH_BANNER_MESSAGE,
() => {
this.setValueForBannerMessage();
this.setValueForDisabledAuditLogEventTypes();
}
);
}
if (this.currentConfig.banner_message) {
this.setValueForBannerMessage();
}
this.initLogEventTypes();
this.setValueForDisabledAuditLogEventTypes();
}
ngAfterViewChecked() {
this.changeDetectorRef.detectChanges();
}
ngOnDestroy() {
@ -84,6 +108,36 @@ export class SystemSettingsComponent implements OnInit, OnDestroy {
}
}
initLogEventTypes() {
this.loading = true;
this.logService
.listAuditLogEventTypesResponse()
.pipe(finalize(() => (this.loading = false)))
.subscribe(
response => {
const auditLogEventTypes =
response.body as AuditLogEventType[];
this.logEventTypes = auditLogEventTypes.map(event => ({
label:
event.event_type.charAt(0).toUpperCase() +
event.event_type.slice(1).replace(/_/g, ' '),
value: event.event_type,
id: event.event_type,
}));
},
error => {
this.errorHandler.error(error);
}
);
}
setValueForDisabledAuditLogEventTypes() {
const checkedEventTypes =
this.currentConfig?.disabled_audit_log_event_types?.value;
this.selectedLogEventTypes =
checkedEventTypes?.split(',')?.filter(evt => evt !== '') ?? [];
}
setValueForBannerMessage() {
if (this.currentConfig.banner_message.value) {
this.messageText = (
@ -165,6 +219,25 @@ export class SystemSettingsComponent implements OnInit, OnDestroy {
);
}
hasLogEventType(resourceType: string): boolean {
return this.selectedLogEventTypes?.indexOf(resourceType) !== -1;
}
setLogEventType(resourceType: string) {
if (this.selectedLogEventTypes.indexOf(resourceType) === -1) {
this.selectedLogEventTypes.push(resourceType);
} else {
this.selectedLogEventTypes.splice(
this.selectedLogEventTypes.findIndex(
item => item === resourceType
),
1
);
}
this.currentConfig.disabled_audit_log_event_types.value =
this.selectedLogEventTypes.join(',');
}
public getChanges() {
let allChanges = getChanges(
this.conf.getOriginalConfig(),
@ -190,6 +263,7 @@ export class SystemSettingsComponent implements OnInit, OnDestroy {
prop === 'skip_audit_log_database' ||
prop === 'session_timeout' ||
prop === 'scanner_skip_update_pulltime' ||
prop === 'disabled_audit_log_event_types' ||
prop === 'banner_message'
) {
changes[prop] = allChanges[prop];

View File

@ -0,0 +1,96 @@
<div>
<div class="row flex-items-xs-between flex-items-xs-bottom">
<div></div>
<div class="action-head-pos">
<div
class="select filter-tag clr-select-wrapper"
[hidden]="!isOpenFilterTag">
<select id="selectKey" (change)="selectFilterKey($event)">
<option value="username">
{{ 'AUDIT_LOG.USERNAME' | translate | lowercase }}
</option>
<option value="resource">
{{ 'AUDIT_LOG.RESOURCE' | translate | lowercase }}
</option>
<option value="resource_type">
{{ 'AUDIT_LOG.RESOURCE_TYPE' | translate | lowercase }}
</option>
<option value="operation">
{{ 'AUDIT_LOG.OPERATION' | translate | lowercase }}
</option>
</select>
</div>
<hbr-filter
[withDivider]="true"
filterPlaceholder="{{
'AUDIT_LOG.FILTER_PLACEHOLDER' | translate
}}"
(filterEvt)="doFilter($event)"
(openFlag)="openFilter($event)"
[currentValue]="currentTerm"></hbr-filter>
<span (click)="refresh()" class="refresh-btn">
<clr-icon
shape="refresh"
[hidden]="inProgress"
ng-disabled="inProgress"></clr-icon>
<span
class="spinner spinner-inline"
[hidden]="!inProgress"></span>
</span>
</div>
</div>
<div>
<clr-datagrid (clrDgRefresh)="load($event)" [clrDgLoading]="loading">
<clr-dg-column class="width-140">{{
'AUDIT_LOG.TIMESTAMP' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'AUDIT_LOG.USERNAME' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'AUDIT_LOG.RESOURCE' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'AUDIT_LOG.RESOURCE_TYPE' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'AUDIT_LOG.OPERATION' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'AUDIT_LOG.OPERATION_DESCRIPTION' | translate
}}</clr-dg-column>
<clr-dg-column>{{ 'AUDIT_LOG.RESULT' | translate }}</clr-dg-column>
<clr-dg-placeholder>{{
'AUDIT_LOG.NOT_FOUND' | translate
}}</clr-dg-placeholder>
<clr-dg-row *ngFor="let l of recentLogs">
<clr-dg-cell>{{
l.op_time | harborDatetime : 'short'
}}</clr-dg-cell>
<clr-dg-cell>{{ l.username }}</clr-dg-cell>
<clr-dg-cell>{{ l.resource }}</clr-dg-cell>
<clr-dg-cell>{{ l.resource_type }}</clr-dg-cell>
<clr-dg-cell>{{ l.operation }}</clr-dg-cell>
<clr-dg-cell>{{ l.operation_description }}</clr-dg-cell>
<clr-dg-cell>{{ l.operation_result }}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination
#pagination
[(clrDgPage)]="currentPage"
[clrDgPageSize]="pageSize"
[clrDgTotalItems]="totalCount">
<clr-dg-page-size [clrPageSizeOptions]="[15, 25, 50]">{{
'PAGINATION.PAGE_SIZE' | translate
}}</clr-dg-page-size>
<span *ngIf="totalCount"
>{{ pagination.firstItem + 1 }} -
{{ pagination.lastItem + 1 }}
{{ 'AUDIT_LOG.OF' | translate }} {{ totalCount }}
{{ 'AUDIT_LOG.ITEMS' | translate }}</span
>
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -0,0 +1,62 @@
.h2-log-override {
margin-top: 0 !important;
}
.action-head-pos {
padding-right: 18px;
height: 24px;
display: flex;
justify-content: flex-end;
}
.refresh-btn {
cursor: pointer;
margin-top: 7px;
}
.refresh-btn:hover {
color: #007CBB;
}
.custom-lines-button {
padding: 0 !important;
min-width: 25px !important;
}
.lines-button-toggole {
font-size: 16px;
text-decoration: underline;
}
.log-select {
width: 130px;
display: inline-block;
top: 1px;
}
.item-divider {
height: 24px;
display: inline-block;
width: 1px;
background-color: #ccc;
opacity: 0.55;
margin-left: 12px;
top: 8px;
position: relative;
}
/* stylelint-disable */
.rightPos {
position: absolute;
z-index: 100;
right: 35px;
margin-top: 4px;
}
.filter-tag {
float: left;
margin-top: 8px;
}
.width-140 {
width: 140px;
}

View File

@ -0,0 +1,191 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { AuditLogComponent } from './audit-log.component';
import { ErrorHandler } from '../../../shared/units/error-handler';
import { FilterComponent } from '../../../shared/components/filter/filter.component';
import { click } from '../../../shared/units/utils';
import { of } from 'rxjs';
import { AuditLogExt } from '../../../../../ng-swagger-gen/models/audit-log-ext';
import { AuditlogService } from '../../../../../ng-swagger-gen/services/auditlog.service';
import { HttpHeaders, HttpResponse } from '@angular/common/http';
import { delay } from 'rxjs/operators';
import { SharedTestingModule } from '../../../shared/shared.module';
describe('AuditLogComponent (inline template)', () => {
let component: AuditLogComponent;
let fixture: ComponentFixture<AuditLogComponent>;
let auditlogService: AuditlogService;
const fakedErrorHandler = {
error() {
return undefined;
},
};
const mockedAuditLogs: AuditLogExt[] = [];
for (let i = 0; i < 18; i++) {
let item: AuditLogExt = {
id: 23 + i,
resource: 'myproject/demo' + i,
resource_type: 'N/A',
operation: 'create',
op_time: '2017-04-11T10:26:22Z',
username: 'user91' + i,
};
mockedAuditLogs.push(item);
}
const fakedAuditlogExtsService = {
listAuditLogExtsResponse(
params: AuditlogService.ListAuditLogExtsParams
) {
if (params && params.q) {
if (params.q.indexOf('demo0') !== -1) {
return of(
new HttpResponse({
body: mockedAuditLogs.slice(0, 1),
headers: new HttpHeaders({
'x-total-count': '18',
}),
})
).pipe(delay(0));
}
return of(
new HttpResponse({
body: mockedAuditLogs,
headers: new HttpHeaders({
'x-total-count': '18',
}),
})
).pipe(delay(0));
} else {
if (params.page === 1) {
return of(
new HttpResponse({
body: mockedAuditLogs.slice(0, 15),
headers: new HttpHeaders({
'x-total-count': '18',
}),
})
).pipe(delay(0));
} else {
return of(
new HttpResponse({
body: mockedAuditLogs.slice(15),
headers: new HttpHeaders({
'x-total-count': '18',
}),
})
).pipe(delay(0));
}
}
},
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SharedTestingModule],
declarations: [FilterComponent, AuditLogComponent],
providers: [
{ provide: ErrorHandler, useValue: fakedErrorHandler },
{
provide: AuditlogService,
useValue: fakedAuditlogExtsService,
},
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AuditLogComponent);
component = fixture.componentInstance;
auditlogService = fixture.debugElement.injector.get(AuditlogService);
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
it('should get data from AccessLogService', () => {
expect(auditlogService).toBeTruthy();
fixture.detectChanges();
fixture.whenStable().then(() => {
// wait for async getRecentLogs
fixture.detectChanges();
expect(component.recentLogs).toBeTruthy();
expect(component.recentLogs.length).toEqual(15);
});
});
it('should render data to view', () => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let de: DebugElement = fixture.debugElement.query(
del => del.classes['datagrid-cell']
);
expect(de).toBeTruthy();
let el: HTMLElement = de.nativeElement;
expect(el).toBeTruthy();
expect(el.textContent.trim()).toEqual('user910');
});
});
it('should support pagination', async () => {
fixture.autoDetectChanges(true);
await fixture.whenStable();
let el: HTMLButtonElement =
fixture.nativeElement.querySelector('.pagination-next');
expect(el).toBeTruthy();
el.click();
fixture.detectChanges();
await fixture.whenStable();
expect(component.currentPage).toEqual(2);
expect(component.recentLogs.length).toEqual(3);
});
it('should support filtering list by keywords', () => {
fixture.detectChanges();
let el: HTMLElement =
fixture.nativeElement.querySelector('.search-btn');
expect(el).toBeTruthy('Not found search icon');
click(el);
fixture.detectChanges();
let el2: HTMLInputElement =
fixture.nativeElement.querySelector('input');
expect(el2).toBeTruthy('Not found input');
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
component.doFilter('demo0');
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(component.recentLogs).toBeTruthy();
expect(component.recentLogs.length).toEqual(1);
});
});
});
it('should support refreshing', () => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLButtonElement =
fixture.nativeElement.querySelector('.pagination-next');
expect(el).toBeTruthy();
el.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(component.recentLogs).toBeTruthy();
expect(component.recentLogs.length).toEqual(3);
let refreshEl: HTMLElement =
fixture.nativeElement.querySelector('.refresh-btn');
expect(refreshEl).toBeTruthy('Not found refresh button');
refreshEl.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(component.recentLogs).toBeTruthy();
expect(component.recentLogs.length).toEqual(15);
});
});
});
});
});

View File

@ -0,0 +1,118 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { ErrorHandler } from '../../../shared/units/error-handler';
import { finalize } from 'rxjs/operators';
import { AuditlogService } from '../../../../../ng-swagger-gen/services/auditlog.service';
import { AuditLogExt } from '../../../../../ng-swagger-gen/models/audit-log-ext';
import { ClrDatagridStateInterface } from '@clr/angular';
import {
getPageSizeFromLocalStorage,
PageSizeMapKeys,
setPageSizeToLocalStorage,
} from '../../../shared/units/utils';
import ListAuditLogExtsParams = AuditlogService.ListAuditLogExtsParams;
@Component({
selector: 'hbr-audit-log',
templateUrl: './audit-log.component.html',
styleUrls: ['./audit-log.component.scss'],
})
export class AuditLogComponent {
recentLogs: AuditLogExt[] = [];
loading: boolean = true;
currentTerm: string;
defaultFilter = 'username';
isOpenFilterTag: boolean;
pageSize: number = getPageSizeFromLocalStorage(
PageSizeMapKeys.SYSTEM_RECENT_LOG_COMPONENT
);
currentPage: number = 1; // Double bound to pagination component
totalCount: number = 0;
constructor(
private logService: AuditlogService,
private errorHandler: ErrorHandler
) {}
public get inProgress(): boolean {
return this.loading;
}
public doFilter(terms: string): void {
// allow search by null characters
if (terms === undefined || terms === null) {
return;
}
this.currentTerm = terms.trim();
this.loading = true;
this.currentPage = 1;
this.totalCount = 0;
this.load();
}
public refresh(): void {
this.doFilter('');
}
openFilter(isOpen: boolean): void {
this.isOpenFilterTag = isOpen;
}
selectFilterKey($event: any): void {
this.defaultFilter = $event['target'].value;
this.doFilter(this.currentTerm);
}
load(state?: ClrDatagridStateInterface) {
if (state && state.page) {
this.pageSize = state.page.size;
setPageSizeToLocalStorage(
PageSizeMapKeys.SYSTEM_RECENT_LOG_COMPONENT,
this.pageSize
);
}
// Keep it for future filter
// this.currentState = state;
const params: ListAuditLogExtsParams = {
page: this.currentPage,
pageSize: this.pageSize,
};
if (this.currentTerm && this.currentTerm !== '') {
params.q = encodeURIComponent(
`${this.defaultFilter}=~${this.currentTerm}`
);
}
this.loading = true;
this.logService
.listAuditLogExtsResponse(params)
.pipe(finalize(() => (this.loading = false)))
.subscribe(
response => {
// Get total count
if (response.headers) {
let xHeader: string =
response.headers.get('x-total-count');
if (xHeader) {
this.totalCount = parseInt(xHeader, 0);
}
}
this.recentLogs = response.body as AuditLogExt[];
},
error => {
this.errorHandler.error(error);
}
);
}
}

View File

@ -15,15 +15,32 @@ import { NgModule } from '@angular/core';
import { SharedModule } from '../../../shared/shared.module';
import { RecentLogComponent } from './recent-log.component';
import { RouterModule, Routes } from '@angular/router';
import { LogsComponent } from './logs.component';
import { AuditLogComponent } from './audit-log.component';
const routes: Routes = [
{
path: '',
component: RecentLogComponent,
component: LogsComponent,
children: [
{
path: 'audit-log',
component: AuditLogComponent,
},
{
path: 'audit-legacy-log',
component: RecentLogComponent,
},
{
path: '',
redirectTo: 'audit-log',
pathMatch: 'full',
},
],
},
];
@NgModule({
imports: [SharedModule, RouterModule.forChild(routes)],
declarations: [RecentLogComponent],
declarations: [LogsComponent, AuditLogComponent, RecentLogComponent],
})
export class LogModule {}

View File

@ -0,0 +1,24 @@
<h2 class="custom-h2" sub-header-title>
{{ 'SIDE_NAV.LOGS' | translate }}
</h2>
<nav class="mt-1">
<ul class="nav">
<li class="nav-item">
<a
class="nav-link"
routerLink="audit-log"
routerLinkActive="active"
>{{ 'SIDE_NAV.AUDIT_LOGS' | translate }}</a
>
</li>
<li class="nav-item">
<a
class="nav-link"
routerLink="audit-legacy-log"
routerLinkActive="active"
>{{ 'SIDE_NAV.LEGACY_LOGS' | translate }}</a
>
</li>
</ul>
</nav>
<router-outlet></router-outlet>

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-logs',
templateUrl: './logs.component.html',
styleUrls: ['./logs.component.scss'],
})
export class LogsComponent {
inProgress: boolean = true;
constructor() {}
}

View File

@ -1,7 +1,4 @@
<div>
<h2 class="custom-h2" sub-header-title>
{{ 'SIDE_NAV.LOGS' | translate }}
</h2>
<div class="row flex-items-xs-between flex-items-xs-bottom">
<div></div>
<div class="action-head-pos">

View File

@ -0,0 +1,103 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 reverse-row log-top">
<div class="row flex-items-xs-right option-right display-f">
<div class="flex-xs-middle">
<button
class="btn btn-link"
(click)="toggleOptionalName(currentOption)">
{{ toggleName[currentOption] | translate }}
</button>
<hbr-filter
[withDivider]="true"
filterPlaceholder="{{
'AUDIT_LOG.FILTER_PLACEHOLDER' | translate
}}"
(filterEvt)="doSearchAuditLogs($event)"></hbr-filter>
<span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</span>
</div>
</div>
<div
class="row flex-items-xs-right row-right"
[hidden]="currentOption === 0">
<clr-dropdown>
<button class="btn btn-link" clrDropdownToggle>
{{ 'AUDIT_LOG.OPERATIONS' | translate }}
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu [clrPosition]="'bottom-left'" *clrIfOpen>
<a
href="javascript:void(0)"
clrDropdownItem
*ngFor="let f of filterOptions"
(click)="toggleFilterOption(f.key)">
<clr-icon
shape="check"
[hidden]="!f.checked"></clr-icon>
<ng-template [ngIf]="!f.checked"
><span class="check-span"></span
></ng-template>
{{ f.description | translate }}
</a>
</clr-dropdown-menu>
</clr-dropdown>
<div class="flex-xs-middle">
<hbr-datetime
[dateInput]="search.startTime"
(search)="doSearchByStartTime($event)"></hbr-datetime>
<hbr-datetime
[dateInput]="search.endTime"
[oneDayOffset]="true"
(search)="doSearchByEndTime($event)"></hbr-datetime>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 datagrid-margin-top">
<clr-datagrid
[clrDgLoading]="loading"
(clrDgRefresh)="retrieve($event)">
<clr-dg-column>{{
'AUDIT_LOG.USERNAME' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'AUDIT_LOG.RESOURCE' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'AUDIT_LOG.RESOURCE_TYPE' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'AUDIT_LOG.OPERATION' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'AUDIT_LOG.TIMESTAMP' | translate
}}</clr-dg-column>
<clr-dg-row *ngFor="let l of auditLogs">
<clr-dg-cell>{{ l.username }}</clr-dg-cell>
<clr-dg-cell>{{ l.resource }}</clr-dg-cell>
<clr-dg-cell>{{ l.resource_type }}</clr-dg-cell>
<clr-dg-cell>{{ l.operation }}</clr-dg-cell>
<clr-dg-cell>{{
l.op_time | harborDatetime : 'short'
}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination
#pagination
[clrDgPageSize]="pageSize"
[(clrDgPage)]="currentPage"
[clrDgTotalItems]="totalRecordCount">
<clr-dg-page-size [clrPageSizeOptions]="[15, 25, 50]">{{
'PAGINATION.PAGE_SIZE' | translate
}}</clr-dg-page-size>
<span *ngIf="showPaginationIndex"
>{{ pagination.firstItem + 1 }} -
{{ pagination.lastItem + 1 }}
{{ 'AUDIT_LOG.OF' | translate }}
</span>
{{ totalRecordCount }} {{ 'AUDIT_LOG.ITEMS' | translate }}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -0,0 +1,47 @@
.option-right {
padding-right: 16px;
}
.refresh-btn {
cursor: pointer;
}
.refresh-btn:hover {
color: #007CBB;
}
.row-right {
padding-right: 60px;
}
.log-top {
top: 12px;
}
.check-span {
display: inline-block;
width: 16px;
}
.display-f{
display: flex;
}
.reverse-row {
display: flex;
flex-direction: row-reverse;
align-items: baseline;
}
.flex-items-xs-right {
display: flex;
align-items: baseline;
::ng-deep {
clr-date-container{
margin-top: 0;
}
}
}

View File

@ -0,0 +1,182 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectAuditLegacyLogComponent } from './audit-legacy-log.component';
import { MessageHandlerService } from '../../../shared/services/message-handler.service';
import { ActivatedRoute, Router } from '@angular/router';
import { of } from 'rxjs';
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement, LOCALE_ID } from '@angular/core';
import { delay } from 'rxjs/operators';
import { AuditLog } from '../../../../../ng-swagger-gen/models/audit-log';
import { HttpHeaders, HttpResponse } from '@angular/common/http';
import { ProjectService } from '../../../../../ng-swagger-gen/services/project.service';
import { click } from '../../../shared/units/utils';
import { SharedTestingModule } from '../../../shared/shared.module';
import { registerLocaleData } from '@angular/common';
import locale_en from '@angular/common/locales/en';
import { DatePickerComponent } from '../../../shared/components/datetime-picker/datetime-picker.component';
describe('ProjectAuditLegacyLogComponent', () => {
let component: ProjectAuditLegacyLogComponent;
let fixture: ComponentFixture<ProjectAuditLegacyLogComponent>;
const mockMessageHandlerService = {
handleError: () => {},
};
const mockActivatedRoute = {
parent: {
parent: {
parent: {
snapshot: {
data: null,
},
},
},
},
snapshot: {
data: null,
},
data: of({
auditLogResolver: '',
}).pipe(delay(0)),
};
const mockRouter = null;
const mockedAuditLogs: AuditLog[] = [];
for (let i = 0; i < 18; i++) {
let item: AuditLog = {
id: 234 + i,
resource: 'myProject/Demo' + i,
resource_type: 'N/A',
operation: 'create',
op_time: '2017-04-11T10:26:22Z',
username: 'user91' + i,
};
mockedAuditLogs.push(item);
}
const fakedAuditlogService = {
getLogsResponse(params: ProjectService.GetLogsParams) {
if (params.q && params.q.indexOf('Demo0') !== -1) {
return of(
new HttpResponse({
body: mockedAuditLogs.slice(0, 1),
headers: new HttpHeaders({
'x-total-count': '18',
}),
})
).pipe(delay(0));
}
if (params.page <= 1) {
return of(
new HttpResponse({
body: mockedAuditLogs.slice(0, 15),
headers: new HttpHeaders({
'x-total-count': '18',
}),
})
).pipe(delay(0));
} else {
return of(
new HttpResponse({
body: mockedAuditLogs.slice(15),
headers: new HttpHeaders({
'x-total-count': '18',
}),
})
).pipe(delay(0));
}
},
};
registerLocaleData(locale_en, 'en-us');
beforeEach(async () => {
TestBed.overrideComponent(DatePickerComponent, {
set: {
providers: [
{
provide: LOCALE_ID,
useValue: 'en-us',
},
],
},
});
await TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [SharedTestingModule],
declarations: [ProjectAuditLegacyLogComponent],
providers: [
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
{ provide: Router, useValue: mockRouter },
{ provide: ProjectService, useValue: fakedAuditlogService },
{
provide: MessageHandlerService,
useValue: mockMessageHandlerService,
},
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ProjectAuditLegacyLogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should get data from AccessLogService', () => {
fixture.detectChanges();
fixture.whenStable().then(() => {
// wait for async getRecentLogs
fixture.detectChanges();
expect(component.auditLogs).toBeTruthy();
expect(component.auditLogs.length).toEqual(15);
});
});
it('should render data to view', () => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let de: DebugElement = fixture.debugElement.query(
del => del.classes['datagrid-cell']
);
expect(de).toBeTruthy();
let el: HTMLElement = de.nativeElement;
expect(el).toBeTruthy();
expect(el.textContent.trim()).toEqual('user910');
});
});
it('should support pagination', async () => {
fixture.autoDetectChanges(true);
await fixture.whenStable();
let el: HTMLButtonElement =
fixture.nativeElement.querySelector('.pagination-next');
expect(el).toBeTruthy();
el.click();
fixture.detectChanges();
await fixture.whenStable();
expect(component.currentPage).toEqual(2);
expect(component.auditLogs.length).toEqual(3);
});
it('should support filtering list by keywords', () => {
fixture.detectChanges();
let el: HTMLElement =
fixture.nativeElement.querySelector('.search-btn');
expect(el).toBeTruthy('Not found search icon');
click(el);
fixture.detectChanges();
let el2: HTMLInputElement =
fixture.nativeElement.querySelector('input');
expect(el2).toBeTruthy('Not found input');
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
component.doSearchAuditLogs('Demo0');
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(component.auditLogs).toBeTruthy();
expect(component.auditLogs.length).toEqual(1);
});
});
});
});

View File

@ -0,0 +1,247 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { SessionUser } from '../../../shared/entities/session-user';
import { MessageHandlerService } from '../../../shared/services/message-handler.service';
import { ProjectService } from '../../../../../ng-swagger-gen/services/project.service';
import { AuditLog } from '../../../../../ng-swagger-gen/models/audit-log';
import { Project } from '../project';
import { finalize } from 'rxjs/operators';
import {
getPageSizeFromLocalStorage,
PageSizeMapKeys,
setPageSizeToLocalStorage,
} from '../../../shared/units/utils';
import { ClrDatagridStateInterface } from '@clr/angular';
const optionalSearch: {} = { 0: 'AUDIT_LOG.ADVANCED', 1: 'AUDIT_LOG.SIMPLE' };
class FilterOption {
key: string;
description: string;
checked: boolean;
constructor(
private iKey: string,
private iDescription: string,
private iChecked: boolean
) {
this.key = iKey;
this.description = iDescription;
this.checked = iChecked;
}
toString(): string {
return (
'key:' +
this.key +
', description:' +
this.description +
', checked:' +
this.checked +
'\n'
);
}
}
export class SearchOption {
startTime: string = '';
endTime: string = '';
}
@Component({
selector: 'project-audit-legacy-log',
templateUrl: './audit-legacy-log.component.html',
styleUrls: ['./audit-legacy-log.component.scss'],
})
export class ProjectAuditLegacyLogComponent implements OnInit {
search: SearchOption = new SearchOption();
currentUser: SessionUser;
projectId: number;
projectName: string;
queryUsername: string;
queryStartTime: string;
queryEndTime: string;
queryOperation: string[] = [];
auditLogs: AuditLog[];
loading: boolean = true;
toggleName = optionalSearch;
currentOption: number = 0;
filterOptions: FilterOption[] = [
new FilterOption('all', 'AUDIT_LOG.ALL_OPERATIONS', true),
new FilterOption('pull', 'AUDIT_LOG.PULL', true),
new FilterOption('create', 'AUDIT_LOG.CREATE', true),
new FilterOption('delete', 'AUDIT_LOG.DELETE', true),
new FilterOption('others', 'AUDIT_LOG.OTHERS', true),
];
pageOffset = 1;
pageSize = getPageSizeFromLocalStorage(
PageSizeMapKeys.PROJECT_AUDIT_LOG_COMPONENT
);
totalRecordCount = 0;
currentPage = 1;
totalPage = 0;
get showPaginationIndex(): boolean {
return this.totalRecordCount > 0;
}
constructor(
private route: ActivatedRoute,
private router: Router,
private auditLogService: ProjectService,
private messageHandlerService: MessageHandlerService
) {
// Get current user from registered resolver.
this.route.data.subscribe(
data => (this.currentUser = <SessionUser>data['auditLogResolver'])
);
}
ngOnInit(): void {
const resolverData = this.route.parent.parent.parent.snapshot.data;
if (resolverData) {
const pro: Project = <Project>resolverData['projectResolver'];
this.projectName = pro.name;
}
}
retrieve(state?: ClrDatagridStateInterface) {
if (state && state.page) {
this.pageSize = state.page.size;
setPageSizeToLocalStorage(
PageSizeMapKeys.PROJECT_AUDIT_LOG_COMPONENT,
this.pageSize
);
}
const arr: string[] = [];
if (this.queryUsername) {
arr.push(`username=~${this.queryUsername}`);
}
if (this.queryStartTime && this.queryEndTime) {
arr.push(`op_time=[${this.queryStartTime}~${this.queryEndTime}]`);
} else {
if (this.queryStartTime) {
arr.push(`op_time=[${this.queryStartTime}~]`);
}
if (this.queryEndTime) {
arr.push(`op_time=[~${this.queryEndTime}]`);
}
}
if (this.queryOperation && this.queryOperation.length > 0) {
arr.push(`operation={${this.queryOperation.join(' ')}}`);
}
const param: ProjectService.GetLogsParams = {
projectName: this.projectName,
pageSize: this.pageSize,
page: this.currentPage,
};
if (arr && arr.length > 0) {
param.q = encodeURIComponent(arr.join(','));
}
this.loading = true;
this.auditLogService
.getLogsResponse(param)
.pipe(finalize(() => (this.loading = false)))
.subscribe(
response => {
// Get total count
if (response.headers) {
let xHeader: string =
response.headers.get('x-total-count');
if (xHeader) {
this.totalRecordCount = Number.parseInt(
xHeader,
10
);
}
}
this.auditLogs = response.body;
},
error => {
this.messageHandlerService.handleError(error);
}
);
}
doSearchAuditLogs(searchUsername: string): void {
this.queryUsername = searchUsername;
this.retrieve();
}
doSearchByStartTime(fromTimestamp: string): void {
this.queryStartTime = fromTimestamp;
this.retrieve();
}
doSearchByEndTime(toTimestamp: string): void {
this.queryEndTime = toTimestamp;
this.retrieve();
}
doSearchByOptions() {
let selectAll = true;
let operationFilter: string[] = [];
for (let filterOption of this.filterOptions) {
if (filterOption.checked) {
operationFilter.push(filterOption.key);
} else {
selectAll = false;
}
}
if (selectAll) {
operationFilter = [];
}
this.queryOperation = operationFilter;
this.retrieve();
}
toggleOptionalName(option: number): void {
option === 1 ? (this.currentOption = 0) : (this.currentOption = 1);
}
toggleFilterOption(option: string): void {
let selectedOption = this.filterOptions.find(
value => value.key === option
);
selectedOption.checked = !selectedOption.checked;
if (selectedOption.key === 'all') {
this.filterOptions
.filter(value => value.key !== selectedOption.key)
.forEach(value => (value.checked = selectedOption.checked));
} else {
if (!selectedOption.checked) {
this.filterOptions.find(value => value.key === 'all').checked =
false;
}
let selectAll = true;
this.filterOptions
.filter(value => value.key !== 'all')
.forEach(value => {
if (!value.checked) {
selectAll = false;
}
});
this.filterOptions.find(value => value.key === 'all').checked =
selectAll;
}
this.doSearchByOptions();
}
refresh(): void {
this.retrieve();
}
}

View File

@ -57,6 +57,9 @@
<clr-datagrid
[clrDgLoading]="loading"
(clrDgRefresh)="retrieve($event)">
<clr-dg-column class="width-140">{{
'AUDIT_LOG.TIMESTAMP' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'AUDIT_LOG.USERNAME' | translate
}}</clr-dg-column>
@ -70,16 +73,22 @@
'AUDIT_LOG.OPERATION' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'AUDIT_LOG.TIMESTAMP' | translate
'AUDIT_LOG.OPERATION_DESCRIPTION' | translate
}}</clr-dg-column>
<clr-dg-column>{{ 'AUDIT_LOG.RESULT' | translate }}</clr-dg-column>
<clr-dg-placeholder>{{
'AUDIT_LOG.NOT_FOUND' | translate
}}</clr-dg-placeholder>
<clr-dg-row *ngFor="let l of auditLogs">
<clr-dg-cell>{{
l.op_time | harborDatetime : 'short'
}}</clr-dg-cell>
<clr-dg-cell>{{ l.username }}</clr-dg-cell>
<clr-dg-cell>{{ l.resource }}</clr-dg-cell>
<clr-dg-cell>{{ l.resource_type }}</clr-dg-cell>
<clr-dg-cell>{{ l.operation }}</clr-dg-cell>
<clr-dg-cell>{{
l.op_time | harborDatetime : 'short'
}}</clr-dg-cell>
<clr-dg-cell>{{ l.operation_description }}</clr-dg-cell>
<clr-dg-cell>{{ l.operation_result }}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination

View File

@ -42,4 +42,8 @@
margin-top: 0;
}
}
}
.width-140 {
width: 140px;
}

View File

@ -1,11 +1,11 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AuditLogComponent } from './audit-log.component';
import { ProjectAuditLogComponent } from './audit-log.component';
import { MessageHandlerService } from '../../../shared/services/message-handler.service';
import { ActivatedRoute, Router } from '@angular/router';
import { of } from 'rxjs';
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement, LOCALE_ID } from '@angular/core';
import { delay } from 'rxjs/operators';
import { AuditLog } from '../../../../../ng-swagger-gen/models/audit-log';
import { AuditLogExt } from '../../../../../ng-swagger-gen/models/audit-log-ext';
import { HttpHeaders, HttpResponse } from '@angular/common/http';
import { ProjectService } from '../../../../../ng-swagger-gen/services/project.service';
import { click } from '../../../shared/units/utils';
@ -14,17 +14,19 @@ import { registerLocaleData } from '@angular/common';
import locale_en from '@angular/common/locales/en';
import { DatePickerComponent } from '../../../shared/components/datetime-picker/datetime-picker.component';
describe('AuditLogComponent', () => {
let component: AuditLogComponent;
let fixture: ComponentFixture<AuditLogComponent>;
describe('ProjectAuditLogComponent', () => {
let component: ProjectAuditLogComponent;
let fixture: ComponentFixture<ProjectAuditLogComponent>;
const mockMessageHandlerService = {
handleError: () => {},
};
const mockActivatedRoute = {
parent: {
parent: {
snapshot: {
data: null,
parent: {
snapshot: {
data: null,
},
},
},
},
@ -36,9 +38,9 @@ describe('AuditLogComponent', () => {
}).pipe(delay(0)),
};
const mockRouter = null;
const mockedAuditLogs: AuditLog[] = [];
const mockedAuditLogExts: AuditLogExt[] = [];
for (let i = 0; i < 18; i++) {
let item: AuditLog = {
let item: AuditLogExt = {
id: 234 + i,
resource: 'myProject/Demo' + i,
resource_type: 'N/A',
@ -46,14 +48,14 @@ describe('AuditLogComponent', () => {
op_time: '2017-04-11T10:26:22Z',
username: 'user91' + i,
};
mockedAuditLogs.push(item);
mockedAuditLogExts.push(item);
}
const fakedAuditlogService = {
getLogsResponse(params: ProjectService.GetLogsParams) {
const fakedAuditlogExtService = {
getLogExtsResponse(params: ProjectService.GetLogsParams) {
if (params.q && params.q.indexOf('Demo0') !== -1) {
return of(
new HttpResponse({
body: mockedAuditLogs.slice(0, 1),
body: mockedAuditLogExts.slice(0, 1),
headers: new HttpHeaders({
'x-total-count': '18',
}),
@ -63,7 +65,7 @@ describe('AuditLogComponent', () => {
if (params.page <= 1) {
return of(
new HttpResponse({
body: mockedAuditLogs.slice(0, 15),
body: mockedAuditLogExts.slice(0, 15),
headers: new HttpHeaders({
'x-total-count': '18',
}),
@ -72,7 +74,7 @@ describe('AuditLogComponent', () => {
} else {
return of(
new HttpResponse({
body: mockedAuditLogs.slice(15),
body: mockedAuditLogExts.slice(15),
headers: new HttpHeaders({
'x-total-count': '18',
}),
@ -96,11 +98,11 @@ describe('AuditLogComponent', () => {
await TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [SharedTestingModule],
declarations: [AuditLogComponent],
declarations: [ProjectAuditLogComponent],
providers: [
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
{ provide: Router, useValue: mockRouter },
{ provide: ProjectService, useValue: fakedAuditlogService },
{ provide: ProjectService, useValue: fakedAuditlogExtService },
{
provide: MessageHandlerService,
useValue: mockMessageHandlerService,
@ -110,7 +112,7 @@ describe('AuditLogComponent', () => {
});
beforeEach(() => {
fixture = TestBed.createComponent(AuditLogComponent);
fixture = TestBed.createComponent(ProjectAuditLogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -16,7 +16,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { SessionUser } from '../../../shared/entities/session-user';
import { MessageHandlerService } from '../../../shared/services/message-handler.service';
import { ProjectService } from '../../../../../ng-swagger-gen/services/project.service';
import { AuditLog } from '../../../../../ng-swagger-gen/models/audit-log';
import { AuditLogExt } from '../../../../../ng-swagger-gen/models/audit-log-ext';
import { Project } from '../project';
import { finalize } from 'rxjs/operators';
import {
@ -62,11 +62,11 @@ export class SearchOption {
}
@Component({
selector: 'audit-log',
selector: 'project-audit-log',
templateUrl: './audit-log.component.html',
styleUrls: ['./audit-log.component.scss'],
})
export class AuditLogComponent implements OnInit {
export class ProjectAuditLogComponent implements OnInit {
search: SearchOption = new SearchOption();
currentUser: SessionUser;
projectId: number;
@ -75,7 +75,7 @@ export class AuditLogComponent implements OnInit {
queryStartTime: string;
queryEndTime: string;
queryOperation: string[] = [];
auditLogs: AuditLog[];
auditLogs: AuditLogExt[];
loading: boolean = true;
toggleName = optionalSearch;
@ -113,7 +113,7 @@ export class AuditLogComponent implements OnInit {
}
ngOnInit(): void {
const resolverData = this.route.parent.parent.snapshot.data;
const resolverData = this.route.parent.parent.parent?.snapshot?.data;
if (resolverData) {
const pro: Project = <Project>resolverData['projectResolver'];
this.projectName = pro.name;
@ -146,7 +146,7 @@ export class AuditLogComponent implements OnInit {
arr.push(`operation={${this.queryOperation.join(' ')}}`);
}
const param: ProjectService.GetLogsParams = {
const param: ProjectService.GetLogExtsParams = {
projectName: this.projectName,
pageSize: this.pageSize,
page: this.currentPage,
@ -156,7 +156,7 @@ export class AuditLogComponent implements OnInit {
}
this.loading = true;
this.auditLogService
.getLogsResponse(param)
.getLogExtsResponse(param)
.pipe(finalize(() => (this.loading = false)))
.subscribe(
response => {

View File

@ -1,16 +1,37 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SharedModule } from '../../../shared/shared.module';
import { AuditLogComponent } from './audit-log.component';
import { ProjectAuditLogComponent } from './audit-log.component';
import { ProjectLogsComponent } from './project-logs.component';
import { ProjectAuditLegacyLogComponent } from './audit-legacy-log.component';
const routes: Routes = [
{
path: '',
component: AuditLogComponent,
component: ProjectLogsComponent,
children: [
{
path: 'project-audit-log',
component: ProjectAuditLogComponent,
},
{
path: 'project-audit-legacy-log',
component: ProjectAuditLegacyLogComponent,
},
{
path: '',
redirectTo: 'project-audit-log',
pathMatch: 'full',
},
],
},
];
@NgModule({
declarations: [AuditLogComponent],
declarations: [
ProjectLogsComponent,
ProjectAuditLogComponent,
ProjectAuditLegacyLogComponent,
],
imports: [RouterModule.forChild(routes), SharedModule],
})
export class AuditLogModule {}

View File

@ -0,0 +1,21 @@
<nav class="mt-1">
<ul class="nav">
<li class="nav-item">
<a
class="nav-link"
routerLink="project-audit-log"
routerLinkActive="active"
>{{ 'SIDE_NAV.AUDIT_LOGS' | translate }}</a
>
</li>
<li class="nav-item">
<a
class="nav-link"
routerLink="project-audit-legacy-log"
routerLinkActive="active"
>{{ 'SIDE_NAV.LEGACY_LOGS' | translate }}</a
>
</li>
</ul>
</nav>
<router-outlet></router-outlet>

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'project-logs',
templateUrl: './project-logs.component.html',
styleUrls: ['./project-logs.component.scss'],
})
export class ProjectLogsComponent {
inProgress: boolean = true;
constructor() {}
}

View File

@ -179,6 +179,8 @@
"INTERROGATION_SERVICES": "Schwachstellen Scan"
},
"LOGS": "Logs",
"AUDIT_LOGS": "Audit Logs",
"LEGACY_LOGS": "Audit Logs (Legacy)",
"TASKS": "Aufgaben",
"API_EXPLORER": "Api Explorer",
"HARBOR_API_MANAGEMENT": "Harbor API V2.0",
@ -526,9 +528,16 @@
"REPOSITORY_NAME": "Repository Name",
"TAGS": "Tags",
"OPERATION": "Operation",
"OPERATION_DESCRIPTION": "Operation Description",
"OPERATIONS": "Operations",
"TIMESTAMP": "Zeitstempel",
"ALL_OPERATIONS": "Alle Operations",
"ARTIFACT": "Artifact",
"USER": "User",
"PROJECT": "Project",
"CONFIGURATION": "Configuration",
"PROJECT_MEMBER": "Project Member",
"USER_LOGIN_LOGOUT": "User Login/Logout",
"PULL": "Pull",
"PUSH": "Push",
"CREATE": "Erstellen",
@ -537,6 +546,7 @@
"ADVANCED": "Erweitert",
"SIMPLE": "Einfach",
"ITEMS": "Einträge",
"RESULT": "Success",
"FILTER_PLACEHOLDER": "Filter Logs",
"INVALID_DATE": "Fehlerhaftes Datum.",
"OF": "von",
@ -1810,12 +1820,18 @@
"INCLUDED_OPERATIONS": "Eingeschlossene Aktionen",
"INCLUDED_OPERATION_TOOLTIP": "Entferne Audit-Logs für die ausgewählten Aktionen",
"INCLUDED_OPERATION_ERROR": "Bitte wähle mindestens eine Aktion",
"INCLUDED_EVENT_TYPES": "Included event types",
"INCLUDED_EVENT_TYPE_TOOLTIP": "Remove audit logs for the selected event types",
"INCLUDED_EVENT_TYPE_ERROR": "Please select at lease one event type",
"PURGE_NOW": "JETZT SÄUBERN",
"PURGE_NOW_SUCCESS": "Signal zur Bereinigung erfolgreich gesendet",
"PURGE_SCHEDULE_RESET": "Bereinigungsplan wurde zurückgesetzt",
"PURGE_HISTORY": "Bereinigungshistorie",
"FORWARD_ENDPOINT": "Syslog Endpunkt für die Weiterleitung des Audit-Logs",
"FORWARD_ENDPOINT_TOOLTIP": "Leite Audit-Logs an einen Syslog-Endpunkt, zum Beispiel: harbor-log:10514",
"DISABLE_AUDIT_LOG_EVENT_TYPE": "Disable Audit Log Event Type",
"DISABLE_AUDIT_LOG_EVENT_TYPE_TOOLTIP": "The comma-separated name of the audit log event to be disabled.",
"AUDIT_LOG_EVENT_TYPE_EMPTY": "No audit log event type exists.",
"SKIP_DATABASE": "Datenbank für Audit-Logs übergehen",
"SKIP_DATABASE_TOOLTIP": "Audit-Logs werden nicht in die Datenbank geschrieben. Nur verfügbar, wenn die Weiterleitung für Audit-Logs konfiguriert ist.",
"STOP_GC_SUCCESS": "Signal zum stoppen der Speicherbereinigungs-Aktion erfolgreich",

View File

@ -179,6 +179,8 @@
"INTERROGATION_SERVICES": "Interrogation Services"
},
"LOGS": "Logs",
"AUDIT_LOGS": "Audit Logs",
"LEGACY_LOGS": "Audit Logs (Legacy)",
"TASKS": "Tasks",
"API_EXPLORER": "Api Explorer",
"HARBOR_API_MANAGEMENT": "Harbor API V2.0",
@ -526,9 +528,16 @@
"REPOSITORY_NAME": "Repository Name",
"TAGS": "Tags",
"OPERATION": "Operation",
"OPERATION_DESCRIPTION": "Operation Description",
"OPERATIONS": "Operations",
"TIMESTAMP": "Timestamp",
"ALL_OPERATIONS": "All Operations",
"ARTIFACT": "Artifact",
"USER": "User",
"PROJECT": "Project",
"CONFIGURATION": "Configuration",
"PROJECT_MEMBER": "Project Member",
"USER_LOGIN_LOGOUT": "User Login/Logout",
"PULL": "Pull",
"PUSH": "Push",
"CREATE": "Create",
@ -537,6 +546,7 @@
"ADVANCED": "Advanced",
"SIMPLE": "Simple",
"ITEMS": "items",
"RESULT": "Success",
"FILTER_PLACEHOLDER": "Filter Logs",
"INVALID_DATE": "Invalid date.",
"OF": "of",
@ -1812,12 +1822,18 @@
"INCLUDED_OPERATIONS": "Included operations",
"INCLUDED_OPERATION_TOOLTIP": "Remove audit logs for the selected operations",
"INCLUDED_OPERATION_ERROR": "Please select at lease one operation",
"INCLUDED_EVENT_TYPES": "Included event types",
"INCLUDED_EVENT_TYPE_TOOLTIP": "Remove audit logs for the selected event types",
"INCLUDED_EVENT_TYPE_ERROR": "Please select at lease one event type",
"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 Syslog Endpoint",
"FORWARD_ENDPOINT_TOOLTIP": "Forward audit logs to the syslog endpoint, for example: harbor-log:10514",
"DISABLE_AUDIT_LOG_EVENT_TYPE": "Disable Audit Log Event Type",
"DISABLE_AUDIT_LOG_EVENT_TYPE_TOOLTIP": "The comma-separated name of the audit log event to be disabled.",
"AUDIT_LOG_EVENT_TYPE_EMPTY": "No audit log event type exists.",
"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",
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",

View File

@ -179,6 +179,8 @@
"INTERROGATION_SERVICES": "Interrogation Services"
},
"LOGS": "Logs",
"AUDIT_LOGS": "Audit Logs",
"LEGACY_LOGS": "Audit Logs (Legacy)",
"TASKS": "Tasks",
"API_EXPLORER": "Api Explorer",
"HARBOR_API_MANAGEMENT": "Harbor API V2.0",
@ -526,9 +528,16 @@
"REPOSITORY_NAME": "Nombre del Repositorio",
"TAGS": "Etiquetas",
"OPERATION": "Operación",
"OPERATION_DESCRIPTION": "Operation Description",
"OPERATIONS": "Operaciones",
"TIMESTAMP": "Fecha",
"ALL_OPERATIONS": "Todas las operaciones",
"ARTIFACT": "Artifact",
"USER": "User",
"PROJECT": "Project",
"CONFIGURATION": "Configuration",
"PROJECT_MEMBER": "Project Member",
"USER_LOGIN_LOGOUT": "User Login/Logout",
"PULL": "Pull",
"PUSH": "Push",
"CREATE": "Crear",
@ -537,6 +546,7 @@
"ADVANCED": "Avanzado",
"SIMPLE": "Simple",
"ITEMS": "elementos",
"RESULT": "Success",
"FILTER_PLACEHOLDER": "Filtrar logs",
"INVALID_DATE": "Fecha invalida.",
"OF": "of",
@ -1802,12 +1812,18 @@
"INCLUDED_OPERATIONS": "Operaciones incluidas",
"INCLUDED_OPERATION_TOOLTIP": "Eliminar registros de auditoría de las operaciones seleccionadas",
"INCLUDED_OPERATION_ERROR": "Por favor seleccione al menos una operación",
"INCLUDED_EVENT_TYPES": "Included event types",
"INCLUDED_EVENT_TYPE_TOOLTIP": "Remove audit logs for the selected event types",
"INCLUDED_EVENT_TYPE_ERROR": "Please select at lease one event type",
"PURGE_NOW": "PURGAR AHORA",
"PURGE_NOW_SUCCESS": "Purga activada exitosamente",
"PURGE_SCHEDULE_RESET": "Se ha restablecido la programación de purga",
"PURGE_HISTORY": "Historial de purga",
"FORWARD_ENDPOINT": "Audit Log Reenviar Syslog Endpoint",
"FORWARD_ENDPOINT_TOOLTIP": "Reenviar audit logs al endpoint de syslog, por ejemplo: harbor-log:10514",
"DISABLE_AUDIT_LOG_EVENT_TYPE": "Disable Audit Log Event Type",
"DISABLE_AUDIT_LOG_EVENT_TYPE_TOOLTIP": "The comma-separated name of the audit log event to be disabled.",
"AUDIT_LOG_EVENT_TYPE_EMPTY": "No audit log event type exists.",
"SKIP_DATABASE": "Omitir Base de Datos Audit Log",
"SKIP_DATABASE_TOOLTIP": "Saltar al registro de auditoría en la base de datos, solo disponible cuando se configura el endpoint de reenvío del registro de auditoría",
"STOP_GC_SUCCESS": "El disparador detiene con éxito la operación GC",

View File

@ -179,6 +179,8 @@
"INTERROGATION_SERVICES": "Services d'analyse"
},
"LOGS": "Logs",
"AUDIT_LOGS": "Audit Logs",
"LEGACY_LOGS": "Audit Logs (Legacy)",
"TASKS": "Tâches",
"API_EXPLORER": "Explorateur d'API",
"HARBOR_API_MANAGEMENT": "Harbor API V2.0",
@ -526,9 +528,16 @@
"REPOSITORY_NAME": "Nom du dépôt",
"TAGS": "Etiquettes",
"OPERATION": "Opération",
"OPERATION_DESCRIPTION": "Operation Description",
"OPERATIONS": "Opérations",
"TIMESTAMP": "Horodatage",
"ALL_OPERATIONS": "Toutes les opérations",
"ARTIFACT": "Artifact",
"USER": "User",
"PROJECT": "Project",
"CONFIGURATION": "Configuration",
"PROJECT_MEMBER": "Project Member",
"USER_LOGIN_LOGOUT": "User Login/Logout",
"PULL": "Pull",
"PUSH": "Push",
"CREATE": "Créer",
@ -537,6 +546,7 @@
"ADVANCED": "Avancé",
"SIMPLE": "Simple",
"ITEMS": "entrées",
"RESULT": "Success",
"FILTER_PLACEHOLDER": "Filtrer les logs",
"INVALID_DATE": "Date invalide.",
"OF": "sur",
@ -1812,12 +1822,18 @@
"INCLUDED_OPERATIONS": "Opérations incluses",
"INCLUDED_OPERATION_TOOLTIP": "Supprimer les logs d'audit pour les opérations sélectionnées",
"INCLUDED_OPERATION_ERROR": "Sélectionner au moins une opération",
"INCLUDED_EVENT_TYPES": "Included event types",
"INCLUDED_EVENT_TYPE_TOOLTIP": "Remove audit logs for the selected event types",
"INCLUDED_EVENT_TYPE_ERROR": "Please select at lease one event type",
"PURGE_NOW": "Purger maintenant",
"PURGE_NOW_SUCCESS": "La purge s'est déclenchée avec succès",
"PURGE_SCHEDULE_RESET": "Le programme de purge a été réinitialisé",
"PURGE_HISTORY": "Historique de purges",
"FORWARD_ENDPOINT": "Endpoint Syslog de transfert de logs d'audit",
"FORWARD_ENDPOINT_TOOLTIP": "Transfère les logs d'audit à l'endpoint Syslog, par exemple : harbor-log:10514",
"DISABLE_AUDIT_LOG_EVENT_TYPE": "Disable Audit Log Event Type",
"DISABLE_AUDIT_LOG_EVENT_TYPE_TOOLTIP": "The comma-separated name of the audit log event to be disabled.",
"AUDIT_LOG_EVENT_TYPE_EMPTY": "No audit log event type exists.",
"SKIP_DATABASE": "Ne pas enregistrer les logs d'audit dans la base de données",
"SKIP_DATABASE_TOOLTIP": "Ne pas enregistrer les logs d'audit dans la base de données, disponible uniquement lorsque l'endpoint de transfert de logs d'audit est configuré",
"STOP_GC_SUCCESS": "Déclenchement avec succès de l'arrêt de l'exécution du GC",

View File

@ -179,6 +179,8 @@
"INTERROGATION_SERVICES": "질의 서비스"
},
"LOGS": "로그",
"AUDIT_LOGS": "Audit Logs",
"LEGACY_LOGS": "Audit Logs (Legacy)",
"TASKS": "테스크",
"API_EXPLORER": "Api 탐색기",
"HARBOR_API_MANAGEMENT": "Harbor API V2.0",
@ -523,9 +525,16 @@
"REPOSITORY_NAME": "저장소 이름",
"TAGS": "태그",
"OPERATION": "작업",
"OPERATION_DESCRIPTION": "Operation Description",
"OPERATIONS": "작업들",
"TIMESTAMP": "타임스탬프",
"ALL_OPERATIONS": "모든 작업들",
"ARTIFACT": "Artifact",
"USER": "User",
"PROJECT": "Project",
"CONFIGURATION": "Configuration",
"PROJECT_MEMBER": "Project Member",
"USER_LOGIN_LOGOUT": "User Login/Logout",
"PULL": "풀(Pull)",
"PUSH": "푸시",
"CREATE": "생성",
@ -534,6 +543,7 @@
"ADVANCED": "Advanced",
"SIMPLE": "Simple",
"ITEMS": "아이템",
"RESULT": "Success",
"FILTER_PLACEHOLDER": "로그 필터",
"INVALID_DATE": "잘못된 날짜.",
"OF": "of",
@ -1803,12 +1813,18 @@
"INCLUDED_OPERATIONS": "포함된 작업",
"INCLUDED_OPERATION_TOOLTIP": "선택한 작업에 대한 감사 로그 삭제",
"INCLUDED_OPERATION_ERROR": "작업을 하나 이상 선택하세요.",
"INCLUDED_EVENT_TYPES": "Included event types",
"INCLUDED_EVENT_TYPE_TOOLTIP": "Remove audit logs for the selected event types",
"INCLUDED_EVENT_TYPE_ERROR": "Please select at lease one event type",
"PURGE_NOW": "지금 제거",
"PURGE_NOW_SUCCESS": "제거가 성공적으로 발생됐습니다",
"PURGE_SCHEDULE_RESET": "제거 예약이 초기화됐습니다",
"PURGE_HISTORY": "제거 기록",
"FORWARD_ENDPOINT": "감사 로그를 Syslog 엔트포인트로 전달",
"FORWARD_ENDPOINT_TOOLTIP": "감사 로그를 syslog 엔드포인트로 전달합니다(예: harbor-log:10514)",
"DISABLE_AUDIT_LOG_EVENT_TYPE": "Disable Audit Log Event Type",
"DISABLE_AUDIT_LOG_EVENT_TYPE_TOOLTIP": "The comma-separated name of the audit log event to be disabled.",
"AUDIT_LOG_EVENT_TYPE_EMPTY": "No audit log event type exists.",
"SKIP_DATABASE": "감사 로그 데이터베이스 건너뛰기",
"SKIP_DATABASE_TOOLTIP": "데이터베이스의 감사 로그 로그로 건너뛰기, 감사 로그 전달 엔드포인트가 구성된 경우에만 사용 가능",
"STOP_GC_SUCCESS": "가비지 컬렉션 작업 중지를 성공적으로 발생시켰습니다",

View File

@ -178,6 +178,8 @@
"INTERROGATION_SERVICES": "Serviços de Diagnóstico"
},
"LOGS": "Eventos",
"AUDIT_LOGS": "Audit Logs",
"LEGACY_LOGS": "Audit Logs (Legacy)",
"TASKS": "Tarefas",
"API_EXPLORER": "Explorador da API",
"HARBOR_API_MANAGEMENT": "Harbor API V2.0",
@ -524,9 +526,16 @@
"REPOSITORY_NAME": "Nome do repositório",
"TAGS": "Tags",
"OPERATION": "Operação",
"OPERATION_DESCRIPTION": "Operation Description",
"OPERATIONS": "Operações",
"TIMESTAMP": "Horáro",
"ALL_OPERATIONS": "Todas as Operações",
"ARTIFACT": "Artifact",
"USER": "User",
"PROJECT": "Project",
"CONFIGURATION": "Configuration",
"PROJECT_MEMBER": "Project Member",
"USER_LOGIN_LOGOUT": "User Login/Logout",
"PULL": "Pull",
"PUSH": "Push",
"CREATE": "Criar",
@ -535,6 +544,7 @@
"ADVANCED": "Avançado",
"SIMPLE": "Simples",
"ITEMS": "itens",
"RESULT": "Success",
"FILTER_PLACEHOLDER": "Filtrar",
"INVALID_DATE": "Data inválida.",
"OF": "de",
@ -1807,12 +1817,18 @@
"INCLUDED_OPERATIONS": "Included operations",
"INCLUDED_OPERATION_TOOLTIP": "Remove audit logs for the selected operations",
"INCLUDED_OPERATION_ERROR": "Please select at lease one operation",
"INCLUDED_EVENT_TYPES": "Included event types",
"INCLUDED_EVENT_TYPE_TOOLTIP": "Remove audit logs for the selected event types",
"INCLUDED_EVENT_TYPE_ERROR": "Please select at lease one event type",
"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 Syslog Endpoint",
"FORWARD_ENDPOINT_TOOLTIP": "Forward audit logs to the syslog endpoint, for example: harbor-log:10514",
"DISABLE_AUDIT_LOG_EVENT_TYPE": "Disable Audit Log Event Type",
"DISABLE_AUDIT_LOG_EVENT_TYPE_TOOLTIP": "The comma-separated name of the audit log event to be disabled.",
"AUDIT_LOG_EVENT_TYPE_EMPTY": "No audit log event type exists.",
"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",
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",

View File

@ -179,6 +179,8 @@
"INTERROGATION_SERVICES": "Interrogation Services"
},
"LOGS": "Kayıtlar",
"AUDIT_LOGS": "Audit Logs",
"LEGACY_LOGS": "Audit Logs (Legacy)",
"TASKS": "Görevler",
"API_EXPLORER": "Api Explorer",
"HARBOR_API_MANAGEMENT": "Harbor API V2.0",
@ -526,9 +528,16 @@
"REPOSITORY_NAME": "Depo İsmi",
"TAGS": "Etiketler",
"OPERATION": "Operasyon",
"OPERATION_DESCRIPTION": "Operation Description",
"OPERATIONS": "Operasyonlar",
"TIMESTAMP": "Zaman Damgası",
"ALL_OPERATIONS": "Tüm Operasyonlar",
"ARTIFACT": "Artifact",
"USER": "User",
"PROJECT": "Project",
"CONFIGURATION": "Configuration",
"PROJECT_MEMBER": "Project Member",
"USER_LOGIN_LOGOUT": "User Login/Logout",
"PULL": "Çek",
"PUSH": "Yükle",
"CREATE": "Oluştur",
@ -537,6 +546,7 @@
"ADVANCED": "Gelişmiş",
"SIMPLE": "Basit",
"ITEMS": "adet",
"RESULT": "Success",
"FILTER_PLACEHOLDER": "Günlükleri Filtrele",
"INVALID_DATE": "Geçersiz tarih.",
"OF": "of",
@ -1810,12 +1820,18 @@
"INCLUDED_OPERATIONS": "Included operations",
"INCLUDED_OPERATION_TOOLTIP": "Remove audit logs for the selected operations",
"INCLUDED_OPERATION_ERROR": "Please select at lease one operation",
"INCLUDED_EVENT_TYPES": "Included event types",
"INCLUDED_EVENT_TYPE_TOOLTIP": "Remove audit logs for the selected event types",
"INCLUDED_EVENT_TYPE_ERROR": "Please select at lease one event type",
"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 Syslog Endpoint",
"FORWARD_ENDPOINT_TOOLTIP": "Forward audit logs to the syslog endpoint, for example: harbor-log:10514",
"DISABLE_AUDIT_LOG_EVENT_TYPE": "Disable Audit Log Event Type",
"DISABLE_AUDIT_LOG_EVENT_TYPE_TOOLTIP": "The comma-separated name of the audit log event to be disabled.",
"AUDIT_LOG_EVENT_TYPE_EMPTY": "No audit log event type exists.",
"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",
"STOP_GC_SUCCESS": "Trigger stopping GC operation successfully",

View File

@ -178,6 +178,8 @@
"INTERROGATION_SERVICES": "审查服务"
},
"LOGS": "日志",
"AUDIT_LOGS": "Audit Logs",
"LEGACY_LOGS": "Audit Logs (Legacy)",
"TASKS": "任务",
"API_EXPLORER": "API控制中心",
"HARBOR_API_MANAGEMENT": "Harbor API V2.0",
@ -524,9 +526,16 @@
"REPOSITORY_NAME": "镜像名称",
"TAGS": "Tags",
"OPERATION": "操作",
"OPERATION_DESCRIPTION": "Operation Description",
"OPERATIONS": "操作",
"TIMESTAMP": "时间戳",
"ALL_OPERATIONS": "所有操作",
"ARTIFACT": "Artifact",
"USER": "User",
"PROJECT": "Project",
"CONFIGURATION": "Configuration",
"PROJECT_MEMBER": "Project Member",
"USER_LOGIN_LOGOUT": "User Login/Logout",
"PULL": "Pull",
"PUSH": "Push",
"CREATE": "Create",
@ -535,6 +544,7 @@
"ADVANCED": "高级检索",
"SIMPLE": "简单检索",
"ITEMS": "条记录",
"RESULT": "Success",
"FILTER_PLACEHOLDER": "过滤日志",
"INVALID_DATE": "无效日期。",
"OF": "共计",
@ -1809,12 +1819,18 @@
"INCLUDED_OPERATIONS": "包含操作",
"INCLUDED_OPERATION_TOOLTIP": "删除指定操作类型的日志",
"INCLUDED_OPERATION_ERROR": "请至少选择一种操作类型",
"INCLUDED_EVENT_TYPES": "Included event types",
"INCLUDED_EVENT_TYPE_TOOLTIP": "Remove audit logs for the selected event types",
"INCLUDED_EVENT_TYPE_ERROR": "Please select at lease one event type",
"PURGE_NOW": "立即清理",
"PURGE_NOW_SUCCESS": "触发清理成功",
"PURGE_SCHEDULE_RESET": "清理计划已被重置",
"PURGE_HISTORY": "清理历史",
"FORWARD_ENDPOINT": "日志转发端点",
"FORWARD_ENDPOINT_TOOLTIP": "将日志转发到指定的 syslog 端点例如harbor-log:10514",
"DISABLE_AUDIT_LOG_EVENT_TYPE": "Disable Audit Log Event Type",
"DISABLE_AUDIT_LOG_EVENT_TYPE_TOOLTIP": "The comma-separated name of the audit log event to be disabled.",
"AUDIT_LOG_EVENT_TYPE_EMPTY": "No audit log event type exists.",
"SKIP_DATABASE": "跳过日志数据库",
"SKIP_DATABASE_TOOLTIP": "开启此项将不会在数据库中记录日志,需先配置日志转发端点",
"STOP_GC_SUCCESS": "成功触发停止垃圾回收的操作",

View File

@ -178,6 +178,8 @@
"INTERROGATION_SERVICES": "審查服務"
},
"LOGS": "日誌",
"AUDIT_LOGS": "Audit Logs",
"LEGACY_LOGS": "Audit Logs (Legacy)",
"TASKS": "任務",
"API_EXPLORER": "API Explorer",
"HARBOR_API_MANAGEMENT": "Harbor API V2.0",
@ -525,9 +527,16 @@
"REPOSITORY_NAME": "儲存庫名稱",
"TAGS": "標籤",
"OPERATION": "操作",
"OPERATION_DESCRIPTION": "Operation Description",
"OPERATIONS": "操作",
"TIMESTAMP": "時間戳記",
"ALL_OPERATIONS": "所有操作",
"ARTIFACT": "Artifact",
"USER": "User",
"PROJECT": "Project",
"CONFIGURATION": "Configuration",
"PROJECT_MEMBER": "Project Member",
"USER_LOGIN_LOGOUT": "User Login/Logout",
"PULL": "拉取",
"PUSH": "推送",
"CREATE": "建立",
@ -536,6 +545,7 @@
"ADVANCED": "進階搜尋",
"SIMPLE": "簡易搜尋",
"ITEMS": "筆紀錄",
"RESULT": "Success",
"FILTER_PLACEHOLDER": "篩選日誌",
"INVALID_DATE": "無效日期。",
"OF": "共計",
@ -1807,12 +1817,18 @@
"INCLUDED_OPERATIONS": "包含的操作",
"INCLUDED_OPERATION_TOOLTIP": "移除所選操作的稽核日誌",
"INCLUDED_OPERATION_ERROR": "請至少選擇一個操作",
"INCLUDED_EVENT_TYPES": "Included event types",
"INCLUDED_EVENT_TYPE_TOOLTIP": "Remove audit logs for the selected event types",
"INCLUDED_EVENT_TYPE_ERROR": "Please select at lease one event type",
"PURGE_NOW": "立即清除",
"PURGE_NOW_SUCCESS": "成功觸發清除",
"PURGE_SCHEDULE_RESET": "清除排程已重設",
"PURGE_HISTORY": "清除歷史",
"FORWARD_ENDPOINT": "稽核日誌轉發 Syslog 端點",
"FORWARD_ENDPOINT_TOOLTIP": "將稽核日誌轉發至 syslog 端點,例如: harbor-log:10514",
"DISABLE_AUDIT_LOG_EVENT_TYPE": "Disable Audit Log Event Type",
"DISABLE_AUDIT_LOG_EVENT_TYPE_TOOLTIP": "The comma-separated name of the audit log event to be disabled.",
"AUDIT_LOG_EVENT_TYPE_EMPTY": "No audit log event type exists.",
"SKIP_DATABASE": "跳過稽核日誌資料庫",
"SKIP_DATABASE_TOOLTIP": "跳過在資料庫中記錄稽核日誌,僅在設定稽核日誌轉發端點時可用",
"STOP_GC_SUCCESS": "成功觸發停止清理垃圾操作",