mirror of
https://github.com/goharbor/harbor.git
synced 2025-02-26 16:51:47 +01:00
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:
parent
45659070b7
commit
b837bbb716
@ -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>
|
||||
|
@ -51,3 +51,8 @@
|
||||
padding-left: .6rem;
|
||||
padding-right: .6rem;
|
||||
}
|
||||
|
||||
.float-left {
|
||||
float:left;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
@ -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', () => {
|
||||
|
@ -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() {
|
||||
|
@ -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 = {
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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);
|
||||
|
@ -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 }}
|
||||
|
@ -212,3 +212,11 @@ $message-type-width: 12rem;
|
||||
.date {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.float-left {
|
||||
float:left;
|
||||
|
||||
label {
|
||||
min-width: 7rem;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
@ -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];
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
118
src/portal/src/app/base/left-side-nav/log/audit-log.component.ts
Normal file
118
src/portal/src/app/base/left-side-nav/log/audit-log.component.ts
Normal 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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -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 {}
|
||||
|
@ -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>
|
11
src/portal/src/app/base/left-side-nav/log/logs.component.ts
Normal file
11
src/portal/src/app/base/left-side-nav/log/logs.component.ts
Normal 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() {}
|
||||
}
|
@ -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">
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -42,4 +42,8 @@
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.width-140 {
|
||||
width: 140px;
|
||||
}
|
@ -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();
|
||||
});
|
||||
|
@ -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 => {
|
||||
|
@ -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 {}
|
||||
|
@ -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>
|
@ -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() {}
|
||||
}
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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": "가비지 컬렉션 작업 중지를 성공적으로 발생시켰습니다",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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": "成功触发停止垃圾回收的操作",
|
||||
|
@ -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": "成功觸發停止清理垃圾操作",
|
||||
|
Loading…
Reference in New Issue
Block a user