Merge pull request #11027 from AllForNothing/webhook

Improve Webhook UI
This commit is contained in:
Will Sun 2020-03-11 18:35:29 +08:00 committed by GitHub
commit aa73f16a20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 869 additions and 242 deletions

View File

@ -63,6 +63,7 @@ import {
import { RepositoryDefaultService, RepositoryService } from "./repository/repository.service"; import { RepositoryDefaultService, RepositoryService } from "./repository/repository.service";
import { ArtifactDefaultService, ArtifactService } from "./repository/artifact/artifact.service"; import { ArtifactDefaultService, ArtifactService } from "./repository/artifact/artifact.service";
import { GridViewComponent } from "./repository/gridview/grid-view.component"; import { GridViewComponent } from "./repository/gridview/grid-view.component";
import { LastTriggerComponent } from "./webhook/last-trigger/last-trigger.component";
@NgModule({ @NgModule({
imports: [ imports: [
@ -110,6 +111,7 @@ import { GridViewComponent } from "./repository/gridview/grid-view.component";
ValuesComponent, ValuesComponent,
ArtifactVulnerabilitiesComponent, ArtifactVulnerabilitiesComponent,
GridViewComponent, GridViewComponent,
LastTriggerComponent
], ],
exports: [ProjectComponent, ListProjectComponent], exports: [ProjectComponent, ListProjectComponent],
providers: [ providers: [

View File

@ -12,7 +12,7 @@
</div> </div>
<ng-container *ngIf="scanner"> <ng-container *ngIf="scanner">
<div class="clr-form-control"> <div class="clr-form-control">
<label for="select-full" class="clr-control-label name">{{'SCANNER.NAME' | translate}}</label> <label class="clr-control-label name">{{'SCANNER.NAME' | translate}}</label>
<div class="clr-control-container"> <div class="clr-control-container">
<div class="clr-input-wrapper"> <div class="clr-input-wrapper">
<div class="clr-input-wrapper name"> <div class="clr-input-wrapper name">

View File

@ -2,13 +2,57 @@
<inline-alert class="modal-title"></inline-alert> <inline-alert class="modal-title"></inline-alert>
<form #webhookForm="ngForm" class="clr-form clr-form-horizontal"> <form #webhookForm="ngForm" class="clr-form clr-form-horizontal">
<section class="form-block webhook-section"> <section class="form-block webhook-section">
<!-- name -->
<div class="clr-form-control">
<label for="edit_endpoint_url" class="clr-control-label required">{{'WEBHOOK.NAME' | translate}}</label>
<div class="clr-control-container" [class.clr-error]="name.errors && name.errors.required && (name.dirty || name.touched)">
<div class="clr-input-wrapper">
<input autocomplete="off" class="clr-input" type="text" id="name" [disabled]="checking" [(ngModel)]="webhook.name"
size="30" name="notify-type" #name="ngModel" required>
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
</div>
<clr-control-error *ngIf="name.errors && name.errors.required && (name.dirty || name.touched)" class="tooltip-content">
{{'WEBHOOK.NAME_REQUIRED' | translate}}
</clr-control-error>
</div>
</div>
<!-- description -->
<div class="clr-form-control">
<label for="edit_endpoint_url" class="clr-control-label">{{'WEBHOOK.DESCRIPTION' | translate}}</label>
<div class="clr-control-container">
<textarea autocomplete="off" class="clr-textarea width-238" type="text" id="description" [disabled]="checking" [(ngModel)]="webhook.description"
name="description"></textarea>
</div>
</div>
<!-- notify type -->
<clr-select-container>
<label class="required">{{'WEBHOOK.NOTIFY_TYPE' | translate}}</label>
<select clrSelect name="notifyType" id="notify_type" [(ngModel)]="webhook.targets[0].type" [disabled]="checking">
<option *ngFor="let type of metadata?.notify_type" value="{{type}}">{{type}}</option>
</select>
</clr-select-container>
<div class="clr-form-control">
<label class="clr-control-label required">{{'WEBHOOK.EVENT_TYPE' | translate}}</label>
<div class="clr-control-container clr-control-inline" [class.clr-error]="!hasEventType()">
<div class="clr-checkbox-wrapper" *ngFor="let item of metadata?.event_type">
<input type="checkbox" id="{{item}}" name="eventTypes" value="{{item}}" class="clr-checkbox" (change)="setEventType(item)" [checked]="getEventType(item)">
<label for="{{item}}" class="clr-control-label">{{item}}</label>
</div>
<div class="clr-subtext-wrapper" *ngIf="!hasEventType()">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
<span class="clr-subtext">{{'WEBHOOK.EVENT_TYPE_REQUIRED' | translate}}</span>
</div>
</div>
</div>
<!-- endpoint URL --> <!-- endpoint URL -->
<div class="clr-form-control"> <div class="clr-form-control">
<label for="edit_endpoint_url" class="clr-control-label required">{{'WEBHOOK.ENDPOINT_URL' | translate}}</label> <label for="edit_endpoint_url" class="clr-control-label required">{{'WEBHOOK.ENDPOINT_URL' | translate}}</label>
<div class="clr-control-container" [class.clr-error]="enpointURL.errors && enpointURL.errors.required && (enpointURL.dirty || enpointURL.touched)"> <div class="clr-control-container" [class.clr-error]="enpointURL.errors && enpointURL.errors.required && (enpointURL.dirty || enpointURL.touched)">
<div class="clr-input-wrapper"> <div class="clr-input-wrapper">
<input class="clr-input" type="text" id="edit_endpoint_url" [disabled]="checking" [(ngModel)]="webhookTarget.address" <input autocomplete="off" class="clr-input" type="text" id="edit_endpoint_url" [disabled]="checking" [(ngModel)]="webhook.targets[0].address"
size="30" name="edit_endpoint_url" #enpointURL="ngModel" required placeholder="http(s)://192.168.1.1"> size="30" name="edit_endpoint_url" #enpointURL="ngModel" required [placeholder]="webhook.targets[0].type ==='http'?'http(s)://192.168.1.1':''">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon> <clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
</div> </div>
<clr-control-error *ngIf="enpointURL.errors && enpointURL.errors.required && (enpointURL.dirty || enpointURL.touched)" class="tooltip-content"> <clr-control-error *ngIf="enpointURL.errors && enpointURL.errors.required && (enpointURL.dirty || enpointURL.touched)" class="tooltip-content">
@ -17,16 +61,18 @@
</div> </div>
</div> </div>
<!-- auth_header --> <!-- auth_header -->
<div class="clr-form-control"> <div class="clr-form-control" *ngIf="webhook?.targets[0]?.type ==='http'">
<label for="auth_header" class="clr-control-label">{{ 'WEBHOOK.AUTH_HEADER' | <label for="auth_header" class="clr-control-label">{{ 'WEBHOOK.AUTH_HEADER' |
translate }}</label> translate }}</label>
<div class="clr-control-container"> <div class="clr-control-container">
<div class="clr-input-wrapper"> <div class="clr-input-wrapper">
<input class="clr-input" type="text" id="auth_header" [disabled]="checking" <input autocomplete="off" class="clr-input" type="text" id="auth_header" [disabled]="checking"
[(ngModel)]="webhookTarget.auth_header" size="30" name="auth_header"> [(ngModel)]="webhook.targets[0].auth_header" size="30" name="auth_header">
</div> </div>
</div> </div>
</div> </div>
<!-- verify remote cert --> <!-- verify remote cert -->
<div class="clr-form-control"> <div class="clr-form-control">
<label for="verify_remote_cert" class="clr-control-label"> <label for="verify_remote_cert" class="clr-control-label">
@ -40,17 +86,18 @@
</label> </label>
<div class="clr-control-container padding-top-3"> <div class="clr-control-container padding-top-3">
<input type="checkbox" [disabled]="checking" clrCheckbox name="verify_remote_cert" id="verify_remote_cert" <input type="checkbox" [disabled]="checking" clrCheckbox name="verify_remote_cert" id="verify_remote_cert"
(ngModelChange)="setCertValue($event)" [ngModel]="!webhookTarget.skip_cert_verify"/> </div> (ngModelChange)="setCertValue($event)" [ngModel]="!webhook?.targets[0]?.skip_cert_verify"/> </div>
</div> </div>
</section> </section>
</form> </form>
<div class="mt-1" *ngIf="!isModify"> <div class="mt-1" *ngIf="!isModify">
<button type="button" id="new-webhook-continue" class="btn btn-primary" [disabled]="!isValid" (click)="onSubmit()">{{'BUTTON.CONTINUE' | translate}}</button> <button type="button" id="webhook-test-add" [clrLoading]="checkBtnState" class="btn btn-outline" (click)="onTestEndpoint()" [disabled]="checking || enpointURL.errors">{{'WEBHOOK.TEST_ENDPOINT_BUTTON' | translate}}</button>
<button type="button" [clrLoading]="checkBtnState" class="btn btn-outline" (click)="onTestEndpoint()" [disabled]="checking || enpointURL.errors">{{'WEBHOOK.TEST_ENDPOINT_BUTTON' | translate}}</button> <button type="button" id="new-webhook-continue" class="btn btn-primary" [disabled]="!isValid" (click)="add()">{{'BUTTON.ADD' | translate}}</button>
<button type="button" class="btn btn-outline" id="add-webhook-cancel" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button>
</div> </div>
<div class="mt-1 bottom-btn" *ngIf="isModify"> <div class="mt-1 bottom-btn" *ngIf="isModify">
<button type="button" [clrLoading]="checkBtnState" class="btn btn-outline" id="webhook-test" (click)="onTestEndpoint()" [disabled]="checking || enpointURL.errors">{{'WEBHOOK.TEST_ENDPOINT_BUTTON' | translate}}</button> <button type="button" [clrLoading]="checkBtnState" class="btn btn-outline" id="webhook-test" (click)="onTestEndpoint()" [disabled]="checking || enpointURL.errors">{{'WEBHOOK.TEST_ENDPOINT_BUTTON' | translate}}</button>
<button type="button" class="btn btn-primary" id="edit-webhook-save" [disabled]="!isValid" (click)="save()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" id="edit-webhook-cancel" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button> <button type="button" class="btn btn-outline" id="edit-webhook-cancel" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" id="edit-webhook-save" [disabled]="!isValid" (click)="onSubmit()">{{'BUTTON.SAVE' | translate}}</button>
</div> </div>
</div> </div>

View File

@ -7,4 +7,7 @@
.bottom-btn { .bottom-btn {
text-align: right; text-align: right;
margin-right: 3.4rem; margin-right: 3.4rem;
}
.width-238 {
width: 238px;
} }

View File

@ -1,7 +1,6 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { AddWebhookFormComponent } from './add-webhook-form.component'; import { AddWebhookFormComponent } from './add-webhook-form.component';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing';
@ -10,6 +9,8 @@ import { FormsModule } from '@angular/forms';
import { WebhookService } from "../webhook.service"; import { WebhookService } from "../webhook.service";
import { MessageHandlerService } from "../../../shared/message-handler/message-handler.service"; import { MessageHandlerService } from "../../../shared/message-handler/message-handler.service";
import { of } from 'rxjs'; import { of } from 'rxjs';
import { Webhook } from "../webhook";
import { InlineAlertComponent } from "../../../shared/inline-alert/inline-alert.component";
describe('AddWebhookFormComponent', () => { describe('AddWebhookFormComponent', () => {
let component: AddWebhookFormComponent; let component: AddWebhookFormComponent;
@ -17,17 +18,60 @@ describe('AddWebhookFormComponent', () => {
const mockWebhookService = { const mockWebhookService = {
getCurrentUser: () => { getCurrentUser: () => {
return of(null); return of(null);
},
createWebhook() {
return of(null);
},
editWebhook() {
return of(null);
},
testEndpoint() {
return of(null);
} }
}; };
const mockMessageHandlerService = { const mockMessageHandlerService = {
handleError: () => { } handleError: () => { }
}; };
const mockedWehook: Webhook = {
id: 1,
project_id: 1,
name: 'test',
description: 'just a test webhook',
targets: [{
address: 'https://test.com',
type: 'http',
attachment: null,
auth_header: null,
skip_cert_verify: true,
}],
event_types: [
'projectQuota'
],
creator: null,
creation_time: null,
update_time: null,
enabled: true,
};
const mockedMetadata = {
"event_type": [
"projectQuota",
"pullImage",
"scanningFailed",
"uploadChart",
"deleteChart",
"downloadChart",
"scanningCompleted",
"pushImage",
"deleteImage"
],
"notify_type": [
"http",
"slack"
]
};
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
imports: [ imports: [
BrowserAnimationsModule, BrowserAnimationsModule,
ClarityModule, ClarityModule,
@ -37,7 +81,9 @@ describe('AddWebhookFormComponent', () => {
NoopAnimationsModule, NoopAnimationsModule,
HttpClientTestingModule HttpClientTestingModule
], ],
declarations: [AddWebhookFormComponent], declarations: [AddWebhookFormComponent,
InlineAlertComponent,
],
providers: [ providers: [
TranslateService, TranslateService,
{ provide: WebhookService, useValue: mockWebhookService }, { provide: WebhookService, useValue: mockWebhookService },
@ -52,10 +98,51 @@ describe('AddWebhookFormComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(AddWebhookFormComponent); fixture = TestBed.createComponent(AddWebhookFormComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.metadata = mockedMetadata;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it('should create', async () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
const cancelButtonForAdd: HTMLButtonElement = fixture.nativeElement.querySelector("#add-webhook-cancel");
expect(cancelButtonForAdd).toBeTruthy();
component.isModify = true;
fixture.detectChanges();
await fixture.whenStable();
const cancelButtonForEdit: HTMLButtonElement = fixture.nativeElement.querySelector("#edit-webhook-cancel");
expect(cancelButtonForEdit).toBeTruthy();
});
it("should occur a 'name is required' error", async () => {
await fixture.whenStable();
fixture.autoDetectChanges(true);
const nameInput: HTMLInputElement = fixture.nativeElement.querySelector("#name");
nameInput.value = "test";
nameInput.dispatchEvent(new Event('input'));
nameInput.value = null;
nameInput.dispatchEvent(new Event('input'));
nameInput.blur();
nameInput.dispatchEvent(new Event('blur'));
const errorEle: HTMLElement = fixture.nativeElement.querySelector("clr-control-error");
expect(errorEle.innerText).toEqual('WEBHOOK.NAME_REQUIRED');
});
it("test button should work", async () => {
const spy: jasmine.Spy = spyOn(component, 'onTestEndpoint').and.returnValue(undefined);
const testButton: HTMLButtonElement = fixture.nativeElement.querySelector("#webhook-test-add");
testButton.dispatchEvent(new Event('click'));
fixture.detectChanges();
await fixture.whenStable();
expect(spy.calls.count()).toEqual(1);
});
it("add button should work", async () => {
const spy: jasmine.Spy = spyOn(component, 'add').and.returnValue(undefined);
component.webhook = mockedWehook;
fixture.detectChanges();
await fixture.whenStable();
expect(component.isValid).toBeTruthy();
const addButton: HTMLButtonElement = fixture.nativeElement.querySelector("#new-webhook-continue");
addButton.dispatchEvent(new Event('click'));
fixture.detectChanges();
await fixture.whenStable();
expect(spy.calls.count()).toEqual(1);
}); });
}); });

View File

@ -1,89 +1,62 @@
import { import {
Component, Component,
OnInit, OnInit,
OnChanges,
Input, Input,
ViewChild, ViewChild,
Output, Output,
EventEmitter, EventEmitter,
SimpleChanges
} from "@angular/core"; } from "@angular/core";
import { Webhook, Target } from "../webhook"; import { Webhook } from "../webhook";
import { NgForm } from "@angular/forms"; import { NgForm } from "@angular/forms";
import { ClrLoadingState } from "@clr/angular"; import { ClrLoadingState } from "@clr/angular";
import { finalize } from "rxjs/operators"; import { finalize } from "rxjs/operators";
import { WebhookService } from "../webhook.service"; import { WebhookService } from "../webhook.service";
import { WebhookEventTypes } from '../../../shared/shared.const';
import { InlineAlertComponent } from "../../../shared/inline-alert/inline-alert.component"; import { InlineAlertComponent } from "../../../shared/inline-alert/inline-alert.component";
import { MessageHandlerService } from "../../../shared/message-handler/message-handler.service";
import { TranslateService } from "@ngx-translate/core";
@Component({ @Component({
selector: 'add-webhook-form', selector: 'add-webhook-form',
templateUrl: './add-webhook-form.component.html', templateUrl: './add-webhook-form.component.html',
styleUrls: ['./add-webhook-form.component.scss'] styleUrls: ['./add-webhook-form.component.scss']
}) })
export class AddWebhookFormComponent implements OnInit, OnChanges { export class AddWebhookFormComponent implements OnInit {
closable: boolean = true; closable: boolean = true;
staticBackdrop: boolean = true; staticBackdrop: boolean = true;
checking: boolean = false; checking: boolean = false;
checkBtnState: ClrLoadingState = ClrLoadingState.DEFAULT; checkBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
webhookForm: NgForm; webhookForm: NgForm;
submitting: boolean = false; submitting: boolean = false;
webhookTarget: Target = new Target();
@Input() projectId: number; @Input() projectId: number;
@Input() webhook: Webhook; webhook: Webhook = new Webhook();
@Input() isModify: boolean; isModify: boolean;
@Input() isOpen: boolean; @Input() isOpen: boolean;
@Output() edit = new EventEmitter<boolean>();
@Output() close = new EventEmitter<boolean>(); @Output() close = new EventEmitter<boolean>();
@ViewChild("webhookForm", { static: true }) currentForm: NgForm; @ViewChild("webhookForm", { static: true }) currentForm: NgForm;
@ViewChild(InlineAlertComponent, { static: false }) inlineAlert: InlineAlertComponent; @ViewChild(InlineAlertComponent, { static: false }) inlineAlert: InlineAlertComponent;
@Input()
metadata: any;
@Output() notify = new EventEmitter<Webhook>();
constructor( constructor(
private webhookService: WebhookService, private webhookService: WebhookService,
private messageHandlerService: MessageHandlerService,
private translate: TranslateService
) { } ) { }
ngOnInit() { ngOnInit() {
} }
ngOnChanges(changes: SimpleChanges) {
if (changes['isOpen'] && changes['isOpen'].currentValue) {
Object.assign(this.webhookTarget, this.webhook.targets[0]);
}
}
onTestEndpoint() { onTestEndpoint() {
this.checkBtnState = ClrLoadingState.LOADING; this.checkBtnState = ClrLoadingState.LOADING;
this.checking = true; this.checking = true;
this.webhookService this.webhookService
.testEndpoint(this.projectId, { .testEndpoint(this.projectId, {
targets: [this.webhookTarget] targets: this.webhook.targets
}) })
.pipe(finalize(() => (this.checking = false))) .pipe(finalize(() => (this.checking = false)))
.subscribe( .subscribe(
response => { response => {
if (this.isModify) { this.inlineAlert.showInlineSuccess({message: "WEBHOOK.TEST_ENDPOINT_SUCCESS"});
this.inlineAlert.showInlineSuccess({
message: "WEBHOOK.TEST_ENDPOINT_SUCCESS"
});
} else {
this.translate.get("WEBHOOK.TEST_ENDPOINT_SUCCESS").subscribe((res: string) => {
this.messageHandlerService.info(res);
});
}
this.checkBtnState = ClrLoadingState.SUCCESS; this.checkBtnState = ClrLoadingState.SUCCESS;
}, },
error => { error => {
if (this.isModify) { this.inlineAlert.showInlineError("WEBHOOK.TEST_ENDPOINT_FAILURE");
this.inlineAlert.showInlineError("WEBHOOK.TEST_ENDPOINT_FAILURE");
} else {
this.messageHandlerService.handleError(error);
}
this.checkBtnState = ClrLoadingState.DEFAULT; this.checkBtnState = ClrLoadingState.DEFAULT;
} }
); );
@ -95,30 +68,38 @@ export class AddWebhookFormComponent implements OnInit, OnChanges {
this.inlineAlert.close(); this.inlineAlert.close();
} }
onSubmit() { add() {
const rx = this.isModify this.submitting = true;
? this.webhookService.editWebhook(this.projectId, this.webhook.id, Object.assign(this.webhook, { targets: [this.webhookTarget] })) this.webhookService.createWebhook(this.projectId, this.webhook)
: this.webhookService.createWebhook(this.projectId, { .pipe(finalize(() => (this.submitting = false)))
targets: [this.webhookTarget],
event_types: Object.keys(WebhookEventTypes).map(key => WebhookEventTypes[key]),
enabled: true,
});
rx.pipe(finalize(() => (this.submitting = false)))
.subscribe( .subscribe(
response => { response => {
this.edit.emit(this.isModify); this.notify.emit();
this.inlineAlert.close(); this.inlineAlert.close();
}, },
error => { error => {
this.isModify this.inlineAlert.showInlineError(error);
? this.inlineAlert.showInlineError(error) }
: this.messageHandlerService.handleError(error); );
}
save() {
this.submitting = true;
this.webhookService.editWebhook(this.projectId, this.webhook.id, this.webhook)
.pipe(finalize(() => (this.submitting = false)))
.subscribe(
response => {
this.inlineAlert.close();
this.notify.emit();
},
error => {
this.inlineAlert.showInlineError(error);
} }
); );
} }
setCertValue($event: any): void { setCertValue($event: any): void {
this.webhookTarget.skip_cert_verify = !$event; this.webhook.targets[0].skip_cert_verify = !$event;
} }
public get isValid(): boolean { public get isValid(): boolean {
@ -126,7 +107,26 @@ export class AddWebhookFormComponent implements OnInit, OnChanges {
this.currentForm && this.currentForm &&
this.currentForm.valid && this.currentForm.valid &&
!this.submitting && !this.submitting &&
!this.checking !this.checking &&
this.hasEventType()
); );
} }
setEventType(eventType) {
if (this.webhook.event_types.indexOf(eventType) === -1) {
this.webhook.event_types.push(eventType);
} else {
this.webhook.event_types.splice(this.webhook.event_types.findIndex(item => item === eventType), 1);
}
}
getEventType(eventType): boolean {
return eventType && this.webhook.event_types.indexOf(eventType) !== -1;
}
hasEventType(): boolean {
return this.metadata
&& this.metadata.event_type
&& this.metadata.event_type.length > 0
&& this.webhook.event_types
&& this.webhook.event_types.length > 0;
}
} }

View File

@ -1,13 +1,12 @@
<clr-modal [(clrModalOpen)]="isOpen" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable"> <clr-modal [(clrModalOpen)]="isOpen" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{'WEBHOOK.EDIT_WEBHOOK' | translate}}</h3> <h3 *ngIf="isEdit" class="modal-title">{{'WEBHOOK.EDIT_WEBHOOK' | translate}}</h3>
<h3 *ngIf="!isEdit" class="modal-title">{{'WEBHOOK.ADD_WEBHOOK' | translate}}</h3>
<div class="modal-body"> <div class="modal-body">
<div>{{'WEBHOOK.EDIT_WEBHOOK_DESC' | translate}}</div> <div>{{'WEBHOOK.EDIT_WEBHOOK_DESC' | translate}}</div>
<add-webhook-form [projectId]="projectId" <add-webhook-form [metadata]="metadata" [projectId]="projectId"
[isModify]="true" [isOpen]="isOpen"
[isOpen]="isOpen" (close)="closeModal()"
[webhook]="webhook" (notify)="notifySuccess()"
(edit)="closeModal($event)"
(close)="closeModal($event)"
></add-webhook-form> ></add-webhook-form>
</div> </div>
</clr-modal> </clr-modal>

View File

@ -1,7 +1,7 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { AddWebhookComponent } from './add-webhook.component'; import { AddWebhookComponent } from './add-webhook.component';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ClarityModule } from '@clr/angular'; import { ClarityModule } from '@clr/angular';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
@ -14,7 +14,8 @@ describe('AddWebhookComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
schemas: [ schemas: [
CUSTOM_ELEMENTS_SCHEMA CUSTOM_ELEMENTS_SCHEMA,
NO_ERRORS_SCHEMA
], ],
imports: [ imports: [
BrowserAnimationsModule, BrowserAnimationsModule,
@ -42,4 +43,14 @@ describe('AddWebhookComponent', () => {
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should open modal and should be edit model', async () => {
component.isEdit = true;
component.isOpen = true;
fixture.detectChanges();
await fixture.whenStable();
const body: HTMLElement = fixture.nativeElement.querySelector(".modal-body");
expect(body).toBeTruthy();
const title: HTMLElement = fixture.nativeElement.querySelector(".modal-title");
expect(title.innerText).toEqual('WEBHOOK.EDIT_WEBHOOK');
});
}); });

View File

@ -15,16 +15,18 @@ import { AddWebhookFormComponent } from "../add-webhook-form/add-webhook-form.co
styleUrls: ['./add-webhook.component.scss'] styleUrls: ['./add-webhook.component.scss']
}) })
export class AddWebhookComponent implements OnInit { export class AddWebhookComponent implements OnInit {
isEdit: boolean;
isOpen: boolean = false; isOpen: boolean = false;
closable: boolean = false; closable: boolean = false;
staticBackdrop: boolean = true; staticBackdrop: boolean = true;
@Input() projectId: number; @Input() projectId: number;
@Input() webhook: Webhook; webhook: Webhook;
@Output() modify = new EventEmitter<boolean>(); @Input()
metadata: any;
@ViewChild(AddWebhookFormComponent, { static: false }) @ViewChild(AddWebhookFormComponent, { static: false })
addWebhookFormComponent: AddWebhookFormComponent; addWebhookFormComponent: AddWebhookFormComponent;
@Output() notify = new EventEmitter<Webhook>();
constructor() { } constructor() { }
@ -38,12 +40,11 @@ export class AddWebhookComponent implements OnInit {
onCancel() { onCancel() {
this.isOpen = false; this.isOpen = false;
} }
notifySuccess() {
closeModal(isModified: boolean): void { this.isOpen = false;
if (isModified) { this.notify.emit();
this.modify.emit(true); }
} closeModal() {
this.isOpen = false; this.isOpen = false;
} }
} }

View File

@ -0,0 +1,29 @@
<h4>{{'WEBHOOK.LAST_TRIGGER' | translate}}</h4>
<clr-datagrid>
<clr-dg-column [clrDgField]="'event_type'">{{'WEBHOOK.TYPE' | translate}}</clr-dg-column>
<clr-dg-column>{{'WEBHOOK.STATUS' | translate}}</clr-dg-column>
<clr-dg-column>{{'WEBHOOK.LAST_TRIGGERED' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let item of lastTriggers">
<clr-dg-cell>{{item.event_type}}</clr-dg-cell>
<clr-dg-cell [ngSwitch]="item.enabled">
<div *ngSwitchCase="true" class="icon-wrap">
<clr-icon shape="check-circle" size="20" class="is-success enabled-icon"></clr-icon>
<span>{{'WEBHOOK.ENABLED' | translate}}</span>
</div>
<div *ngSwitchCase="false" class="icon-wrap">
<clr-icon shape="exclamation-triangle" size="20" class="is-warning"></clr-icon>
<span>{{'WEBHOOK.DISABLED' | translate}}</span>
</div>
</clr-dg-cell>
<clr-dg-cell>{{item.last_trigger_time | date: 'short'}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-placeholder>
{{'WEBHOOK.NO_TRIGGER' | translate}}
</clr-dg-placeholder>
<clr-dg-footer>
<span *ngIf="lastTriggers.length">1 - {{lastTriggers.length}} {{'WEBHOOK.OF' | translate}} </span> {{lastTriggers.length}} {{'WEBHOOK.ITEMS' | translate}}
<clr-dg-pagination [clrDgPageSize]="10"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -0,0 +1,57 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { ClarityModule } from "@clr/angular";
import { SharedModule } from "../../../shared/shared.module";
import { LastTriggerComponent } from "./last-trigger.component";
import { LastTrigger } from "../webhook";
import { SimpleChange } from "@angular/core";
describe('LastTriggerComponent', () => {
const mokedTriggers: LastTrigger[] = [
{
policy_name: 'http',
enabled: true,
event_type: 'pullImage',
creation_time: null,
last_trigger_time: null
},
{
policy_name: 'slack',
enabled: true,
event_type: 'pullImage',
creation_time: null,
last_trigger_time: null
}
];
let component: LastTriggerComponent;
let fixture: ComponentFixture<LastTriggerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
SharedModule,
BrowserAnimationsModule,
ClarityModule,
],
declarations: [
LastTriggerComponent
],
});
});
beforeEach(() => {
fixture = TestBed.createComponent(LastTriggerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render one row', async () => {
component.inputLastTriggers = mokedTriggers;
component.webhookName = 'slack';
component.ngOnChanges({inputLastTriggers: new SimpleChange([], mokedTriggers, true)});
fixture.detectChanges();
await fixture.whenStable();
const rows = fixture.nativeElement.getElementsByTagName('clr-dg-row');
expect(rows.length).toEqual(1);
});
});

View File

@ -0,0 +1,31 @@
import {
Component, Input, OnChanges,
OnInit, SimpleChanges
} from "@angular/core";
import { LastTrigger } from "../webhook";
@Component({
selector: 'last-trigger',
templateUrl: 'last-trigger.component.html',
styleUrls: ['./last-trigger.component.scss']
})
export class LastTriggerComponent implements OnInit , OnChanges {
@Input() inputLastTriggers: LastTrigger[];
@Input() webhookName: string;
lastTriggers: LastTrigger[] = [];
constructor() {
}
ngOnChanges(changes: SimpleChanges): void {
if (changes && changes['inputLastTriggers']) {
this.lastTriggers = [];
this.inputLastTriggers.forEach(item => {
if (this.webhookName === item.policy_name) {
this.lastTriggers.push(item);
}
});
}
}
ngOnInit(): void {
}
}

View File

@ -1,54 +1,109 @@
<div *ngIf="loadingWebhook" class="clr-row mt-2 center"> <div class="row">
<span class="spinner spinner-md"></span> <h4 class="mt-1">{{'WEBHOOK.WEBHOOKS' | translate}}</h4>
</div> <clr-datagrid [clrDgLoading]="loadingWebhookList || loadingMetadata" [(clrDgSelected)]="selectedRow">
<div class="row" *ngIf="!loadingWebhook"> <clr-dg-action-bar>
<div *ngIf="!showCreate"> <div class="clr-row">
<div> <div class="clr-col-7">
<div class="row flex-items-xs-between rightPos"> <button type="button" class="btn btn-secondary" (click)="newWebhook()">
<div class="flex-xs-middle option-left"> <clr-icon shape="plus" size="16"></clr-icon>
<div> {{'WEBHOOK.NEW_WEBHOOK' | translate}}
<span class="endpoint-label">Webhook endpoint</span>: {{endpoint}} </button>
<button class="btn btn-link" id="edit-webhook" (click)="openAddWebhookModal()">{{'WEBHOOK.EDIT_BUTTON' | translate}}</button> <clr-dropdown [clrCloseMenuOnItemClick]="false" class="btn btn-link" clrDropdownTrigger>
</div> <span id="action-scanner">{{'MEMBER.ACTION' | translate}}
<div [ngSwitch]="isEnabled"> <clr-icon class="clr-icon" shape="caret down"></clr-icon></span>
<button *ngSwitchCase="false" id="enable-webhook-action" class="btn btn-link" (click)="switchWebhookStatus(true)">{{'WEBHOOK.ENABLED_BUTTON' | translate}}</button> <clr-dropdown-menu *clrIfOpen>
<button *ngSwitchCase="true" id="disable-webhook-action" class="btn btn-link disabled-btn" (click)="switchWebhookStatus(false)">{{'WEBHOOK.DISABLED_BUTTON' | translate}}</button> <button clrDropdownItem (click)="switchWebhookStatus()"
</div> [disabled]="!(selectedRow && selectedRow.length === 1)">
<span *ngIf="selectedRow[0] && !selectedRow[0].enabled">
<clr-icon class="margin-top-2" size="16" shape="success-standard"></clr-icon>
<span class="margin-left-10">{{'WEBHOOK.ENABLE' | translate}}</span>
</span>
<span *ngIf="!(selectedRow[0] && !selectedRow[0].enabled)">
<clr-icon class="margin-top-2" size="16" shape="ban"></clr-icon>
<span class="margin-left-10">{{'WEBHOOK.DISABLE' | translate}}</span>
</span>
</button>
<button clrDropdownItem (click)="editWebhook()"
class="btn btn-secondary" [disabled]="!(selectedRow && selectedRow.length === 1)">
<clr-icon class="margin-top-0" size="16" shape="pencil"></clr-icon>
<span class="margin-left-10">{{'BUTTON.EDIT' | translate}}</span>
</button>
<button clrDropdownItem (click)="deleteWebhook()"
class="btn btn-secondary" [disabled]="!(selectedRow && selectedRow.length >= 1)">
<clr-icon class="margin-top-0" size="16" shape="times"></clr-icon>
<span id="delete-scanner-action"
class="margin-left-10">{{'BUTTON.DELETE' | translate}}</span>
</button>
</clr-dropdown-menu>
</clr-dropdown>
</div>
<div class="clr-col-5">
<div class="action-head-pos">
<span class="refresh-btn">
<clr-icon shape="refresh" (click)="success()" [hidden]="loadingWebhookList || loadingMetadata"></clr-icon>
</span>
</div>
</div>
</div> </div>
</div> </clr-dg-action-bar>
</div> <clr-dg-column [clrDgField]="'name'">{{'WEBHOOK.NAME' | translate}}</clr-dg-column>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 datagrid-margin-top"> <clr-dg-column>{{'WEBHOOK.NOTIFY_TYPE' | translate}}</clr-dg-column>
<clr-datagrid [clrDgLoading]="loading"> <clr-dg-column>{{'WEBHOOK.TARGET' | translate}}</clr-dg-column>
<clr-dg-column>{{'WEBHOOK.TYPE' | translate}}</clr-dg-column> <clr-dg-column>{{'WEBHOOK.ENABLED' | translate}}</clr-dg-column>
<clr-dg-column>{{'WEBHOOK.STATUS' | translate}}</clr-dg-column> <clr-dg-column>{{'WEBHOOK.EVENT_TYPES' | translate}}</clr-dg-column>
<clr-dg-column>{{'WEBHOOK.CREATED' | translate}}</clr-dg-column> <clr-dg-column>{{'WEBHOOK.CREATED' | translate}}</clr-dg-column>
<clr-dg-column>{{'WEBHOOK.LAST_TRIGGERED' | translate}}</clr-dg-column> <clr-dg-column>{{'WEBHOOK.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let item of lastTriggers"> <clr-dg-placeholder>
<clr-dg-cell>{{item.event_type}}</clr-dg-cell> {{'WEBHOOK.NO_WEBHOOK' | translate}}
<clr-dg-cell [ngSwitch]="item.enabled"> </clr-dg-placeholder>
<clr-dg-row *clrDgItems="let w of webhookList" [clrDgItem]="w">
<clr-dg-cell>{{w.name}}</clr-dg-cell>
<clr-dg-cell>{{w?.targets[0].type}}</clr-dg-cell>
<clr-dg-cell>{{w?.targets[0].address}}</clr-dg-cell>
<clr-dg-cell [ngSwitch]="w.enabled">
<div *ngSwitchCase="true" class="icon-wrap"> <div *ngSwitchCase="true" class="icon-wrap">
<clr-icon shape="check-circle" size="20" class="is-success enabled-icon"></clr-icon> <clr-icon shape="check-circle" size="20" class="is-success enabled-icon"></clr-icon>
<span>{{'WEBHOOK.ENABLED' | translate}}</span> <span>{{'WEBHOOK.ENABLED' | translate}}</span>
</div> </div>
<div *ngSwitchCase="false" class="icon-wrap"> <div *ngSwitchCase="false" class="icon-wrap">
<clr-icon shape="exclamation-triangle" size="20" class="is-warning"></clr-icon> <clr-icon shape="exclamation-triangle" size="20" class="is-warning"></clr-icon>
<span>{{'WEBHOOK.DISABLED' | translate}}</span> <span>{{'WEBHOOK.DISABLED' | translate}}</span>
</div> </div>
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell>{{item.creation_time | date: 'short'}}</clr-dg-cell> <clr-dg-cell>
<clr-dg-cell>{{item.last_trigger_time | date: 'short'}}</clr-dg-cell> <div class="cell" *ngIf="w?.event_types?.length">
</clr-dg-row> <div class="bar-state">
<clr-dg-footer> <span class="label" *ngIf="w?.event_types[0]">{{w?.event_types[0]}}</span>
<span *ngIf="lastTriggerCount">1 - {{lastTriggerCount}} {{'WEBHOOK.OF' | translate}} </span> {{lastTriggerCount}} {{'WEBHOOK.ITEMS' | translate}} </div>
<clr-dg-pagination [clrDgPageSize]="10"></clr-dg-pagination> <div class="signpost-item" [hidden]="w?.event_types?.length<=1">
</clr-dg-footer> <div class="trigger-item">
</clr-datagrid> <clr-signpost>
</div> <button class="btn btn-link font-size-20" clrSignpostTrigger>...</button>
</div> <clr-signpost-content [clrPosition]="'left-top'" *clrIfOpen>
<div *ngIf="showCreate"> <div>
<p class="create-text pt-1">{{'WEBHOOK.CREATE_WEBHOOK_DESC' | translate}}</p> <div *ngFor="let e of w?.event_types" class="bar-state">
<add-webhook-form class="webhook-form-wrap" [projectId]="projectId" [webhook]="webhook" [isModify]="false" (edit)="editWebhook($event)"></add-webhook-form> <span class="label not-scan">{{e}}</span>
</div> </div>
<add-webhook [projectId]="projectId" [webhook]="webhook" (modify)="editWebhook($event)"></add-webhook> </div>
<confirmation-dialog #confirmationDialogComponent (confirmAction)="confirmSwitch($event)"></confirmation-dialog> </clr-signpost-content>
</clr-signpost>
</div>
</div>
</div>
</clr-dg-cell>
<clr-dg-cell>{{w.creation_time | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{w.description}}</clr-dg-cell>
<clr-dg-row-detail *clrIfExpanded>
<last-trigger class="w-100" [webhookName]="w.name" *clrIfExpanded [inputLastTriggers]="lastTriggers"></last-trigger>
</clr-dg-row-detail>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="webhookList?.length > 0">1 - {{webhookList?.length}} {{'WEBHOOK.OF' | translate}} </span> {{webhookList?.length}} {{'WEBHOOK.ITEMS' | translate}}
<clr-dg-pagination [clrDgPageSize]="10"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div> </div>
<add-webhook (notify)="success()" [metadata]="metadata" [projectId]="projectId"></add-webhook>
<confirmation-dialog #confirmationDialogComponent (confirmAction)="confirmSwitch($event)"></confirmation-dialog>

View File

@ -39,3 +39,19 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.cell {
display: flex;
align-items: center;
width: 100%;
height: 100%;
}
.font-size-20 {
font-size: 20px;
}
.action-head-pos {
padding-right: 18px;
height: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
}

View File

@ -11,26 +11,69 @@ import { ClarityModule } from '@clr/angular';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing';
import { delay } from "rxjs/operators";
import { Webhook } from "./webhook";
import { AddWebhookFormComponent } from "./add-webhook-form/add-webhook-form.component";
import { InlineAlertComponent } from "../../shared/inline-alert/inline-alert.component";
import { AddWebhookComponent } from "./add-webhook/add-webhook.component";
import { ConfirmationDialogComponent } from "../../../lib/components/confirmation-dialog";
describe('WebhookComponent', () => { describe('WebhookComponent', () => {
let component: WebhookComponent; let component: WebhookComponent;
let fixture: ComponentFixture<WebhookComponent>; let fixture: ComponentFixture<WebhookComponent>;
const mockMessageHandlerService = { const mockMessageHandlerService = {
handleError: () => { } handleError: () => { }
}; };
const mockedMetadata = {
"event_type": [
"projectQuota",
"pullImage",
"scanningFailed",
"uploadChart",
"deleteChart",
"downloadChart",
"scanningCompleted",
"pushImage",
"deleteImage"
],
"notify_type": [
"http",
"slack"
]
};
const mockedWehook: Webhook = {
id: 1,
project_id: 1,
name: 'test',
description: 'just a test webhook',
targets: [{
address: 'https://test.com',
type: 'http',
attachment: null,
auth_header: null,
skip_cert_verify: true,
}],
event_types: [
'projectQuota'
],
creator: null,
creation_time: null,
update_time: null,
enabled: true,
};
const mockWebhookService = { const mockWebhookService = {
listLastTrigger: () => { listLastTrigger: () => {
return of([]); return of([]).pipe(delay(0));
}, },
listWebhook: () => { listWebhook: () => {
return of([ return of([mockedWehook
{ ]).pipe(delay(0));
targets: [
{ address: "" }
],
enabled: true
}
]);
}, },
getWebhookMetadata() {
return of(mockedMetadata).pipe(delay(0));
},
editWebhook() {
return of(true);
}
}; };
const mockActivatedRoute = { const mockActivatedRoute = {
RouterparamMap: of({ get: (key) => 'value' }), RouterparamMap: of({ get: (key) => 'value' }),
@ -61,7 +104,12 @@ describe('WebhookComponent', () => {
NoopAnimationsModule, NoopAnimationsModule,
HttpClientTestingModule HttpClientTestingModule
], ],
declarations: [WebhookComponent], declarations: [WebhookComponent,
AddWebhookComponent,
AddWebhookFormComponent,
InlineAlertComponent,
ConfirmationDialogComponent
],
providers: [ providers: [
TranslateService, TranslateService,
{ provide: WebhookService, useValue: mockWebhookService }, { provide: WebhookService, useValue: mockWebhookService },
@ -72,13 +120,69 @@ describe('WebhookComponent', () => {
.compileComponents(); .compileComponents();
})); }));
beforeEach(() => { beforeEach(async () => {
fixture = TestBed.createComponent(WebhookComponent); fixture = TestBed.createComponent(WebhookComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.autoDetectChanges(true);
await fixture.whenStable();
}); });
it('should create', () => { it('should create', async () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should get webhook list', async () => {
const rows = fixture.nativeElement.getElementsByTagName('clr-dg-row');
expect(rows.length).toEqual(1);
});
it('should open modal', async () => {
component.newWebhook();
fixture.detectChanges();
await fixture.whenStable();
const body: HTMLElement = fixture.nativeElement.querySelector(".modal-body");
expect(body).toBeTruthy();
const title: HTMLElement = fixture.nativeElement.querySelector(".modal-title");
expect(title.innerText).toEqual('WEBHOOK.ADD_WEBHOOK');
});
it('should open edit modal', async () => {
component.webhookList[0].name = 'test';
component.selectedRow[0] = component.webhookList[0];
component.editWebhook();
fixture.detectChanges();
await fixture.whenStable();
const body: HTMLElement = fixture.nativeElement.querySelector(".modal-body");
expect(body).toBeTruthy();
const title: HTMLElement = fixture.nativeElement.querySelector(".modal-title");
expect(title.innerText).toEqual('WEBHOOK.EDIT_WEBHOOK');
const nameInput: HTMLInputElement = fixture.nativeElement.querySelector("#name");
expect(nameInput.value).toEqual('test');
});
it('should disable webhook', async () => {
await fixture.whenStable();
component.selectedRow[0] = component.webhookList[0];
component.webhookList[0].enabled = true;
component.switchWebhookStatus();
fixture.detectChanges();
await fixture.whenStable();
const button: HTMLButtonElement = fixture.nativeElement.querySelector("#dialog-action-disable");
button.dispatchEvent(new Event('click'));
await fixture.whenStable();
const body: HTMLElement = fixture.nativeElement.querySelector(".modal-body");
expect(body).toBeFalsy();
});
it('should enable webhook', async () => {
await fixture.whenStable();
component.webhookList[0].enabled = false;
component.selectedRow[0] = component.webhookList[0];
component.switchWebhookStatus();
fixture.detectChanges();
await fixture.whenStable();
const buttonEnable: HTMLButtonElement = fixture.nativeElement.querySelector("#dialog-action-enable");
buttonEnable.dispatchEvent(new Event('click'));
await fixture.whenStable();
const bodyEnable: HTMLElement = fixture.nativeElement.querySelector(".modal-body");
expect(bodyEnable).toBeFalsy();
});
}); });

View File

@ -28,13 +28,13 @@ import {
} from "../../shared/shared.const"; } from "../../shared/shared.const";
import { ConfirmationMessage } from "../../shared/confirmation-dialog/confirmation-message"; import { ConfirmationMessage } from "../../shared/confirmation-dialog/confirmation-message";
import { ConfirmationAcknowledgement } from "../../shared/confirmation-dialog/confirmation-state-message";
import { ConfirmationDialogComponent } from "../../shared/confirmation-dialog/confirmation-dialog.component"; import { ConfirmationDialogComponent } from "../../shared/confirmation-dialog/confirmation-dialog.component";
import { clone } from "../../../lib/utils/utils";
import { forkJoin, Observable } from "rxjs";
@Component({ @Component({
templateUrl: './webhook.component.html', templateUrl: './webhook.component.html',
styleUrls: ['./webhook.component.scss'], styleUrls: ['./webhook.component.scss']
// changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class WebhookComponent implements OnInit { export class WebhookComponent implements OnInit {
@ViewChild(AddWebhookComponent, { static: false } ) @ViewChild(AddWebhookComponent, { static: false } )
@ -43,16 +43,16 @@ export class WebhookComponent implements OnInit {
addWebhookFormComponent: AddWebhookFormComponent; addWebhookFormComponent: AddWebhookFormComponent;
@ViewChild("confirmationDialogComponent", { static: false }) @ViewChild("confirmationDialogComponent", { static: false })
confirmationDialogComponent: ConfirmationDialogComponent; confirmationDialogComponent: ConfirmationDialogComponent;
webhook: Webhook;
endpoint: string = '';
lastTriggers: LastTrigger[] = []; lastTriggers: LastTrigger[] = [];
lastTriggerCount: number = 0; lastTriggerCount: number = 0;
isEnabled: boolean;
loading: boolean = false;
showCreate: boolean = false;
loadingWebhook: boolean = true;
projectId: number; projectId: number;
projectName: string; projectName: string;
selectedRow: Webhook[] = [];
webhookList: Webhook[] = [];
metadata: any;
loadingMetadata: boolean = false;
loadingWebhookList: boolean = false;
loadingTriggers: boolean = false;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private translate: TranslateService, private translate: TranslateService,
@ -66,19 +66,34 @@ export class WebhookComponent implements OnInit {
let project = <Project>(resolverData["projectResolver"]); let project = <Project>(resolverData["projectResolver"]);
this.projectName = project.name; this.projectName = project.name;
} }
this.getData(this.projectId); this.getData();
} }
getData(projectId: number) { getData() {
this.getLastTriggers(projectId); this.getMetadata();
this.getWebhook(projectId); this.getLastTriggers();
this.getWebhooks();
this.selectedRow = [];
}
getMetadata() {
this.loadingMetadata = true;
this.webhookService.getWebhookMetadata(this.projectId)
.pipe(finalize(() => (this.loadingMetadata = false)))
.subscribe(
response => {
this.metadata = response;
},
error => {
this.messageHandlerService.handleError(error);
}
);
} }
getLastTriggers(projectId: number) { getLastTriggers() {
this.loading = true; this.loadingTriggers = true;
this.webhookService this.webhookService
.listLastTrigger(projectId) .listLastTrigger(this.projectId)
.pipe(finalize(() => (this.loading = false))) .pipe(finalize(() => (this.loadingTriggers = false)))
.subscribe( .subscribe(
response => { response => {
this.lastTriggers = response; this.lastTriggers = response;
@ -90,20 +105,14 @@ export class WebhookComponent implements OnInit {
); );
} }
getWebhook(projectId: number) { getWebhooks() {
this.loadingWebhookList = true;
this.webhookService this.webhookService
.listWebhook(projectId) .listWebhook(this.projectId)
.pipe(finalize(() => (this.loadingWebhook = false))) .pipe(finalize(() => (this.loadingWebhookList = false)))
.subscribe( .subscribe(
response => { response => {
if (response.length) { this.webhookList = response;
this.webhook = response[0];
this.endpoint = this.webhook.targets[0].address;
this.isEnabled = this.webhook.enabled;
this.showCreate = false;
} else {
this.showCreate = true;
}
}, },
error => { error => {
this.messageHandlerService.handleError(error); this.messageHandlerService.handleError(error);
@ -111,46 +120,103 @@ export class WebhookComponent implements OnInit {
); );
} }
switchWebhookStatus(enabled = false) { switchWebhookStatus() {
let content = ''; let content = '';
this.translate.get( this.translate.get(
enabled !this.selectedRow[0].enabled
? 'WEBHOOK.ENABLED_WEBHOOK_SUMMARY' ? 'WEBHOOK.ENABLED_WEBHOOK_SUMMARY'
: 'WEBHOOK.DISABLED_WEBHOOK_SUMMARY' : 'WEBHOOK.DISABLED_WEBHOOK_SUMMARY'
).subscribe((res) => content = res + this.projectName); , {name: this.selectedRow[0].name}).subscribe((res) => {
let message = new ConfirmationMessage( content = res;
enabled ? 'WEBHOOK.ENABLED_WEBHOOK_TITLE' : 'WEBHOOK.DISABLED_WEBHOOK_TITLE', let message = new ConfirmationMessage(
content, !this.selectedRow[0].enabled ? 'WEBHOOK.ENABLED_WEBHOOK_TITLE' : 'WEBHOOK.DISABLED_WEBHOOK_TITLE',
'', content,
{}, '',
ConfirmationTargets.WEBHOOK, {},
enabled ? ConfirmationButtons.ENABLE_CANCEL : ConfirmationButtons.DISABLE_CANCEL ConfirmationTargets.WEBHOOK,
); !this.selectedRow[0].enabled ? ConfirmationButtons.ENABLE_CANCEL : ConfirmationButtons.DISABLE_CANCEL
this.confirmationDialogComponent.open(message); );
this.confirmationDialogComponent.open(message);
});
} }
confirmSwitch(message: ConfirmationAcknowledgement) { confirmSwitch(message) {
if (message && if (message &&
message.source === ConfirmationTargets.WEBHOOK && message.source === ConfirmationTargets.WEBHOOK &&
message.state === ConfirmationState.CONFIRMED) { message.state === ConfirmationState.CONFIRMED) {
this.webhookService if (JSON.stringify(message.data) === '{}') {
.editWebhook(this.projectId, this.webhook.id, Object.assign({}, this.webhook, { enabled: !this.isEnabled })) this.webhookService
.subscribe( .editWebhook(this.projectId, this.selectedRow[0].id,
Object.assign({}, this.selectedRow[0], { enabled: !this.selectedRow[0].enabled }))
.subscribe(
response => {
this.getData();
},
error => {
this.messageHandlerService.handleError(error);
}
);
} else {
const observableLists: Observable<any>[] = [];
message.data.forEach(item => {
observableLists.push(this.webhookService.deleteWebhook(this.projectId, item.id));
});
forkJoin(...observableLists).subscribe(
response => { response => {
this.getData(this.projectId); this.getData();
}, },
error => { error => {
this.messageHandlerService.handleError(error); this.messageHandlerService.handleError(error);
} }
); );
}
} }
} }
editWebhook(isModify: boolean): void { editWebhook() {
this.getData(this.projectId); if (this.metadata) {
this.addWebhookComponent.isOpen = true;
this.addWebhookComponent.isEdit = true;
this.addWebhookComponent.addWebhookFormComponent.isModify = true;
this.addWebhookComponent.addWebhookFormComponent.webhook = clone(this.selectedRow[0]);
this.addWebhookComponent.addWebhookFormComponent.webhook.event_types = clone(this.selectedRow[0].event_types);
}
} }
openAddWebhookModal(): void { openAddWebhookModal(): void {
this.addWebhookComponent.openAddWebhookModal(); this.addWebhookComponent.openAddWebhookModal();
} }
newWebhook() {
if (this.metadata) {
this.addWebhookComponent.isOpen = true;
this.addWebhookComponent.isEdit = false;
this.addWebhookComponent.addWebhookFormComponent.isModify = false;
this.addWebhookComponent.addWebhookFormComponent.currentForm.reset({notifyType: this.metadata.notify_type[0]});
this.addWebhookComponent.addWebhookFormComponent.webhook = new Webhook();
this.addWebhookComponent.addWebhookFormComponent.webhook.event_types = clone(this.metadata.event_type);
}
}
success() {
this.getData();
}
deleteWebhook() {
const names: string[] = [];
this.selectedRow.forEach(item => {
names.push(item.name);
});
let content = '';
this.translate.get(
'WEBHOOK.DELETE_WEBHOOK_SUMMARY'
, {names: names.join(',')}).subscribe((res) => content = res);
const msg: ConfirmationMessage = new ConfirmationMessage(
"SCANNER.CONFIRM_DELETION",
content,
names.join(','),
this.selectedRow,
ConfirmationTargets.WEBHOOK,
ConfirmationButtons.DELETE_CANCEL
);
this.confirmationDialogComponent.open(msg);
}
} }

View File

@ -11,8 +11,8 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { throwError as observableThrowError, Observable } from "rxjs"; import { throwError as observableThrowError, Observable, of } from "rxjs";
import { map, catchError } from "rxjs/operators"; import { map, catchError, delay } from "rxjs/operators";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Webhook, LastTrigger } from "./webhook"; import { Webhook, LastTrigger } from "./webhook";
@ -42,6 +42,12 @@ export class WebhookService {
.pipe(catchError(error => observableThrowError(error))); .pipe(catchError(error => observableThrowError(error)));
} }
public deleteWebhook(projectId: number, policyId: number): Observable<any> {
return this.http
.delete(`${ CURRENT_BASE_HREF }/projects/${projectId}/webhook/policies/${policyId}`)
.pipe(catchError(error => observableThrowError(error)));
}
public createWebhook(projectId: number, data: any): Observable<any> { public createWebhook(projectId: number, data: any): Observable<any> {
return this.http return this.http
.post(`${ CURRENT_BASE_HREF }/projects/${projectId}/webhook/policies`, data) .post(`${ CURRENT_BASE_HREF }/projects/${projectId}/webhook/policies`, data)
@ -54,4 +60,10 @@ export class WebhookService {
.post(`${ CURRENT_BASE_HREF }/projects/${projectId}/webhook/policies/test`, param) .post(`${ CURRENT_BASE_HREF }/projects/${projectId}/webhook/policies/test`, param)
.pipe(catchError(error => observableThrowError(error))); .pipe(catchError(error => observableThrowError(error)));
} }
public getWebhookMetadata(projectId: number): Observable<any> {
return this.http
.get(`${CURRENT_BASE_HREF}/projects/${projectId}/webhook/events`)
.pipe(catchError(error => observableThrowError(error)));
}
} }

View File

@ -1,16 +1,20 @@
import { WebhookEventTypes } from '../../shared/shared.const';
export class Webhook { export class Webhook {
id: number; id: number;
name: string; name: string;
project_id: number; project_id: number;
description: string; description: string;
targets: Target[]; targets: Target[];
event_types: WebhookEventTypes[]; event_types: string[];
creator: string; creator: string;
creation_time: Date; creation_time: Date;
update_time: Date; update_time: Date;
enabled: boolean; enabled: boolean;
constructor () {
this.targets = [];
this.targets.push(new Target());
this.event_types = [];
this.enabled = true;
}
} }
export class Target { export class Target {
@ -28,6 +32,7 @@ export class Target {
} }
export class LastTrigger { export class LastTrigger {
policy_name: string;
enabled: boolean; enabled: boolean;
event_type: string; event_type: string;
creation_time: Date; creation_time: Date;

View File

@ -99,15 +99,3 @@ export enum ResourceType {
CHART_VERSION = 2, CHART_VERSION = 2,
REPOSITORY_TAG = 3, REPOSITORY_TAG = 3,
} }
export enum WebhookEventTypes {
DOWNLOAD_CHART = "downloadChart",
DELETE_CHART = "deleteChart",
UPLOAD_CHART = "uploadChart",
DELETE_IMAGE = "deleteImage",
PULL_IMAGE = "pullImage",
PUSH_IMAGE = "pushImage",
SCANNING_FAILED = "scanningFailed",
SCANNING_COMPLETED = "scanningCompleted",
PROJECT_QUOTA = "projectQuota",
}

View File

@ -363,7 +363,8 @@
"OF": "of", "OF": "of",
"ITEMS": "items", "ITEMS": "items",
"LAST_TRIGGERED": "Last Triggered", "LAST_TRIGGERED": "Last Triggered",
"EDIT_WEBHOOK": "Webhook Endpoint", "EDIT_WEBHOOK": "Edit Webhook",
"ADD_WEBHOOK": "Add Webhook",
"CREATE_WEBHOOK": "Getting started with webhooks", "CREATE_WEBHOOK": "Getting started with webhooks",
"EDIT_WEBHOOK_DESC": "Specify the endpoint for receiving webhook notifications", "EDIT_WEBHOOK_DESC": "Specify the endpoint for receiving webhook notifications",
"CREATE_WEBHOOK_DESC": "To get started with webhooks, provide an endpoint and credentials to access the webhook server.", "CREATE_WEBHOOK_DESC": "To get started with webhooks, provide an endpoint and credentials to access the webhook server.",
@ -377,10 +378,28 @@
"SAVE_BUTTON": "SAVE", "SAVE_BUTTON": "SAVE",
"TEST_ENDPOINT_SUCCESS": "Connection tested successfully.", "TEST_ENDPOINT_SUCCESS": "Connection tested successfully.",
"TEST_ENDPOINT_FAILURE": "Failed to ping endpoint.", "TEST_ENDPOINT_FAILURE": "Failed to ping endpoint.",
"ENABLED_WEBHOOK_TITLE": "Enable Project Webhooks", "ENABLED_WEBHOOK_TITLE": "Enable Webhook",
"ENABLED_WEBHOOK_SUMMARY": "Do you want to enable webhooks for project ", "ENABLED_WEBHOOK_SUMMARY": "Do you want to enable webhook {{name}}?",
"DISABLED_WEBHOOK_TITLE": "Disable Project Webhooks", "DISABLED_WEBHOOK_TITLE": "Disable Webhook",
"DISABLED_WEBHOOK_SUMMARY": "Do you want to disable webhooks for project " "DISABLED_WEBHOOK_SUMMARY": "Do you want to disable webhook {{name}}?",
"DELETE_WEBHOOK_TITLE": "Delete Webhook(s)",
"DELETE_WEBHOOK_SUMMARY": "Do you want to delete webhook(s) {{names}}?",
"WEBHOOKS": "Webhooks",
"NEW_WEBHOOK": "New Webhook",
"ENABLE": "Enable",
"DISABLE": "Disable",
"NAME": "Name",
"TARGET": "Target",
"EVENT_TYPES": "Event types",
"DESCRIPTION": "Description",
"NO_WEBHOOK": "No Webhook",
"LAST_TRIGGER": "Last Trigger",
"WEBHOOK_NAME": "Webhook Name",
"NO_TRIGGER": "No Trigger",
"NAME_REQUIRED": "Name is required",
"NOTIFY_TYPE": "Notify Type",
"EVENT_TYPE": "Event Type",
"EVENT_TYPE_REQUIRED": "Require at least one event type"
}, },
"GROUP": { "GROUP": {
"GROUP": "Group", "GROUP": "Group",

View File

@ -364,7 +364,8 @@
"OF": "of", "OF": "of",
"ITEMS": "items", "ITEMS": "items",
"LAST_TRIGGERED": "Last Triggered", "LAST_TRIGGERED": "Last Triggered",
"EDIT_WEBHOOK": "Webhook Endpoint", "EDIT_WEBHOOK": "Edit Webhook",
"ADD_WEBHOOK": "Add Webhook",
"CREATE_WEBHOOK": "Getting started with webhooks", "CREATE_WEBHOOK": "Getting started with webhooks",
"EDIT_WEBHOOK_DESC": "Specify the endpoint for receiving webhook notifications", "EDIT_WEBHOOK_DESC": "Specify the endpoint for receiving webhook notifications",
"CREATE_WEBHOOK_DESC": "To get started with webhooks, provide an endpoint and credentials to access the webhook server.", "CREATE_WEBHOOK_DESC": "To get started with webhooks, provide an endpoint and credentials to access the webhook server.",
@ -378,10 +379,28 @@
"SAVE_BUTTON": "SAVE", "SAVE_BUTTON": "SAVE",
"TEST_ENDPOINT_SUCCESS": "Connection tested successfully.", "TEST_ENDPOINT_SUCCESS": "Connection tested successfully.",
"TEST_ENDPOINT_FAILURE": "Failed to ping endpoint.", "TEST_ENDPOINT_FAILURE": "Failed to ping endpoint.",
"ENABLED_WEBHOOK_TITLE": "Enable Project Webhooks", "ENABLED_WEBHOOK_TITLE": "Enable Webhook",
"ENABLED_WEBHOOK_SUMMARY": "Do you want to enable webhooks for project ", "ENABLED_WEBHOOK_SUMMARY": "Do you want to enable webhook {{name}}?",
"DISABLED_WEBHOOK_TITLE": "Disable Project Webhooks", "DISABLED_WEBHOOK_TITLE": "Disable Webhook",
"DISABLED_WEBHOOK_SUMMARY": "Do you want to disable webhooks for project " "DISABLED_WEBHOOK_SUMMARY": "Do you want to disable webhook {{name}}?",
"DELETE_WEBHOOK_TITLE": "Delete Webhook(s)",
"DELETE_WEBHOOK_SUMMARY": "Do you want to delete webhook(s) {{names}}?",
"WEBHOOKS": "Webhooks",
"NEW_WEBHOOK": "New Webhook",
"ENABLE": "Enable",
"DISABLE": "Disable",
"NAME": "Name",
"TARGET": "Target",
"EVENT_TYPES": "Event types",
"DESCRIPTION": "Description",
"NO_WEBHOOK": "No Webhook",
"LAST_TRIGGER": "Last Trigger",
"WEBHOOK_NAME": "Webhook Name",
"NO_TRIGGER": "No Trigger",
"NAME_REQUIRED": "Name is required",
"NOTIFY_TYPE": "Notify Type",
"EVENT_TYPE": "Event Type",
"EVENT_TYPE_REQUIRED": "Require at least one event type"
}, },
"GROUP": { "GROUP": {
"GROUP": "Group", "GROUP": "Group",

View File

@ -355,7 +355,8 @@
"OF": "of", "OF": "of",
"ITEMS": "items", "ITEMS": "items",
"LAST_TRIGGERED": "Last Triggered", "LAST_TRIGGERED": "Last Triggered",
"EDIT_WEBHOOK": "Webhook Endpoint", "EDIT_WEBHOOK": "Edit Webhook",
"ADD_WEBHOOK": "Add Webhook",
"CREATE_WEBHOOK": "Getting started with webhooks", "CREATE_WEBHOOK": "Getting started with webhooks",
"EDIT_WEBHOOK_DESC": "Specify the endpoint for receiving webhook notifications", "EDIT_WEBHOOK_DESC": "Specify the endpoint for receiving webhook notifications",
"CREATE_WEBHOOK_DESC": "To get started with webhooks, provide an endpoint and credentials to access the webhook server.", "CREATE_WEBHOOK_DESC": "To get started with webhooks, provide an endpoint and credentials to access the webhook server.",
@ -369,10 +370,28 @@
"SAVE_BUTTON": "SAVE", "SAVE_BUTTON": "SAVE",
"TEST_ENDPOINT_SUCCESS": "Connection tested successfully.", "TEST_ENDPOINT_SUCCESS": "Connection tested successfully.",
"TEST_ENDPOINT_FAILURE": "Failed to ping endpoint.", "TEST_ENDPOINT_FAILURE": "Failed to ping endpoint.",
"ENABLED_WEBHOOK_TITLE": "Enable Project Webhooks", "ENABLED_WEBHOOK_TITLE": "Enable Webhook",
"ENABLED_WEBHOOK_SUMMARY": "Do you want to enable webhooks for project ", "ENABLED_WEBHOOK_SUMMARY": "Do you want to enable webhook {{name}}?",
"DISABLED_WEBHOOK_TITLE": "Disable Project Webhooks", "DISABLED_WEBHOOK_TITLE": "Disable Webhook",
"DISABLED_WEBHOOK_SUMMARY": "Do you want to disable webhooks for project " "DISABLED_WEBHOOK_SUMMARY": "Do you want to disable webhook {{name}}?",
"DELETE_WEBHOOK_TITLE": "Delete Webhook(s)",
"DELETE_WEBHOOK_SUMMARY": "Do you want to delete webhook(s) {{names}}?",
"WEBHOOKS": "Webhooks",
"NEW_WEBHOOK": "New Webhook",
"ENABLE": "Enable",
"DISABLE": "Disable",
"NAME": "Name",
"TARGET": "Target",
"EVENT_TYPES": "Event types",
"DESCRIPTION": "Description",
"NO_WEBHOOK": "No Webhook",
"LAST_TRIGGER": "Last Trigger",
"WEBHOOK_NAME": "Webhook Name",
"NO_TRIGGER": "No Trigger",
"NAME_REQUIRED": "Name is required",
"NOTIFY_TYPE": "Notify Type",
"EVENT_TYPE": "Event Type",
"EVENT_TYPE_REQUIRED": "Require at least one event type"
}, },
"GROUP": { "GROUP": {
"Group": "Group", "Group": "Group",

View File

@ -393,7 +393,8 @@
"OF": "of", "OF": "of",
"ITEMS": "items", "ITEMS": "items",
"LAST_TRIGGERED": "Last Triggered", "LAST_TRIGGERED": "Last Triggered",
"EDIT_WEBHOOK": "Webhook Endpoint", "EDIT_WEBHOOK": "Edit Webhook",
"ADD_WEBHOOK": "Add Webhook",
"CREATE_WEBHOOK": "Getting started with webhooks", "CREATE_WEBHOOK": "Getting started with webhooks",
"EDIT_WEBHOOK_DESC": "Specify the endpoint for receiving webhook notifications", "EDIT_WEBHOOK_DESC": "Specify the endpoint for receiving webhook notifications",
"CREATE_WEBHOOK_DESC": "To get started with webhooks, provide an endpoint and credentials to access the webhook server.", "CREATE_WEBHOOK_DESC": "To get started with webhooks, provide an endpoint and credentials to access the webhook server.",
@ -407,10 +408,28 @@
"SAVE_BUTTON": "SAVE", "SAVE_BUTTON": "SAVE",
"TEST_ENDPOINT_SUCCESS": "Connection tested successfully.", "TEST_ENDPOINT_SUCCESS": "Connection tested successfully.",
"TEST_ENDPOINT_FAILURE": "Failed to ping endpoint.", "TEST_ENDPOINT_FAILURE": "Failed to ping endpoint.",
"ENABLED_WEBHOOK_TITLE": "Enable Project Webhooks", "ENABLED_WEBHOOK_TITLE": "Enable Webhook",
"ENABLED_WEBHOOK_SUMMARY": "Do you want to enable webhooks for project ", "ENABLED_WEBHOOK_SUMMARY": "Do you want to enable webhook {{name}}?",
"DISABLED_WEBHOOK_TITLE": "Disable Project Webhooks", "DISABLED_WEBHOOK_TITLE": "Disable Webhook",
"DISABLED_WEBHOOK_SUMMARY": "Do you want to disable webhooks for project " "DISABLED_WEBHOOK_SUMMARY": "Do you want to disable webhook {{name}}?",
"DELETE_WEBHOOK_TITLE": "Delete Webhook(s)",
"DELETE_WEBHOOK_SUMMARY": "Do you want to delete webhook(s) {{names}}?",
"WEBHOOKS": "Webhooks",
"NEW_WEBHOOK": "New Webhook",
"ENABLE": "Enable",
"DISABLE": "Disable",
"NAME": "Name",
"TARGET": "Target",
"EVENT_TYPES": "Event types",
"DESCRIPTION": "Description",
"NO_WEBHOOK": "No Webhook",
"LAST_TRIGGER": "Last Trigger",
"WEBHOOK_NAME": "Webhook Name",
"NO_TRIGGER": "No Trigger",
"NAME_REQUIRED": "Name is required",
"NOTIFY_TYPE": "Notify Type",
"EVENT_TYPE": "Event Type",
"EVENT_TYPE_REQUIRED": "Require at least one event type"
}, },
"AUDIT_LOG": { "AUDIT_LOG": {
"USERNAME": "Nome do usuário", "USERNAME": "Nome do usuário",

View File

@ -363,7 +363,8 @@
"OF": "of", "OF": "of",
"ITEMS": "adetler", "ITEMS": "adetler",
"LAST_TRIGGERED": "Son Tetiklenen", "LAST_TRIGGERED": "Son Tetiklenen",
"EDIT_WEBHOOK": "Ağ Kancası Uç Noktası", "EDIT_WEBHOOK": "Edit Webhook",
"ADD_WEBHOOK": "Add Webhook",
"CREATE_WEBHOOK": "Ağ kancaları ile başladı", "CREATE_WEBHOOK": "Ağ kancaları ile başladı",
"EDIT_WEBHOOK_DESC": "Ağ kancası bildirimleri almak için bitiş noktasını belirtin", "EDIT_WEBHOOK_DESC": "Ağ kancası bildirimleri almak için bitiş noktasını belirtin",
"CREATE_WEBHOOK_DESC": "Ağ kancalarına başlamak için, web kanca sunucusuna erişmek için bir uç nokta ve kimlik bilgisi sağlayın.", "CREATE_WEBHOOK_DESC": "Ağ kancalarına başlamak için, web kanca sunucusuna erişmek için bir uç nokta ve kimlik bilgisi sağlayın.",
@ -377,10 +378,28 @@
"SAVE_BUTTON": "KAYDET", "SAVE_BUTTON": "KAYDET",
"TEST_ENDPOINT_SUCCESS": "Connection tested successfully.", "TEST_ENDPOINT_SUCCESS": "Connection tested successfully.",
"TEST_ENDPOINT_FAILURE": "Failed to ping endpoint.", "TEST_ENDPOINT_FAILURE": "Failed to ping endpoint.",
"ENABLED_WEBHOOK_TITLE": "Proje Ağ Kancalarını Etkinleştir", "ENABLED_WEBHOOK_TITLE": "Enable Webhook",
"ENABLED_WEBHOOK_SUMMARY": "Proje için ağ kancalarını etkinleştirmek istiyor musunuz?", "ENABLED_WEBHOOK_SUMMARY": "Do you want to enable webhook {{name}}?",
"DISABLED_WEBHOOK_TITLE": "Proje Ağ kancalarını Devre Dışı Bırak", "DISABLED_WEBHOOK_TITLE": "Disable Webhook",
"DISABLED_WEBHOOK_SUMMARY": "Proje için ağ kancalarını devre dışı bırakmak istiyor musunuz?" "DISABLED_WEBHOOK_SUMMARY": "Do you want to disable webhook {{name}}?",
"DELETE_WEBHOOK_TITLE": "Delete Webhook(s)",
"DELETE_WEBHOOK_SUMMARY": "Do you want to delete webhook(s) {{names}}?",
"WEBHOOKS": "Webhooks",
"NEW_WEBHOOK": "New Webhook",
"ENABLE": "Enable",
"DISABLE": "Disable",
"NAME": "Name",
"TARGET": "Target",
"EVENT_TYPES": "Event types",
"DESCRIPTION": "Description",
"NO_WEBHOOK": "No Webhook",
"LAST_TRIGGER": "Last Trigger",
"WEBHOOK_NAME": "Webhook Name",
"NO_TRIGGER": "No Trigger",
"NAME_REQUIRED": "Name is required",
"NOTIFY_TYPE": "Notify Type",
"EVENT_TYPE": "Event Type",
"EVENT_TYPE_REQUIRED": "Require at least one event type"
}, },
"GROUP": { "GROUP": {
"GROUP": "Grup", "GROUP": "Grup",

View File

@ -361,8 +361,9 @@
"DISABLED": "停用", "DISABLED": "停用",
"OF": "共计", "OF": "共计",
"ITEMS": "条记录", "ITEMS": "条记录",
"LAST_TRIGGERED": "最近触发事件", "LAST_TRIGGERED": "最近触发时间",
"EDIT_WEBHOOK": "Webhook 目标", "EDIT_WEBHOOK": "编辑 Webhook",
"ADD_WEBHOOK": "新建 Webhook",
"CREATE_WEBHOOK": "创建 Webhooks", "CREATE_WEBHOOK": "创建 Webhooks",
"EDIT_WEBHOOK_DESC": "指定接收 Webhook 通知的目标", "EDIT_WEBHOOK_DESC": "指定接收 Webhook 通知的目标",
"CREATE_WEBHOOK_DESC": "为了启用 webhook, 请提供 Endpoint 和凭据以访问 Webhook 服务器。", "CREATE_WEBHOOK_DESC": "为了启用 webhook, 请提供 Endpoint 和凭据以访问 Webhook 服务器。",
@ -376,10 +377,28 @@
"SAVE_BUTTON": "保存", "SAVE_BUTTON": "保存",
"TEST_ENDPOINT_SUCCESS": "测试连接成功。", "TEST_ENDPOINT_SUCCESS": "测试连接成功。",
"TEST_ENDPOINT_FAILURE": "测试连接失败。", "TEST_ENDPOINT_FAILURE": "测试连接失败。",
"ENABLED_WEBHOOK_TITLE": "启用项目的 Webhooks", "ENABLED_WEBHOOK_TITLE": "启用 Webhook",
"ENABLED_WEBHOOK_SUMMARY": "你希望开启项目的 Webhooks 吗?", "ENABLED_WEBHOOK_SUMMARY": "确认启用 webhook {{name}}",
"DISABLED_WEBHOOK_TITLE": "停用项目的 Webhooks", "DISABLED_WEBHOOK_TITLE": "停用 Webhook",
"DISABLED_WEBHOOK_SUMMARY": "你希望停用项目的 Webhooks 吗?" "DISABLED_WEBHOOK_SUMMARY": "确认停用 webhook {{name}}",
"DELETE_WEBHOOK_TITLE": "删除 Webhook(s)",
"DELETE_WEBHOOK_SUMMARY": "确认删除 webhook(s) {{names}}?",
"WEBHOOKS": "Webhooks",
"NEW_WEBHOOK": "新建 Webhook",
"ENABLE": "启用",
"DISABLE": "禁用",
"NAME": "名称",
"TARGET": "目标地址",
"EVENT_TYPES": "事件类型",
"DESCRIPTION": "简介",
"NO_WEBHOOK": "暂无 Webhook 记录",
"LAST_TRIGGER": "最新触发",
"WEBHOOK_NAME": "Webhook 名称",
"NO_TRIGGER": "暂无触发记录",
"NAME_REQUIRED": "名称为必填项",
"NOTIFY_TYPE": "通知类型",
"EVENT_TYPE": "事件类型",
"EVENT_TYPE_REQUIRED": "请至少选择一种事件类型"
}, },
"GROUP": { "GROUP": {
"GROUP": "组", "GROUP": "组",